diff --git a/.planning/phases/02-frontend-cite-and-export-affordances/02-01-PLAN.md b/.planning/phases/02-frontend-cite-and-export-affordances/02-01-PLAN.md
new file mode 100644
index 0000000..c9d2ee7
--- /dev/null
+++ b/.planning/phases/02-frontend-cite-and-export-affordances/02-01-PLAN.md
@@ -0,0 +1,123 @@
+---
+phase: 02-frontend-cite-and-export-affordances
+plan: "02-01"
+type: tdd
+wave: 1
+depends_on: []
+files_modified:
+ - src/lib/export-single.js
+ - src/lib/export-single.test.js
+autonomous: true
+requirements:
+ - REQ-FE-02
+ - REQ-FE-06
+
+must_haves:
+ truths:
+ - "buildSingleRisContent(cslItem) returns a valid RIS string for one entry"
+ - "buildSingleBibtexContent(cslItem) returns a valid BibTeX string for one entry"
+ - "buildSingleCslJsonContent(cslItem) returns a valid JSON string for one entry"
+ - "Functions accept a non-DOI entry (thesis) without errors"
+ - "A reader can receive a single-entry export in RIS, BibTeX, or CSL-JSON format by clicking a per-entry button"
+ artifacts:
+ - path: "src/lib/export-single.js"
+ provides: "Per-entry export helpers (RIS, BibTeX, CSL-JSON)"
+ exports:
+ - buildSingleRisContent
+ - buildSingleBibtexContent
+ - buildSingleCslJsonContent
+ - path: "src/lib/export-single.test.js"
+ provides: "Unit tests for all three per-entry helpers"
+ min_lines: 60
+ key_links:
+ - from: "src/lib/export-single.js"
+ to: "src/lib/export.js"
+ via: "buildRisExportContent, buildBibtexExportContent imports"
+ pattern: "from './export'"
+---
+
+
+Create the per-entry export helpers that the frontend view script will call to generate BibTeX, RIS, and CSL-JSON content for a single citation.
+
+Purpose: The existing export.js functions operate on arrays with sorting. The frontend needs thin, single-entry wrappers that accept one CSL-JSON object and return the formatted export string without sort side-effects.
+Output: src/lib/export-single.js with three tested functions; src/lib/export-single.test.js with full unit coverage.
+
+
+
+@/Users/danknauss/.claude/get-shit-done/workflows/execute-plan.md
+@/Users/danknauss/.claude/get-shit-done/templates/summary.md
+
+
+
+@.planning/PROJECT.md
+@.planning/ROADMAP.md
+@.planning/STATE.md
+@.planning/phases/02-frontend-cite-and-export-affordances/02-RESEARCH.md
+
+
+
+
+```javascript
+// src/lib/export.js — relevant exports
+
+export function buildRisExportContent(citations, citationStyle)
+// citations: Array of { id: string, csl: Object } — pass [{id: cslItem.id || 'citation', csl: cslItem}]
+// citationStyle: string — for single entry pass any valid style; sort has no effect on one item
+// returns: string (RIS formatted, synchronous)
+
+export async function buildBibtexExportContent(citations, citationStyle, { CiteCtor } = {})
+// Same citation record shape; async (loads @citation-js/core dynamically)
+// CiteCtor: optional test injection to replace Cite class
+// returns: Promise
+
+export function buildCslJsonExportContent(citations, citationStyle)
+// returns: string (JSON array with trailing newline)
+
+export const RIS_EXPORT_MIME_TYPE
+export const BIBTEX_EXPORT_MIME_TYPE
+export const CSL_JSON_EXPORT_MIME_TYPE
+```
+
+
+
+
+ Per-entry export helpers
+ src/lib/export-single.js, src/lib/export-single.test.js
+
+ - buildSingleRisContent({ type: 'thesis', title: 'My Thesis', id: 'thesis-1' }) returns string starting with 'TY - THES'
+ - buildSingleRisContent({ type: 'article-journal', title: 'Paper', DOI: '10.1/x', id: 'j1' }) contains 'DO - 10.1/x'
+ - buildSingleRisContent for non-DOI entry returns valid RIS (no crash, no empty string)
+ - buildSingleBibtexContent({ type: 'book', title: 'Book Title', id: 'b1' }) resolves to string containing '@book'
+ - buildSingleBibtexContent accepts CiteCtor injection for testing (no real @citation-js load)
+ - buildSingleCslJsonContent({ type: 'article-journal', title: 'T', id: 'c1' }) returns JSON string parseable as array of length 1
+ - buildSingleCslJsonContent preserves all fields from the input cslItem
+ - All three functions accept a cslItem with no id property (fall back to 'citation' as id)
+
+
+ TDD red-green-refactor. Tests first, then minimal implementation.
+
+ src/lib/export-single.js:
+ - Import buildRisExportContent, buildBibtexExportContent, buildCslJsonExportContent from './export'
+ - buildSingleRisContent(cslItem): wraps cslItem as [{ id: cslItem.id || 'citation', csl: cslItem }], calls buildRisExportContent with 'chicago-notes-bibliography' style, returns result
+ - buildSingleBibtexContent(cslItem, { CiteCtor } = {}): same wrapping, calls buildBibtexExportContent async, passes CiteCtor through for test injection
+ - buildSingleCslJsonContent(cslItem): returns JSON.stringify([cslItem], null, 2) + '\n' directly (no sort needed, simpler than going through buildCslJsonExportContent which wraps into citation records)
+
+ Note on buildSingleCslJsonContent: The base buildCslJsonExportContent maps citation.csl — for single-entry frontend use it is simpler and more predictable to serialize the cslItem array directly. This avoids the citation-record wrapping round-trip and is consistent with what the frontend embed already stores.
+
+
+
+
+npm test -- --testPathPattern="export-single" --passWithNoTests=false
+
+
+
+- src/lib/export-single.js exports buildSingleRisContent, buildSingleBibtexContent, buildSingleCslJsonContent
+- All unit tests pass (red commit then green commit per TDD cycle)
+- buildSingleRisContent is synchronous; buildSingleBibtexContent is async
+- Non-DOI thesis entry produces valid RIS output without errors
+- CiteCtor injection works in buildSingleBibtexContent (no real @citation-js load in tests)
+
+
+
diff --git a/.planning/phases/02-frontend-cite-and-export-affordances/02-02-PLAN.md b/.planning/phases/02-frontend-cite-and-export-affordances/02-02-PLAN.md
new file mode 100644
index 0000000..a8f4680
--- /dev/null
+++ b/.planning/phases/02-frontend-cite-and-export-affordances/02-02-PLAN.md
@@ -0,0 +1,200 @@
+---
+phase: 02-frontend-cite-and-export-affordances
+plan: "02-02"
+type: tdd
+wave: 1
+depends_on: []
+files_modified:
+ - src/save-markup.js
+ - block.json
+ - webpack.config.js
+autonomous: true
+requirements:
+ - REQ-FE-01
+ - REQ-FE-03
+ - REQ-FE-04
+ - REQ-FE-05
+
+must_haves:
+ truths:
+ - "Each
in rendered save markup has a data-csl attribute containing escaped JSON"
+ - "data-csl JSON contains the full csl object for that entry"
+ - "Angle-bracket characters in title fields are escaped as \\u003c in data-csl"
+ - "block.json declares viewScript pointing to build/view.js"
+ - "webpack.config.js includes a 'view' entry for src/view.js"
+ - "Save markup without JS is fully readable (bibliography text unaffected)"
+ artifacts:
+ - path: "src/save-markup.js"
+ provides: "data-csl attribute on each "
+ contains: "data-csl"
+ - path: "block.json"
+ provides: "viewScript registration"
+ contains: "viewScript"
+ - path: "webpack.config.js"
+ provides: "view entry point"
+ contains: "view:"
+ key_links:
+ - from: "block.json viewScript"
+ to: "build/view.js"
+ via: "file:./build/view.js reference"
+ pattern: "viewScript.*file:.*build/view"
+ - from: "src/save-markup.js "
+ to: "data-csl attribute"
+ via: "JSON.stringify(citation.csl).replace"
+ pattern: "data-csl"
+---
+
+
+Embed per-entry CSL-JSON into save markup and wire the viewScript entry point so WordPress will auto-enqueue the frontend script when the block is present.
+
+Purpose: The view.js (Plan 03) needs two things from this plan: (1) CSL-JSON data co-located with each bibliography entry in saved HTML, and (2) WordPress to auto-enqueue view.js only on pages containing the block.
+Output: Modified save-markup.js with data-csl on each li; block.json with viewScript; webpack.config.js with view entry. No render_callback added.
+
+
+
+@/Users/danknauss/.claude/get-shit-done/workflows/execute-plan.md
+@/Users/danknauss/.claude/get-shit-done/templates/summary.md
+
+
+
+@.planning/PROJECT.md
+@.planning/ROADMAP.md
+@.planning/phases/02-frontend-cite-and-export-affordances/02-RESEARCH.md
+@src/save-markup.js
+@src/save.test.js
+
+
+
+
+```jsx
+
+```
+
+
+
+
+
+"editorScript": "file:./build/index.js",
+"editorStyle": "file:./build/index.css",
+"style": "file:./build/style-index.css"
+
+
+
+entry: async () => {
+ return {
+ ...base,
+ validation: path.resolve(__dirname, 'src/validation.js'),
+ };
+}
+
+
+
+
+
+
+
+ Task 1: Add data-csl attribute to save-markup li elements
+ src/save-markup.js, src/save.test.js
+
+ - save output for a single citation includes data-csl attribute on the li element
+ - data-csl value is a valid JSON object matching the citation's csl property
+ - a citation with title containing angle brackets (e.g. "A title") has data-csl with < escaped as \\u003c
+ - the existing bibliography text content is unchanged (no regression)
+ - citations without csl.language still render (lang attribute remains optional)
+
+
+ In src/save-markup.js, inside the renderBibliographySave function, add a data-csl attribute to each li element:
+
+ ```jsx
+
+ ```
+
+ The .replace(/
+
+ npm test -- --testPathPattern="src/save.test" --passWithNoTests=false
+
+
+ - src/save-markup.js has data-csl on every li
+ - New tests pass: data-csl present, JSON parses correctly, angle brackets escaped as \\u003c
+ - All existing save.test.js tests still pass
+
+
+
+
+ Task 2: Wire viewScript in block.json and webpack.config.js
+ block.json, webpack.config.js
+
+ 1. In block.json, add "viewScript" field after "style":
+ "viewScript": "file:./build/view.js"
+
+ WordPress 6.1+ auto-enqueues this script on frontend pages where the block is present. The generated handle is "bibliography-builder/bibliography-view-script". No PHP enqueue code is needed.
+
+ 2. In webpack.config.js, add the view entry to the entry async function:
+ ```js
+ entry: async () => {
+ const base = typeof defaultEntry === 'function' ? await defaultEntry() : defaultEntry;
+ return {
+ ...base,
+ validation: path.resolve(__dirname, 'src/validation.js'),
+ view: path.resolve(__dirname, 'src/view.js'),
+ };
+ },
+ ```
+
+ Note: src/view.js does not exist yet (Plan 03 creates it). webpack will fail to build until view.js is created. This is expected — Plan 03 runs in Wave 2 after this plan. The build does not need to pass until Plan 03 completes.
+
+ The viewScript field in block.json tells WordPress to load build/view.js only when the block is rendered on a page. This is zero-overhead for pages without bibliography blocks and requires no PHP changes — consistent with the no-render_callback requirement.
+
+
+ node -e "const b=require('./block.json'); if(!b.viewScript) process.exit(1); console.log('viewScript:', b.viewScript)" && node -e "require('./webpack.config.js')" && echo 'webpack config parses'
+
+
+ - block.json has "viewScript": "file:./build/view.js"
+ - webpack.config.js entry async function includes view: path.resolve(__dirname, 'src/view.js')
+ - webpack.config.js is syntactically valid (node -e require parses without error)
+ - No PHP files modified
+
+
+
+
+
+
+Run after both tasks:
+- `npm test -- --testPathPattern="src/save.test"` — all save tests pass including new data-csl cases
+- `node -e "const b=require('./block.json'); console.log('viewScript:', b.viewScript)"` — prints the viewScript value
+- `node -e "require('./webpack.config.js')" && echo 'webpack config parses'` — webpack config is syntactically valid
+- `grep -n "view:" webpack.config.js` — shows the view entry
+
+
+
+- Every li in save markup output has data-csl with valid, \\u003c-escaped JSON
+- block.json viewScript field is present and points to file:./build/view.js
+- webpack.config.js entry includes view entry and is syntactically valid
+- All existing save tests still pass, new data-csl tests pass
+- No PHP files changed, no render_callback added
+
+
+
diff --git a/.planning/phases/02-frontend-cite-and-export-affordances/02-03-PLAN.md b/.planning/phases/02-frontend-cite-and-export-affordances/02-03-PLAN.md
new file mode 100644
index 0000000..2356b06
--- /dev/null
+++ b/.planning/phases/02-frontend-cite-and-export-affordances/02-03-PLAN.md
@@ -0,0 +1,282 @@
+---
+phase: 02-frontend-cite-and-export-affordances
+plan: "02-03"
+type: tdd
+wave: 2
+depends_on:
+ - "02-01"
+ - "02-02"
+files_modified:
+ - src/view.js
+ - src/view.test.js
+ - src/style.scss
+autonomous: true
+requirements:
+ - REQ-FE-01
+ - REQ-FE-02
+ - REQ-FE-04
+ - REQ-FE-05
+ - REQ-FE-06
+
+must_haves:
+ truths:
+ - "view.js reads data-csl from each li and appends a cite panel"
+ - "The cite panel has Copy RIS, Download RIS, Download BibTeX, and Download CSL-JSON buttons"
+ - "bibliography-builder-js-ready class is added to the block wrapper after panels are injected"
+ - "li elements without data-csl are silently skipped (old saved blocks work)"
+ - "Malformed data-csl JSON does not throw — entry is skipped"
+ - "CSS hides .bibliography-builder-cite-controls until .bibliography-builder-js-ready is present"
+ artifacts:
+ - path: "src/view.js"
+ provides: "Frontend DOMContentLoaded hydration script"
+ exports: []
+ - path: "src/view.test.js"
+ provides: "JSDOM unit tests for cite panel injection and progressive enhancement"
+ min_lines: 80
+ - path: "src/style.scss"
+ provides: "Progressive enhancement CSS for cite controls"
+ contains: "bibliography-builder-cite-controls"
+ key_links:
+ - from: "src/view.js"
+ to: "src/lib/export-single.js"
+ via: "buildSingleRisContent, buildSingleBibtexContent, buildSingleCslJsonContent imports"
+ pattern: "from './lib/export-single'"
+ - from: "src/view.js"
+ to: "src/lib/clipboard.js"
+ via: "copyTextToClipboard import"
+ pattern: "from './lib/clipboard'"
+ - from: "src/view.js"
+ to: "src/lib/export.js"
+ via: "downloadTextExport, MIME type constants imports"
+ pattern: "from './lib/export'"
+ - from: "CSS .bibliography-builder-js-ready"
+ to: ".bibliography-builder-cite-controls visibility"
+ via: "parent class selector"
+ pattern: "bibliography-builder-js-ready.*cite-controls"
+---
+
+
+Create the frontend viewScript (view.js) that hydrates bibliography entries with Cite/Export controls, and add the progressive-enhancement CSS to style.scss.
+
+Purpose: This is the user-visible deliverable of Phase 2. When a reader visits a page with a bibliography block, JS adds per-entry "Cite / Export" disclosure panels with copy and download buttons for RIS, BibTeX, and CSL-JSON formats. Without JS, the bibliography renders identically to before.
+Output: src/view.js (DOMContentLoaded hydration), src/view.test.js (JSDOM unit tests), src/style.scss additions.
+
+
+
+@/Users/danknauss/.claude/get-shit-done/workflows/execute-plan.md
+@/Users/danknauss/.claude/get-shit-done/templates/summary.md
+
+
+
+@.planning/PROJECT.md
+@.planning/ROADMAP.md
+@.planning/phases/02-frontend-cite-and-export-affordances/02-RESEARCH.md
+@.planning/phases/02-frontend-cite-and-export-affordances/02-01-SUMMARY.md
+@.planning/phases/02-frontend-cite-and-export-affordances/02-02-SUMMARY.md
+
+
+
+
+```javascript
+export function buildSingleRisContent(cslItem)
+// synchronous; returns RIS string for one CSL item
+
+export async function buildSingleBibtexContent(cslItem, { CiteCtor } = {})
+// async (dynamic import of @citation-js); returns BibTeX string
+
+export function buildSingleCslJsonContent(cslItem)
+// synchronous; returns JSON array string with trailing newline
+```
+
+
+
+```javascript
+export async function copyTextToClipboard(text, { navigatorRef, documentRef } = {})
+// returns true on success; throws if clipboard unavailable
+```
+
+
+
+```javascript
+export function downloadTextExport({ content, filename, mimeType }, { documentRef, urlRef, BlobCtor } = {})
+export const RIS_EXPORT_MIME_TYPE // 'application/x-research-info-systems;charset=utf-8'
+export const BIBTEX_EXPORT_MIME_TYPE // 'text/x-bibtex;charset=utf-8'
+export const CSL_JSON_EXPORT_MIME_TYPE // 'application/vnd.citationstyles.csl+json;charset=utf-8'
+```
+
+
+
+
+
+
+
+
+
+ Frontend cite panel hydration (view.js)
+ src/view.js, src/view.test.js
+
+ DOM injection (unit-testable via JSDOM):
+ - buildCitePanel(cslItem) returns a element with class 'bibliography-builder-cite-controls'
+ - The contains a with text 'Cite / Export'
+ - The contains a with buttons
+ - Buttons present: 'Copy RIS', 'Copy BibTeX', 'Download .ris', 'Download .bib', 'Download CSL-JSON'
+ - Each button has type="button"
+
+ Hydration (unit-testable via JSDOM with mocked document):
+ - hydrateBibliography(container) finds all li[data-csl] within the container and appends cite panels
+ - After hydrateBibliography, the container has class 'bibliography-builder-js-ready'
+ - li elements without data-csl are skipped (no panel appended)
+ - li with invalid JSON in data-csl is skipped (no panel, no throw)
+ - li with null or non-object JSON in data-csl is skipped
+
+ Button behavior (unit tests use stubs for downloadTextExport and copyTextToClipboard):
+ - 'Copy RIS' click calls copyTextToClipboard with buildSingleRisContent(cslItem) result
+ - 'Download .ris' click calls downloadTextExport with RIS content and RIS MIME type
+ - 'Download .bib' click awaits buildSingleBibtexContent then calls downloadTextExport
+ - 'Download CSL-JSON' click calls downloadTextExport with CSL-JSON content and CSL-JSON MIME type
+
+
+ TDD red-green-refactor.
+
+ IMPORTANT: Export buildCitePanel and hydrateBibliography as named exports from view.js so they can be tested in isolation. The DOMContentLoaded listener at module scope is the only non-exported code.
+
+ src/view.js structure:
+ 1. Imports: copyTextToClipboard from ./lib/clipboard; downloadTextExport, RIS/BIBTEX/CSL_JSON_EXPORT_MIME_TYPE from ./lib/export; buildSingleRisContent, buildSingleBibtexContent, buildSingleCslJsonContent from ./lib/export-single
+ 2. export function buildCitePanel(cslItem, { copyFn, downloadFn } = {}) — creates and returns the element. Accept copyFn and downloadFn as injectable dependencies for testing (default to copyTextToClipboard and downloadTextExport).
+ 3. export function hydrateBibliography(container, { copyFn, downloadFn } = {}) — finds li[data-csl], parses JSON, appends panel, marks container js-ready.
+ 4. DOMContentLoaded listener calls hydrateBibliography on each .wp-block-bibliography-builder-bibliography element.
+
+ src/view.test.js: Use JSDOM (Jest's default jsdom environment). Mock imports:
+ - jest.mock('./lib/export-single', ...) with sync stubs that return predictable strings
+ - jest.mock('./lib/clipboard', ...) with a stub returning Promise.resolve(true)
+ - jest.mock('./lib/export', ...) returning a stub downloadTextExport and the real MIME type constants
+
+ i18n note: Button labels are hardcoded English in this phase. The open question about data-i18n attributes is deferred — hardcoded labels are acceptable for Phase 2. Track as follow-on.
+
+
+
+
+
+
+ Task 1: TDD — view.js cite panel and hydration logic
+ src/view.js, src/view.test.js
+
+ Per the feature behavior block above — run RED commit then GREEN commit.
+
+
+ Follow TDD cycle strictly:
+
+ RED: Write src/view.test.js with:
+ - Tests for buildCitePanel(cslItem) DOM structure (class, summary text, button count/labels, button types)
+ - Tests for hydrateBibliography(container): panels added to li[data-csl] entries, js-ready class added, li without data-csl skipped, invalid JSON skipped
+ - Tests for button click behavior using injected copyFn/downloadFn stubs (no real async imports needed)
+
+ Mock strategy in test file:
+ ```js
+ jest.mock('./lib/export-single', () => ({
+ buildSingleRisContent: jest.fn(() => 'RIS_CONTENT'),
+ buildSingleBibtexContent: jest.fn(async () => 'BIBTEX_CONTENT'),
+ buildSingleCslJsonContent: jest.fn(() => 'CSL_CONTENT'),
+ }));
+ jest.mock('./lib/clipboard', () => ({
+ copyTextToClipboard: jest.fn(async () => true),
+ }));
+ jest.mock('./lib/export', () => ({
+ downloadTextExport: jest.fn(),
+ RIS_EXPORT_MIME_TYPE: 'application/x-research-info-systems;charset=utf-8',
+ BIBTEX_EXPORT_MIME_TYPE: 'text/x-bibtex;charset=utf-8',
+ CSL_JSON_EXPORT_MIME_TYPE: 'application/vnd.citationstyles.csl+json;charset=utf-8',
+ }));
+ ```
+
+ Run `npm test -- --testPathPattern="src/view.test"` — tests must FAIL.
+ Commit: test(02-03): add failing tests for view.js cite panel hydration
+
+ GREEN: Create src/view.js with buildCitePanel and hydrateBibliography exported, plus DOMContentLoaded listener.
+ Run tests — must PASS.
+ Commit: feat(02-03): implement view.js cite panel hydration
+
+ The DOMContentLoaded listener is not tested directly — it is glue code calling the tested hydrateBibliography function.
+
+
+ npm test -- --testPathPattern="src/view.test" --passWithNoTests=false
+
+
+ - src/view.js exports buildCitePanel and hydrateBibliography
+ - src/view.test.js has tests covering DOM structure, hydration, skip-on-missing-data-csl, skip-on-invalid-JSON, button behavior
+ - All view.test.js tests pass
+ - RED commit then GREEN commit both present
+
+
+
+
+ Task 2: Progressive enhancement CSS in style.scss
+ src/style.scss
+
+ Append to src/style.scss (do not replace existing content):
+
+ ```scss
+ // Cite / Export panel — progressive enhancement
+ // Controls are hidden by default; view.js adds .bibliography-builder-js-ready
+ // to reveal them only when JS has run successfully.
+ .bibliography-builder-cite-controls {
+ display: none;
+ }
+
+ .bibliography-builder-js-ready .bibliography-builder-cite-controls {
+ display: block;
+ }
+
+ .bibliography-builder-cite-panel {
+ display: flex;
+ flex-wrap: wrap;
+ gap: 0.5em;
+ padding: 0.5em 0;
+ }
+
+ .bibliography-builder-cite-panel button {
+ font-size: 0.875em;
+ cursor: pointer;
+ }
+ ```
+
+ This CSS ensures:
+ - No cite controls are visible when JS is disabled (REQ-FE-04: no-JS bibliography fully readable)
+ - Controls appear only after view.js adds bibliography-builder-js-ready (REQ-FE-05: deactivation-resilient)
+ - When plugin is deactivated, bibliography-builder-js-ready is never added, controls remain hidden
+ - Existing bibliography styles are unaffected (additive-only change)
+
+
+ npm run build && echo "Build clean"
+
+
+ - src/style.scss includes .bibliography-builder-cite-controls { display: none }
+ - src/style.scss includes .bibliography-builder-js-ready .bibliography-builder-cite-controls rules
+ - npm run build completes without errors
+ - build/view.js is present in the build output
+ - build/style-index.css includes the cite control rules
+
+
+
+
+
+
+After both tasks:
+- `npm test` — full suite passes (export-single + view + save tests all green)
+- `npm run build` — build/view.js present, no build errors
+- `grep -n "bibliography-builder-cite-controls" build/style-index.css` — CSS rule present in output
+- `ls build/view.js` — viewScript compiled artifact exists
+
+
+
+- src/view.js exists, exports buildCitePanel and hydrateBibliography, has DOMContentLoaded listener
+- src/view.test.js passes: DOM structure, hydration, skip logic, button wiring all covered
+- src/style.scss hides cite controls by default, reveals under js-ready parent
+- npm run build succeeds with build/view.js in output
+- Full Jest suite green
+- No PHP files modified
+
+
+
diff --git a/.planning/phases/02-frontend-cite-and-export-affordances/02-RESEARCH.md b/.planning/phases/02-frontend-cite-and-export-affordances/02-RESEARCH.md
new file mode 100644
index 0000000..03d10d2
--- /dev/null
+++ b/.planning/phases/02-frontend-cite-and-export-affordances/02-RESEARCH.md
@@ -0,0 +1,560 @@
+# Phase 2: Frontend Cite and Export Affordances - Research
+
+**Researched:** 2026-06-04
+**Domain:** WordPress static block, progressive-enhancement frontend interactivity, citation export (BibTeX/RIS/CSL-JSON), Clipboard API, Blob download
+**Confidence:** HIGH
+
+---
+
+
+## Phase Requirements
+
+| ID | Description | Research Support |
+|----|-------------|-----------------|
+| REQ-FE-01 | Visible per-entry Cite/Export UI on the public bibliography frontend | viewScript + data-attribute hydration pattern; Google Scholar-style cite affordance |
+| REQ-FE-02 | BibTeX, RIS, and CSL-JSON download/copy per entry | `buildBibtexExportContent`, `buildRisExportContent`, `buildCslJsonExportContent` in `src/lib/export.js` are single-entry capable after trivial per-entry extraction; Blob+anchor download pattern |
+| REQ-FE-03 | No render_callback — all output stays in static save() | viewScript hydrates HTML already saved by save.js; no PHP render required for JS-layer behavior |
+| REQ-FE-04 | No-JS bibliography remains fully readable | Existing `` + `` structure is complete text; cite/export controls are additive DOM elements hidden until JS runs or styled as graceful degradation |
+| REQ-FE-05 | Preserves plugin-deactivation resilience | Citation data embedded in `data-csl` attributes is inert HTML; controls can be hidden by default (CSS: `display:none`) and shown by JS; deactivation removes JS but leaves readable bibliography markup intact |
+| REQ-FE-06 | Mendeley/Zotero manual-import acceptance | Visible per-entry RIS and BibTeX download buttons directly serve the gap confirmed in Mendeley testing (non-DOI entries missed by auto-detection) |
+
+
+---
+
+## Summary
+
+This phase adds optional Scholar-like Cite/Export controls to the already-saved static bibliography frontend. The plugin uses a static `save()` function — no PHP `render_callback` — which constrains the approach: all interactive behavior must come from JavaScript that hydrates the saved HTML at runtime.
+
+The cleanest solution for this project is a `viewScript` frontend bundle registered in `block.json`. WordPress automatically enqueues `viewScript` only when the block is present on a page (since WP 6.1), making it zero-cost for posts without bibliography blocks. The script reads per-entry CSL-JSON data from attributes embedded during save and renders Cite/Export controls progressively. The WordPress Interactivity API is available but not required; it introduces a `data-wp-interactive` namespace coupling into saved block markup that becomes dead HTML if the plugin is deactivated — contrary to the deactivation-resilience requirement. Vanilla JS with `data-csl` attributes is the preferred approach.
+
+The existing `src/lib/export.js` already contains `buildBibtexExportContent`, `buildRisExportContent`, `buildCslJsonExportContent`, and `buildCslJsonExportContent`. These functions operate on arrays of citations and sort them by style; they need a thin wrapper to operate on a single CSL item. The existing `copyTextToClipboard` in `src/lib/clipboard.js` already handles the `navigator.clipboard` + `execCommand` fallback and is injectable for testing.
+
+**Primary recommendation:** Add a `viewScript` file (`src/view.js`) that reads per-entry CSL-JSON from `data-csl` attributes, builds a per-entry modal/details panel with Copy and Download buttons (BibTeX, RIS, CSL-JSON), and uses the existing export utilities. Embed per-entry CSL-JSON into the save markup using a `data-csl` attribute on each `
`. Keep controls hidden with CSS until JS initialises them.
+
+---
+
+## Standard Stack
+
+### Core
+| Library | Version | Purpose | Why Standard |
+|---------|---------|---------|--------------|
+| `@citation-js/core` | 0.7.18 (already in deps) | BibTeX generation | Already bundled; `buildBibtexExportContent` uses it async |
+| `@citation-js/plugin-bibtex` | 0.7.18 (already in deps) | BibTeX/BibLaTeX format | Already bundled async chunk `citation-plugin-bibtex.js` |
+| WordPress `viewScript` | WP 6.1+ | Frontend-only JS enqueue | Auto-enqueued when block present; zero overhead otherwise |
+| Clipboard API (`navigator.clipboard`) | Baseline (2025) | Copy text to clipboard | `copyTextToClipboard` in `clipboard.js` already wraps it with fallback |
+
+### Supporting
+| Library | Version | Purpose | When to Use |
+|---------|---------|---------|-------------|
+| `Blob` + `URL.createObjectURL` | Baseline | File download in browser | Already used by `downloadTextExport` in `export.js` |
+| ``/`` HTML | HTML5 native | No-JS-accessible disclosure widget | Use as the cite panel toggle; degrades to always-open without CSS |
+
+### Alternatives Considered
+| Instead of | Could Use | Tradeoff |
+|------------|-----------|----------|
+| `viewScript` + vanilla JS | WordPress Interactivity API (`viewScriptModule`) | Interactivity API requires `data-wp-interactive` in saved markup, which becomes dead/inert HTML post-deactivation. Also requires WP 6.5+ minimum. Not worth the constraint for this use case. |
+| `data-csl` attribute per `` | Existing `