---
phase: 28-dashboard-shell-context-detection
plan: 01
type: execute
wave: 1
depends_on: []
files_modified:
  - paperforge/plugin/main.js
  - paperforge/plugin/styles.css
autonomous: true
requirements:
  - DASH-01  # Index loading required for collection mode lookup
  - DASH-02  # _findEntry(key) required for per-paper mode
  - REFR-01  # _loadIndex() required for index refresh

must_haves:
  truths:
    - "PaperForgeStatusView can load and parse formal-library.json via _loadIndex() returning the parsed JSON object or null on failure"
    - "PaperForgeStatusView can look up a single paper entry by zotero_key via _findEntry(key) returning the entry or null"
    - "PaperForgeStatusView can filter papers by domain name via _filterByDomain(domain) returning an array of matching entries"
    - "PaperForgeStatusView has a lazy-loaded cached index via _getCachedIndex() that returns items array after first call"
    - "styles.css has a .paperforge-content-area class for the mode-switched content region"
    - "styles.css has a .paperforge-mode-context class for the header mode indicator area"
  artifacts:
    - path: "paperforge/plugin/main.js"
      provides: "Index loading utilities: _loadIndex, _getCachedIndex, _findEntry, _filterByDomain"
      min_lines: 50  # new JS content for 4 methods
    - path: "paperforge/plugin/styles.css"
      provides: "CSS for mode-aware header and content layout"
      min_lines: 60  # new CSS for header mode context + content area
  key_links:
    - from: "main.js PaperForgeStatusView._loadIndex"
      to: "formal-library.json path"
      via: "app.vault.adapter.basePath + plugin.settings.system_dir + 'PaperForge/indexes/formal-library.json'"
    - from: "main.js PaperForgeStatusView._findEntry"
      to: "_getCachedIndex() items array"
      via: "Array.find(item => item.zotero_key === key)"
    - from: "main.js PaperForgeStatusView._filterByDomain"
      to: "_getCachedIndex() items array"
      via: "Array.filter(item => item.domain === domain)"
---

<objective>
Add index loading utility methods to `PaperForgeStatusView` in `paperforge/plugin/main.js` and add CSS for the mode-aware dashboard layout in `paperforge/plugin/styles.css`. These are the foundational building blocks that Plan 28-02 (Context Detection & Mode Switching) depends on.

Purpose: Establish data access layer and visual shell so Plan 28-02 can focus purely on context detection logic and mode switching without getting bogged down in index I/O or layout CSS.

Output: `paperforge/plugin/main.js` with 4 new methods; `paperforge/plugin/styles.css` with 2 new CSS sections.
</objective>

<execution_context>
@C:/Users/Lin/.opencode/get-shit-done/workflows/execute-plan.md
@C:/Users/Lin/.opencode/get-shit-done/templates/summary.md
</execution_context>

<context>
@.planning/phases/28-dashboard-shell-context-detection/28-CONTEXT.md
@paperforge/plugin/main.js
@paperforge/plugin/styles.css
</context>

<tasks>

<task type="auto">
  <name>Task 1: Add index loading utilities (_loadIndex, _getCachedIndex, _findEntry, _filterByDomain)</name>
  <files>paperforge/plugin/main.js</files>

  <read_first>
  - paperforge/plugin/main.js — PaperForgeStatusView class (lines 210-815), especially the existing _fetchStats() method at lines 285-375 which already reads formal-library.json with path resolution at lines 294-297
  - .planning/phases/28-dashboard-shell-context-detection/28-CONTEXT.md (decisions D-11, D-12, D-13, D-14, D-16, D-17, D-19)
  </read_first>

  <action>
  Add FOUR methods to the `PaperForgeStatusView` class in `paperforge/plugin/main.js`.

  **Insertion point:** Add these methods after the `_buildMetricBar` method (currently around line 399) and before the `_renderStats` method (currently around line 402). This keeps all data-loading utilities grouped together, before the render methods.

  **Method 1: `_loadIndex()` — reads and parses formal-library.json (D-11, D-17, D-19)**

  ```javascript
  /* ── Index Loading (D-11, D-17, D-19) ── */
  _loadIndex() {
      const vp = this.app.vault.adapter.basePath;
      const plugin = this.app.plugins.plugins['paperforge'];
      const systemDir = plugin?.settings?.system_dir || '99_System';
      const indexPath = path.join(vp, systemDir, 'PaperForge', 'indexes', 'formal-library.json');
      try {
          const raw = fs.readFileSync(indexPath, 'utf-8');
          return JSON.parse(raw);
      } catch {
          return null;
      }
  }
  ```

  Uses the same path resolution pattern already established in `_fetchStats()` (lines 294-297 of current main.js). Does NOT cache — always reads from disk. Returns `null` (not an empty object) on failure so callers can distinguish "file missing/corrupt" from "empty index" per D-17.

  **Method 2: `_getCachedIndex()` — lazy-load and cache the items array (D-14)**

  ```javascript
  /* ── Cached Index Accessor (D-14) ── */
  _getCachedIndex() {
      if (!this._cachedItems) {
          const index = this._loadIndex();
          this._cachedItems = index ? (index.items || []) : [];
      }
      return this._cachedItems;
  }
  ```

  - First call loads from disk and caches in `this._cachedItems`
  - Subsequent calls return cached array without disk I/O
  - Returns empty array (not null) when index is missing
  - Cached items must be invalidated by setting `this._cachedItems = null` when a refresh is needed (done in Plan 28-02)

  **Method 3: `_findEntry(key)` — lookup single paper by zotero_key (D-12, D-18)**

  ```javascript
  /* ── Single Paper Lookup by Key (D-12, D-18) ── */
  _findEntry(key) {
      if (!key) return null;
      return this._getCachedIndex().find(item => item.zotero_key === key) || null;
  }
  ```

  - Returns the first matching entry, or `null` if not found (per D-18)
  - Returns `null` when key is falsy (empty/undefined/null)

  **Method 4: `_filterByDomain(domain)` — filter index items by domain (D-13, D-16)**

  ```javascript
  /* ── Filter Papers by Domain (D-13, D-16) ── */
  _filterByDomain(domain) {
      if (!domain) return [];
      return this._getCachedIndex().filter(item => item.domain === domain);
  }
  ```

  - Returns array of matching items
  - Returns empty array when domain is falsy

  **IMPORTANT:** Do NOT modify the existing `_fetchStats()` method. It currently has its own inline path resolution — leave it as-is to minimize risk. The new `_loadIndex()` duplicates path resolution intentionally (self-contained, no hidden coupling).
  </action>

  <acceptance_criteria>
  - `main.js` contains a method `_loadIndex()` that reads from `formal-library.json` using path.join with `basePath`, `system_dir`, `PaperForge`, `indexes`, `formal-library.json`
  - `_loadIndex()` returns `null` inside the `catch` block (D-17: null means missing/corrupt)
  - `main.js` contains a method `_getCachedIndex()` that stores result in `this._cachedItems`
  - `_getCachedIndex()` returns `[]` (not null) when `_loadIndex()` returns null
  - `main.js` contains a method `_findEntry(key)` using `Array.find(item => item.zotero_key === key)`
  - `_findEntry(key)` returns `null` when key is falsy or not found
  - `main.js` contains a method `_filterByDomain(domain)` using `Array.filter(item => item.domain === domain)`
  - `_filterByDomain(domain)` returns `[]` when domain is falsy
  - All 4 methods use proper class method syntax (not arrow functions assigned to `this`)
  - grep: `_loadIndex(` exists, `_getCachedIndex(` exists, `_findEntry(` exists, `_filterByDomain(` exists
  - grep: `this._cachedItems` appears at least twice (assignment + read)
  - grep: `item.zotero_key === key` appears in _findEntry
  - grep: `item.domain === domain` appears in _filterByDomain
  </acceptance_criteria>

  <verify>
  <automated>python -c "
with open('paperforge/plugin/main.js') as f:
    c = f.read()
assert '_loadIndex(' in c, 'Missing _loadIndex method'
assert '_getCachedIndex(' in c, 'Missing _getCachedIndex method'
assert '_findEntry(' in c, 'Missing _findEntry method'
assert '_filterByDomain(' in c, 'Missing _filterByDomain method'
assert 'this._cachedItems' in c, 'Missing _cachedItems caching'
assert 'item.zotero_key === key' in c, 'Missing zotero_key lookup'
assert 'item.domain === domain' in c, 'Missing domain filter'
assert 'formal-library.json' in c, 'Missing formal-library.json path reference'
assert 'JSON.parse(raw)' in c, 'Missing JSON parse'
assert 'return null' in c or 'return null;' in c, 'Missing null return on error'
print('Task 1 index utilities: ALL CHECKS PASSED')
"</automated>
  </verify>

  <done>
  PaperForgeStatusView has four new methods: _loadIndex() (reads formal-library.json, returns parsed object or null), _getCachedIndex() (lazy-loads and caches items array), _findEntry(key) (lookup by zotero_key, returns entry or null), _filterByDomain(domain) (filters items by domain). All methods handle null/empty inputs gracefully. Cached index invalidatable via this._cachedItems = null.
  </done>
</task>

<task type="auto">
  <name>Task 2: Add CSS for mode-aware header context and content area layout</name>
  <files>paperforge/plugin/styles.css</files>

  <read_first>
  - paperforge/plugin/styles.css (existing file, to append after Section 12 at the end)
  - .planning/phases/28-dashboard-shell-context-detection/28-CONTEXT.md (decisions D-06, D-07, D-15)
  </read_first>

  <action>
  Append TWO new CSS sections to `paperforge/plugin/styles.css` at the end of the file (after the last line, currently Section 12 — Bar Chart ending at line 1048).

  **Section 13 — Mode-Aware Content Area (D-06)**

  This section provides the container that gets cleared and repopulated on each mode switch:

  ```css
  /* ==========================================================================
     SECTION 13 — Mode-Aware Content Area
     ========================================================================== */
  .paperforge-content-area {
      display: flex;
      flex-direction: column;
      gap: 24px;
      transition: opacity 0.2s ease;
  }

  .paperforge-content-area.switching {
      opacity: 0.4;
      pointer-events: none;
  }

  .paperforge-content-placeholder {
      text-align: center;
      padding: 32px 12px;
      color: var(--text-muted);
      font-size: var(--font-ui-small);
      border: 1px dashed var(--background-modifier-border);
      border-radius: var(--radius-m);
      background: var(--background-secondary);
      font-style: italic;
  }
  ```

  **Section 14 — Mode-Aware Header Context (D-07, D-15)**

  This section styles the mode context area in the dashboard header that shows the current mode's identifier (paper title, domain name, or "PaperForge"):

  ```css
  /* ==========================================================================
     SECTION 14 — Mode-Aware Header Context
     ========================================================================== */
  .paperforge-mode-context {
      display: flex;
      align-items: center;
      gap: 8px;
      min-width: 0;
      flex: 1;
  }

  .paperforge-mode-badge {
      font-size: 10px;
      font-weight: var(--font-semibold);
      padding: 2px 8px;
      border-radius: 10px;
      text-transform: uppercase;
      letter-spacing: 0.5px;
      line-height: 1.4;
      flex-shrink: 0;
  }

  .paperforge-mode-badge.global {
      background: var(--interactive-accent);
      color: var(--text-on-accent);
  }

  .paperforge-mode-badge.paper {
      background: var(--color-cyan);
      color: var(--text-on-accent);
  }

  .paperforge-mode-badge.collection {
      background: var(--color-purple);
      color: var(--text-on-accent);
  }

  .paperforge-mode-name {
      font-size: var(--font-ui-small);
      font-weight: var(--font-semibold);
      color: var(--text-normal);
      white-space: nowrap;
      overflow: hidden;
      text-overflow: ellipsis;
      min-width: 0;
      line-height: 1.3;
  }

  .paperforge-mode-warning {
      font-size: var(--font-ui-smaller);
      color: var(--text-warning);
      margin-left: auto;
  }
  ```

  **IMPORTANT:** Add BOTH sections at the very end of styles.css, separated by a blank line. Do not modify any existing CSS. The sections must be appended in order (Section 13 first, then Section 14).
  </action>

  <acceptance_criteria>
  - `styles.css` contains a new Section 13 comment `/* ===== SECTION 13 — Mode-Aware Content Area ===== */`
  - `styles.css` contains `.paperforge-content-area` with `flex-direction: column` and `gap: 24px`
  - `styles.css` contains `.paperforge-content-area.switching` with `opacity: 0.4` and `pointer-events: none`
  - `styles.css` contains `.paperforge-content-placeholder` with `border: 1px dashed`, `border-radius: var(--radius-m)`, `font-style: italic`
  - `styles.css` contains a new Section 14 comment `/* ===== SECTION 14 — Mode-Aware Header Context ===== */`
  - `styles.css` contains `.paperforge-mode-context` with `display: flex`, `align-items: center`, `flex: 1`
  - `styles.css` contains `.paperforge-mode-badge.global` with `var(--interactive-accent)`
  - `styles.css` contains `.paperforge-mode-badge.paper` with `var(--color-cyan)`
  - `styles.css` contains `.paperforge-mode-badge.collection` with `var(--color-purple)`
  - `styles.css` contains `.paperforge-mode-name` with `text-overflow: ellipsis` for truncation
  - `styles.css` contains `.paperforge-mode-warning` with `var(--text-warning)`
  - All colors use `var(--*)` — zero hardcoded hex/rgb values
  - File length increases by at least 60 lines (two new sections)
  </acceptance_criteria>

  <verify>
  <automated>python -c "
with open('paperforge/plugin/styles.css') as f:
    c = f.read()
assert 'SECTION 13' in c, 'Missing Section 13 header'
assert '.paperforge-content-area' in c, 'Missing content area class'
assert 'flex-direction: column' in c, 'Missing flex column'
assert 'opacity: 0.4' in c, 'Missing switching opacity'
assert '.paperforge-content-placeholder' in c, 'Missing placeholder class'
assert 'SECTION 14' in c, 'Missing Section 14 header'
assert '.paperforge-mode-context' in c, 'Missing mode context class'
assert '.paperforge-mode-badge.global' in c, 'Missing global badge'
assert '.paperforge-mode-badge.paper' in c, 'Missing paper badge'
assert '.paperforge-mode-badge.collection' in c, 'Missing collection badge'
assert '.paperforge-mode-name' in c, 'Missing mode name class'
assert '.paperforge-mode-warning' in c, 'Missing mode warning class'
assert 'var(--text-warning)' in c, 'Hardcoded warning color'
print('Task 2 CSS mode shell: ALL CHECKS PASSED')
"</automated>
  </verify>

  <done>
  styles.css has two new sections: Section 13 (content area container with switching fade transition and placeholder styling) and Section 14 (mode-aware header context with color-coded mode badges, truncated mode name, and warning text). All colors use Obsidian CSS variables. Existing CSS untouched.
  </done>
</task>

</tasks>

<verification>
1. 4 new index utility methods exist on PaperForgeStatusView: `_loadIndex()`, `_getCachedIndex()`, `_findEntry(key)`, `_filterByDomain(domain)`
2. All 4 methods handle null/empty inputs without throwing
3. `_loadIndex()` returns `null` on file read/parse failure (distinguishable from empty array)
4. `_getCachedIndex()` stores cached items in `this._cachedItems` for invalidation
5. All path resolution uses the same pattern as existing `_fetchStats()`: `basePath + system_dir + 'PaperForge/indexes/formal-library.json'`
6. styles.css has Section 13 and Section 14 appended at end
7. Content area has switching state with opacity fade
8. Placeholder styled as dashed border container for Phase 29/30 content
9. Mode badges have 3 color states: global (accent), paper (cyan), collection (purple)
10. Mode name truncates with `text-overflow: ellipsis`
</verification>

<success_criteria>
- [ ] main.js has `_loadIndex()` that reads formal-library.json via the established path pattern, returns parsed JSON or null
- [ ] main.js has `_getCachedIndex()` that lazy-loads and caches items in `this._cachedItems`
- [ ] main.js has `_findEntry(key)` that looks up by zotero_key using Array.find, returns entry or null
- [ ] main.js has `_filterByDomain(domain)` that filters by domain using Array.filter, returns array or []
- [ ] All 4 methods are class methods on PaperForgeStatusView
- [ ] Existing `_fetchStats()` is untouched (still has its own path resolution)
- [ ] styles.css has Section 13: `.paperforge-content-area` with switching state
- [ ] styles.css has Section 14: `.paperforge-mode-context` with `.paperforge-mode-badge.global/paper/collection`, `.paperforge-mode-name`, `.paperforge-mode-warning`
- [ ] All new CSS uses Obsidian CSS variables only
- [ ] Existing CSS unchanged
</success_criteria>

<output>
After completion, create `.planning/phases/28-dashboard-shell-context-detection/28-01-SUMMARY.md`
</output>
