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
deleted file mode 100644
index c9d2ee7..0000000
--- a/.planning/phases/02-frontend-cite-and-export-affordances/02-01-PLAN.md
+++ /dev/null
@@ -1,123 +0,0 @@
----
-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
deleted file mode 100644
index a8f4680..0000000
--- a/.planning/phases/02-frontend-cite-and-export-affordances/02-02-PLAN.md
+++ /dev/null
@@ -1,200 +0,0 @@
----
-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
deleted file mode 100644
index 2356b06..0000000
--- a/.planning/phases/02-frontend-cite-and-export-affordances/02-03-PLAN.md
+++ /dev/null
@@ -1,282 +0,0 @@
----
-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
deleted file mode 100644
index 03d10d2..0000000
--- a/.planning/phases/02-frontend-cite-and-export-affordances/02-RESEARCH.md
+++ /dev/null
@@ -1,560 +0,0 @@
-# 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 `