@deskwork/studio 0.12.1 → 0.14.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (131) hide show
  1. package/dist/components/scrapbook-item.d.ts +9 -1
  2. package/dist/components/scrapbook-item.d.ts.map +1 -1
  3. package/dist/components/scrapbook-item.js +11 -2
  4. package/dist/components/scrapbook-item.js.map +1 -1
  5. package/dist/data/glossary.json +62 -0
  6. package/dist/lib/glossary-helper.d.ts +16 -0
  7. package/dist/lib/glossary-helper.d.ts.map +1 -0
  8. package/dist/lib/glossary-helper.js +26 -0
  9. package/dist/lib/glossary-helper.js.map +1 -0
  10. package/dist/pages/chrome.d.ts +24 -13
  11. package/dist/pages/chrome.d.ts.map +1 -1
  12. package/dist/pages/chrome.js +25 -24
  13. package/dist/pages/chrome.js.map +1 -1
  14. package/dist/pages/chrome.ts +38 -27
  15. package/dist/pages/content-detail.d.ts.map +1 -1
  16. package/dist/pages/content-detail.js +6 -5
  17. package/dist/pages/content-detail.js.map +1 -1
  18. package/dist/pages/content-detail.ts +6 -5
  19. package/dist/pages/content.d.ts.map +1 -1
  20. package/dist/pages/content.js +4 -6
  21. package/dist/pages/content.js.map +1 -1
  22. package/dist/pages/content.ts +4 -6
  23. package/dist/pages/dashboard/affordances.d.ts +6 -1
  24. package/dist/pages/dashboard/affordances.d.ts.map +1 -1
  25. package/dist/pages/dashboard/affordances.js +17 -2
  26. package/dist/pages/dashboard/affordances.js.map +1 -1
  27. package/dist/pages/dashboard/affordances.ts +18 -2
  28. package/dist/pages/dashboard/header.js +2 -2
  29. package/dist/pages/dashboard/header.ts +2 -2
  30. package/dist/pages/dashboard/section.d.ts +2 -2
  31. package/dist/pages/dashboard/section.d.ts.map +1 -1
  32. package/dist/pages/dashboard/section.js +5 -5
  33. package/dist/pages/dashboard/section.js.map +1 -1
  34. package/dist/pages/dashboard/section.ts +9 -5
  35. package/dist/pages/dashboard.d.ts.map +1 -1
  36. package/dist/pages/dashboard.js +3 -2
  37. package/dist/pages/dashboard.js.map +1 -1
  38. package/dist/pages/dashboard.ts +3 -2
  39. package/dist/pages/entry-review/data.d.ts +54 -0
  40. package/dist/pages/entry-review/data.d.ts.map +1 -0
  41. package/dist/pages/entry-review/data.js +116 -0
  42. package/dist/pages/entry-review/data.js.map +1 -0
  43. package/dist/pages/entry-review/decision-strip.d.ts +37 -0
  44. package/dist/pages/entry-review/decision-strip.d.ts.map +1 -0
  45. package/dist/pages/entry-review/decision-strip.js +137 -0
  46. package/dist/pages/entry-review/decision-strip.js.map +1 -0
  47. package/dist/pages/entry-review/edit-panes.d.ts +12 -0
  48. package/dist/pages/entry-review/edit-panes.d.ts.map +1 -0
  49. package/dist/pages/entry-review/edit-panes.js +28 -0
  50. package/dist/pages/entry-review/edit-panes.js.map +1 -0
  51. package/dist/pages/entry-review/edit-toolbar.d.ts +20 -0
  52. package/dist/pages/entry-review/edit-toolbar.d.ts.map +1 -0
  53. package/dist/pages/entry-review/edit-toolbar.js +46 -0
  54. package/dist/pages/entry-review/edit-toolbar.js.map +1 -0
  55. package/dist/pages/entry-review/index.d.ts +50 -0
  56. package/dist/pages/entry-review/index.d.ts.map +1 -0
  57. package/dist/pages/entry-review/index.js +219 -0
  58. package/dist/pages/entry-review/index.js.map +1 -0
  59. package/dist/pages/entry-review/marginalia.d.ts +28 -0
  60. package/dist/pages/entry-review/marginalia.d.ts.map +1 -0
  61. package/dist/pages/entry-review/marginalia.js +67 -0
  62. package/dist/pages/entry-review/marginalia.js.map +1 -0
  63. package/dist/pages/entry-review/not-found.d.ts +9 -0
  64. package/dist/pages/entry-review/not-found.d.ts.map +1 -0
  65. package/dist/pages/entry-review/not-found.js +34 -0
  66. package/dist/pages/entry-review/not-found.js.map +1 -0
  67. package/dist/pages/entry-review/outline-drawer.d.ts +12 -0
  68. package/dist/pages/entry-review/outline-drawer.d.ts.map +1 -0
  69. package/dist/pages/entry-review/outline-drawer.js +28 -0
  70. package/dist/pages/entry-review/outline-drawer.js.map +1 -0
  71. package/dist/pages/entry-review/shortcuts.d.ts +14 -0
  72. package/dist/pages/entry-review/shortcuts.d.ts.map +1 -0
  73. package/dist/pages/entry-review/shortcuts.js +36 -0
  74. package/dist/pages/entry-review/shortcuts.js.map +1 -0
  75. package/dist/pages/entry-review/version-strip.d.ts +33 -0
  76. package/dist/pages/entry-review/version-strip.d.ts.map +1 -0
  77. package/dist/pages/entry-review/version-strip.js +81 -0
  78. package/dist/pages/entry-review/version-strip.js.map +1 -0
  79. package/dist/pages/entry-review.d.ts +6 -21
  80. package/dist/pages/entry-review.d.ts.map +1 -1
  81. package/dist/pages/entry-review.js +6 -144
  82. package/dist/pages/entry-review.js.map +1 -1
  83. package/dist/pages/entry-review.ts +11 -181
  84. package/dist/pages/html.d.ts +2 -0
  85. package/dist/pages/html.d.ts.map +1 -1
  86. package/dist/pages/html.js +2 -0
  87. package/dist/pages/html.js.map +1 -1
  88. package/dist/pages/html.ts +4 -0
  89. package/dist/pages/index.d.ts.map +1 -1
  90. package/dist/pages/index.js +6 -10
  91. package/dist/pages/index.js.map +1 -1
  92. package/dist/pages/index.ts +6 -10
  93. package/dist/pages/layout.d.ts.map +1 -1
  94. package/dist/pages/layout.js +6 -0
  95. package/dist/pages/layout.js.map +1 -1
  96. package/dist/pages/layout.ts +7 -0
  97. package/dist/pages/review-scrapbook-drawer.d.ts +7 -0
  98. package/dist/pages/review-scrapbook-drawer.d.ts.map +1 -1
  99. package/dist/pages/review-scrapbook-drawer.js +45 -6
  100. package/dist/pages/review-scrapbook-drawer.js.map +1 -1
  101. package/dist/pages/review-scrapbook-drawer.ts +50 -6
  102. package/dist/pages/review.d.ts.map +1 -1
  103. package/dist/pages/review.js +168 -41
  104. package/dist/pages/review.js.map +1 -1
  105. package/dist/pages/review.ts +192 -41
  106. package/dist/pages/scrapbook.d.ts +7 -14
  107. package/dist/pages/scrapbook.d.ts.map +1 -1
  108. package/dist/pages/scrapbook.js +548 -192
  109. package/dist/pages/scrapbook.js.map +1 -1
  110. package/dist/pages/scrapbook.ts +588 -221
  111. package/dist/pages/shortform-review.d.ts +32 -0
  112. package/dist/pages/shortform-review.d.ts.map +1 -0
  113. package/dist/pages/shortform-review.js +270 -0
  114. package/dist/pages/shortform-review.js.map +1 -0
  115. package/dist/pages/shortform-review.ts +342 -0
  116. package/dist/pages/shortform.js +1 -1
  117. package/dist/pages/shortform.js.map +1 -1
  118. package/dist/pages/shortform.ts +1 -1
  119. package/dist/routes/api.d.ts +8 -0
  120. package/dist/routes/api.d.ts.map +1 -1
  121. package/dist/routes/api.js +115 -0
  122. package/dist/routes/api.js.map +1 -1
  123. package/dist/routes/entry-annotation-body.d.ts +26 -0
  124. package/dist/routes/entry-annotation-body.d.ts.map +1 -0
  125. package/dist/routes/entry-annotation-body.js +152 -0
  126. package/dist/routes/entry-annotation-body.js.map +1 -0
  127. package/dist/server.d.ts +1 -1
  128. package/dist/server.d.ts.map +1 -1
  129. package/dist/server.js +56 -196
  130. package/dist/server.js.map +1 -1
  131. package/package.json +4 -4
@@ -0,0 +1,32 @@
1
+ /**
2
+ * Shortform review surface — `/dev/editorial-review/<workflow-id>`
3
+ * (when the bare-UUID resolves to a workflow record, not an entry).
4
+ *
5
+ * Phase 34a (#171): the longform/outline halves of `pages/review.ts`
6
+ * were retired. The workflow-keyed shortform pipeline survives
7
+ * intentionally — operator decision recorded in the PRD ("Not a
8
+ * shortform retirement"). This file holds the slim subset of the
9
+ * old renderer that shortform actually uses.
10
+ *
11
+ * Retirement: when shortform's own migration phase ships (tracked
12
+ * separately from #171), `pages/shortform-review.ts` and the bare-UUID
13
+ * route's workflow branch get deleted together. Until then, this file
14
+ * is a stable backwards-compat shim — not a "for now" code-comment IOU.
15
+ *
16
+ * Workflow-keyed wording in this file is documenting that deliberate
17
+ * deferral; do not flag in audits.
18
+ */
19
+ import type { StudioContext } from '../routes/api.ts';
20
+ interface ShortformReviewQuery {
21
+ /** ?v=<n>; null shows the workflow's currentVersion. */
22
+ version: string | null;
23
+ }
24
+ /**
25
+ * Render the shortform review surface for a workflow id. The bare-UUID
26
+ * route in `server.ts` calls this when `:id` resolves to a workflow
27
+ * record (i.e. shortform). Longform UUIDs 301-redirect to the
28
+ * entry-keyed surface instead.
29
+ */
30
+ export declare function renderShortformReviewPage(ctx: StudioContext, workflowId: string, query: ShortformReviewQuery): Promise<string>;
31
+ export {};
32
+ //# sourceMappingURL=shortform-review.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"shortform-review.d.ts","sourceRoot":"","sources":["../../src/pages/shortform-review.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;GAiBG;AAWH,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,kBAAkB,CAAC;AAMtD,UAAU,oBAAoB;IAC5B,wDAAwD;IACxD,OAAO,EAAE,MAAM,GAAG,IAAI,CAAC;CACxB;AA6MD;;;;;GAKG;AACH,wBAAsB,yBAAyB,CAC7C,GAAG,EAAE,aAAa,EAClB,UAAU,EAAE,MAAM,EAClB,KAAK,EAAE,oBAAoB,GAC1B,OAAO,CAAC,MAAM,CAAC,CAyFjB"}
@@ -0,0 +1,270 @@
1
+ /**
2
+ * Shortform review surface — `/dev/editorial-review/<workflow-id>`
3
+ * (when the bare-UUID resolves to a workflow record, not an entry).
4
+ *
5
+ * Phase 34a (#171): the longform/outline halves of `pages/review.ts`
6
+ * were retired. The workflow-keyed shortform pipeline survives
7
+ * intentionally — operator decision recorded in the PRD ("Not a
8
+ * shortform retirement"). This file holds the slim subset of the
9
+ * old renderer that shortform actually uses.
10
+ *
11
+ * Retirement: when shortform's own migration phase ships (tracked
12
+ * separately from #171), `pages/shortform-review.ts` and the bare-UUID
13
+ * route's workflow branch get deleted together. Until then, this file
14
+ * is a stable backwards-compat shim — not a "for now" code-comment IOU.
15
+ *
16
+ * Workflow-keyed wording in this file is documenting that deliberate
17
+ * deferral; do not flag in audits.
18
+ */
19
+ import { handleGetWorkflow } from '@deskwork/core/review/handlers';
20
+ import { parseDraftFrontmatter, renderMarkdownToHtml, } from '@deskwork/core/review/render';
21
+ import { html, unsafe } from "./html.js";
22
+ import { layout } from "./layout.js";
23
+ import { renderEditorialFolio } from "./chrome.js";
24
+ import { escapeHtml, gloss } from "./html.js";
25
+ function isSuccessBody(body) {
26
+ if (typeof body !== 'object' || body === null)
27
+ return false;
28
+ return 'workflow' in body && 'versions' in body;
29
+ }
30
+ function errorFromBody(body) {
31
+ if (typeof body === 'object' && body !== null) {
32
+ const value = Reflect.get(body, 'error');
33
+ if (typeof value === 'string')
34
+ return value;
35
+ }
36
+ return 'unknown error';
37
+ }
38
+ function stringField(v) {
39
+ return typeof v === 'string' ? v : undefined;
40
+ }
41
+ function stateLabel(state) {
42
+ return (state ?? '').replace('-', ' ');
43
+ }
44
+ async function prepareShortformRender(markdown) {
45
+ const parsed = parseDraftFrontmatter(markdown);
46
+ const fm = parsed.frontmatter;
47
+ const bodyHtml = await renderMarkdownToHtml(parsed.body);
48
+ // Inject the description as a dek after the body's first <h1>.
49
+ const description = stringField(fm.description);
50
+ const dekHtml = description
51
+ ? `<p class="er-dispatch-dek">${escapeHtml(description)}</p>`
52
+ : '';
53
+ const h1Close = bodyHtml.indexOf('</h1>');
54
+ const renderedHtml = dekHtml && h1Close >= 0
55
+ ? bodyHtml.slice(0, h1Close + 5) + dekHtml + bodyHtml.slice(h1Close + 5)
56
+ : dekHtml + bodyHtml;
57
+ return { fm, bodyHtml: renderedHtml };
58
+ }
59
+ function renderVersionsStrip(versions, current) {
60
+ if (versions.length <= 1)
61
+ return unsafe('');
62
+ const links = versions
63
+ .map((v) => {
64
+ const isActive = v.version === current.version;
65
+ const href = `?v=${v.version}`;
66
+ return html `<a href="${href}" class="${isActive ? 'active' : ''}">v${v.version}</a>`;
67
+ })
68
+ .join('');
69
+ return unsafe(html `<span class="er-strip-versions">${unsafe(links)}</span>`);
70
+ }
71
+ function pendingSkillCmd(workflow) {
72
+ const { site, slug, state } = workflow;
73
+ if (state === 'iterating') {
74
+ return `/deskwork:iterate --site ${site} ${slug}`;
75
+ }
76
+ if (state === 'approved') {
77
+ return `/deskwork:approve --site ${site} ${slug}`;
78
+ }
79
+ return '';
80
+ }
81
+ function shortcutChipWrap(buttonHtml, letter) {
82
+ return html `<span class="er-shortcut-chip-wrap">${unsafe(buttonHtml)}<small class="er-shortcut-chip"><kbd>${letter}</kbd><kbd>${letter}</kbd></small></span>`;
83
+ }
84
+ function renderControlsRight(workflow) {
85
+ const isActive = workflow.state === 'open' || workflow.state === 'in-review';
86
+ const isApproved = workflow.state === 'approved';
87
+ const isIterating = workflow.state === 'iterating';
88
+ const isTerminal = workflow.state === 'applied' || workflow.state === 'cancelled';
89
+ const buttons = [];
90
+ buttons.push(html `<button class="er-btn er-btn-small" data-action="toggle-edit" type="button">Edit</button><span class="er-edit-mode-label" data-mode="preview">preview</span>`);
91
+ if (isActive) {
92
+ buttons.push(shortcutChipWrap(html `<button class="er-btn er-btn-small er-btn-approve" data-action="approve" type="button">Approve</button>`, 'a'));
93
+ buttons.push(shortcutChipWrap(html `<button class="er-btn er-btn-small" data-action="iterate" type="button">Iterate</button>`, 'i'));
94
+ buttons.push(shortcutChipWrap(html `<button class="er-btn er-btn-small er-btn-reject" data-action="reject" type="button">Reject</button>`, 'r'));
95
+ }
96
+ if (isApproved) {
97
+ const applyCmd = pendingSkillCmd(workflow);
98
+ buttons.push(html `<span class="er-pending-state">awaiting apply…</span>`);
99
+ buttons.push(html `<button class="er-btn er-btn-small" data-action="copy-cmd" data-cmd="${applyCmd}" title="Copy ${applyCmd} to clipboard" type="button">copy <code>/deskwork:approve</code></button>`);
100
+ buttons.push(shortcutChipWrap(html `<button class="er-btn er-btn-small er-btn-reject" data-action="reject" type="button">Reject</button>`, 'r'));
101
+ }
102
+ if (isIterating) {
103
+ const iterateCmd = pendingSkillCmd(workflow);
104
+ buttons.push(html `<span class="er-pending-state">agent iterating…</span>`);
105
+ buttons.push(html `<button class="er-btn er-btn-small" data-action="copy-cmd" data-cmd="${iterateCmd}" title="Copy ${iterateCmd} to clipboard" type="button">copy <code>/deskwork:iterate</code></button>`);
106
+ }
107
+ if (isTerminal) {
108
+ buttons.push(html `<span class="er-pending-state er-pending-state--filed">filed (${workflow.state})</span>`);
109
+ }
110
+ buttons.push(html `<button class="er-btn er-btn-small" data-action="shortcuts" type="button" aria-label="Show keyboard shortcuts" title="Keyboard shortcuts">?</button>`);
111
+ return unsafe(`<span class="er-strip-right">${buttons.join('')}</span>`);
112
+ }
113
+ function renderShortcutsOverlay() {
114
+ return unsafe(html `
115
+ <div class="er-shortcuts" data-shortcuts-overlay hidden role="dialog" aria-modal="true" aria-label="Keyboard shortcuts">
116
+ <div class="er-shortcuts-backdrop" data-shortcuts-backdrop></div>
117
+ <div class="er-shortcuts-panel">
118
+ <h2>Keyboard</h2>
119
+ <dl>
120
+ <dt><kbd>e</kbd> / dbl-click</dt><dd>toggle edit mode</dd>
121
+ <dt><kbd>a</kbd> <kbd>a</kbd></dt><dd>approve <em>— press twice within 500ms</em></dd>
122
+ <dt><kbd>i</kbd> <kbd>i</kbd></dt><dd>iterate <em>— press twice within 500ms</em></dd>
123
+ <dt><kbd>r</kbd> <kbd>r</kbd></dt><dd>reject <em>— press twice within 500ms</em></dd>
124
+ <dt><kbd>?</kbd></dt><dd>this panel</dd>
125
+ <dt><kbd>esc</kbd></dt><dd>close</dd>
126
+ </dl>
127
+ <p class="er-shortcuts-footer">Press <kbd>?</kbd> anytime.</p>
128
+ </div>
129
+ </div>`);
130
+ }
131
+ function renderEditToolbar() {
132
+ return unsafe(html `
133
+ <div class="er-edit-toolbar" data-edit-toolbar hidden>
134
+ <div class="er-edit-modes" role="tablist" aria-label="Editor mode">
135
+ <button class="er-edit-mode-btn" data-edit-view="source" type="button" aria-pressed="true">Source</button>
136
+ <button class="er-edit-mode-btn" data-edit-view="split" type="button" aria-pressed="false">Split</button>
137
+ <button class="er-edit-mode-btn" data-edit-view="preview" type="button" aria-pressed="false">Preview</button>
138
+ </div>
139
+ <div class="er-edit-actions">
140
+ <button class="er-btn er-btn-small" data-action="focus-mode" type="button" title="Distraction-free mode (Shift+F)" aria-pressed="false">Focus ⛶</button>
141
+ <button class="er-btn er-btn-primary" data-action="save-version" type="button">Save as new version</button>
142
+ <button class="er-btn" data-action="cancel-edit" type="button">Cancel</button>
143
+ <span class="er-edit-hint" data-edit-hint></span>
144
+ </div>
145
+ </div>`);
146
+ }
147
+ function renderEditPanes() {
148
+ return unsafe(html `
149
+ <div class="er-edit-mode" data-edit-panes-host hidden>
150
+ <div class="er-edit-panes" data-edit-panes data-view="source">
151
+ <div class="er-edit-source" data-edit-source aria-label="Markdown source"></div>
152
+ <div class="er-edit-preview" data-edit-preview aria-label="Rendered preview"></div>
153
+ </div>
154
+ <textarea id="draft-edit" data-draft-edit hidden></textarea>
155
+ <div class="er-focus-exit" data-focus-exit aria-hidden="true">
156
+ <button type="button" data-action="exit-focus" title="Exit focus (Esc)">← exit focus</button>
157
+ </div>
158
+ <div class="er-focus-save" data-focus-save aria-hidden="true">
159
+ <button type="button" class="er-btn er-btn-small er-btn-primary" data-action="save-version">Save</button>
160
+ <span class="er-focus-save-hint" data-focus-save-hint></span>
161
+ </div>
162
+ </div>`);
163
+ }
164
+ function renderError(workflowId, message) {
165
+ const body = html `
166
+ <div data-review-ui="shortform">
167
+ ${renderEditorialFolio('shortform', `shortform · ${workflowId}`)}
168
+ <div class="er-error">
169
+ <h1>No galley to review.</h1>
170
+ <p><strong>Workflow:</strong> <code>${workflowId}</code></p>
171
+ <p>${message}</p>
172
+ <p style="margin-top: 2rem;"><a href="/dev/editorial-studio">← back to the studio</a></p>
173
+ </div>
174
+ </div>`;
175
+ return layout({
176
+ title: `Review — ${workflowId} — error`,
177
+ cssHrefs: [
178
+ '/static/css/editorial-review.css',
179
+ '/static/css/editorial-nav.css',
180
+ ],
181
+ bodyHtml: body,
182
+ scriptModules: [],
183
+ });
184
+ }
185
+ /**
186
+ * Render the shortform review surface for a workflow id. The bare-UUID
187
+ * route in `server.ts` calls this when `:id` resolves to a workflow
188
+ * record (i.e. shortform). Longform UUIDs 301-redirect to the
189
+ * entry-keyed surface instead.
190
+ */
191
+ export async function renderShortformReviewPage(ctx, workflowId, query) {
192
+ const fetched = handleGetWorkflow(ctx.projectRoot, ctx.config, {
193
+ id: workflowId,
194
+ entryId: null,
195
+ site: null,
196
+ slug: null,
197
+ contentKind: null,
198
+ platform: null,
199
+ channel: null,
200
+ });
201
+ if (fetched.status !== 200 || !isSuccessBody(fetched.body)) {
202
+ return renderError(workflowId, errorFromBody(fetched.body));
203
+ }
204
+ const { workflow, versions } = fetched.body;
205
+ const slug = workflow.slug;
206
+ const requested = query.version ? parseInt(query.version, 10) : workflow.currentVersion;
207
+ const currentVersion = versions.find((v) => v.version === requested) ?? versions[versions.length - 1];
208
+ if (!currentVersion) {
209
+ return renderError(workflowId, 'no current version on this workflow');
210
+ }
211
+ const { fm, bodyHtml } = await prepareShortformRender(currentVersion.markdown);
212
+ const draftState = { workflow, currentVersion, versions };
213
+ const titleField = stringField(fm.title) ?? `Draft: ${slug}`;
214
+ const shortformMeta = unsafe(html `
215
+ <div class="er-shortform-meta">
216
+ <span class="er-platform">${workflow.platform ?? 'other'}</span>
217
+ ${workflow.channel
218
+ ? unsafe(html `<span class="er-channel">${workflow.channel}</span>`)
219
+ : ''}
220
+ </div>`);
221
+ const folioSpine = `shortform · ${workflow.platform ?? '?'}${workflow.channel ? ` · ${workflow.channel}` : ''} · ${slug}`;
222
+ const pageGrid = html `
223
+ <div class="er-page-grid">
224
+ <div class="er-draft-frame">
225
+ <div id="draft-body" data-draft-body
226
+ title="Double-click to edit">${unsafe(bodyHtml)}</div>
227
+ ${renderEditPanes()}
228
+ </div>
229
+ </div>`;
230
+ const body = html `
231
+ <div data-review-ui="shortform" class="er-review-shell">
232
+ ${renderEditorialFolio('shortform', folioSpine)}
233
+ ${shortformMeta}
234
+ <div class="er-strip">
235
+ <div class="er-strip-inner">
236
+ <a class="er-strip-back" href="/dev/editorial-studio" title="Back to the editorial studio">← studio</a>
237
+ <span class="er-strip-galley">${gloss('galley')} <em>№ ${currentVersion.version}</em></span>
238
+ <span class="er-strip-slug">${workflow.site} / ${workflow.slug}</span>
239
+ ${renderVersionsStrip(versions, currentVersion)}
240
+ <span class="er-strip-center">
241
+ <span class="er-stamp er-stamp-big er-stamp-${workflow.state}" data-state-label>
242
+ ${stateLabel(workflow.state)}
243
+ </span>
244
+ <span class="er-strip-hint">double-click to edit · <kbd>?</kbd> for shortcuts</span>
245
+ </span>
246
+ ${renderControlsRight(workflow)}
247
+ </div>
248
+ </div>
249
+ ${renderEditToolbar()}
250
+ <article class="er-page">
251
+ ${unsafe(pageGrid)}
252
+ </article>
253
+ <div class="er-toast" data-toast hidden></div>
254
+ ${renderShortcutsOverlay()}
255
+ <div class="er-poll-indicator" data-poll>auto-refresh · 8s</div>
256
+ </div>`;
257
+ return layout({
258
+ title: `${titleField} — Review`,
259
+ cssHrefs: [
260
+ '/static/css/editorial-review.css',
261
+ '/static/css/editorial-nav.css',
262
+ '/static/css/blog-figure.css',
263
+ '/static/css/review-viewport.css',
264
+ ],
265
+ bodyHtml: body,
266
+ embeddedJson: [{ id: 'draft-state', data: draftState }],
267
+ scriptModules: ['editorial-review-client'],
268
+ });
269
+ }
270
+ //# sourceMappingURL=shortform-review.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"shortform-review.js","sourceRoot":"","sources":["../../src/pages/shortform-review.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,342 @@
1
+ /**
2
+ * Shortform review surface — `/dev/editorial-review/<workflow-id>`
3
+ * (when the bare-UUID resolves to a workflow record, not an entry).
4
+ *
5
+ * Phase 34a (#171): the longform/outline halves of `pages/review.ts`
6
+ * were retired. The workflow-keyed shortform pipeline survives
7
+ * intentionally — operator decision recorded in the PRD ("Not a
8
+ * shortform retirement"). This file holds the slim subset of the
9
+ * old renderer that shortform actually uses.
10
+ *
11
+ * Retirement: when shortform's own migration phase ships (tracked
12
+ * separately from #171), `pages/shortform-review.ts` and the bare-UUID
13
+ * route's workflow branch get deleted together. Until then, this file
14
+ * is a stable backwards-compat shim — not a "for now" code-comment IOU.
15
+ *
16
+ * Workflow-keyed wording in this file is documenting that deliberate
17
+ * deferral; do not flag in audits.
18
+ */
19
+
20
+ import { handleGetWorkflow } from '@deskwork/core/review/handlers';
21
+ import type {
22
+ DraftVersion,
23
+ DraftWorkflowItem,
24
+ } from '@deskwork/core/review/types';
25
+ import {
26
+ parseDraftFrontmatter,
27
+ renderMarkdownToHtml,
28
+ } from '@deskwork/core/review/render';
29
+ import type { StudioContext } from '../routes/api.ts';
30
+ import { html, unsafe, type RawHtml } from './html.ts';
31
+ import { layout } from './layout.ts';
32
+ import { renderEditorialFolio } from './chrome.ts';
33
+ import { escapeHtml, gloss } from './html.ts';
34
+
35
+ interface ShortformReviewQuery {
36
+ /** ?v=<n>; null shows the workflow's currentVersion. */
37
+ version: string | null;
38
+ }
39
+
40
+ function isSuccessBody(
41
+ body: unknown,
42
+ ): body is { workflow: DraftWorkflowItem; versions: DraftVersion[] } {
43
+ if (typeof body !== 'object' || body === null) return false;
44
+ return 'workflow' in body && 'versions' in body;
45
+ }
46
+
47
+ function errorFromBody(body: unknown): string {
48
+ if (typeof body === 'object' && body !== null) {
49
+ const value = Reflect.get(body, 'error');
50
+ if (typeof value === 'string') return value;
51
+ }
52
+ return 'unknown error';
53
+ }
54
+
55
+ function stringField(v: unknown): string | undefined {
56
+ return typeof v === 'string' ? v : undefined;
57
+ }
58
+
59
+ function stateLabel(state?: string): string {
60
+ return (state ?? '').replace('-', ' ');
61
+ }
62
+
63
+ interface PreparedRender {
64
+ fm: Record<string, unknown>;
65
+ bodyHtml: string;
66
+ }
67
+
68
+ async function prepareShortformRender(markdown: string): Promise<PreparedRender> {
69
+ const parsed = parseDraftFrontmatter(markdown);
70
+ const fm = parsed.frontmatter;
71
+ const bodyHtml = await renderMarkdownToHtml(parsed.body);
72
+
73
+ // Inject the description as a dek after the body's first <h1>.
74
+ const description = stringField(fm.description);
75
+ const dekHtml = description
76
+ ? `<p class="er-dispatch-dek">${escapeHtml(description)}</p>`
77
+ : '';
78
+ const h1Close = bodyHtml.indexOf('</h1>');
79
+ const renderedHtml =
80
+ dekHtml && h1Close >= 0
81
+ ? bodyHtml.slice(0, h1Close + 5) + dekHtml + bodyHtml.slice(h1Close + 5)
82
+ : dekHtml + bodyHtml;
83
+
84
+ return { fm, bodyHtml: renderedHtml };
85
+ }
86
+
87
+ function renderVersionsStrip(
88
+ versions: readonly DraftVersion[],
89
+ current: DraftVersion,
90
+ ): RawHtml {
91
+ if (versions.length <= 1) return unsafe('');
92
+ const links = versions
93
+ .map((v) => {
94
+ const isActive = v.version === current.version;
95
+ const href = `?v=${v.version}`;
96
+ return html`<a href="${href}" class="${isActive ? 'active' : ''}">v${v.version}</a>`;
97
+ })
98
+ .join('');
99
+ return unsafe(html`<span class="er-strip-versions">${unsafe(links)}</span>`);
100
+ }
101
+
102
+ function pendingSkillCmd(workflow: DraftWorkflowItem): string {
103
+ const { site, slug, state } = workflow;
104
+ if (state === 'iterating') {
105
+ return `/deskwork:iterate --site ${site} ${slug}`;
106
+ }
107
+ if (state === 'approved') {
108
+ return `/deskwork:approve --site ${site} ${slug}`;
109
+ }
110
+ return '';
111
+ }
112
+
113
+ function shortcutChipWrap(buttonHtml: string, letter: 'a' | 'i' | 'r'): string {
114
+ return html`<span class="er-shortcut-chip-wrap">${unsafe(buttonHtml)}<small class="er-shortcut-chip"><kbd>${letter}</kbd><kbd>${letter}</kbd></small></span>`;
115
+ }
116
+
117
+ function renderControlsRight(workflow: DraftWorkflowItem): RawHtml {
118
+ const isActive = workflow.state === 'open' || workflow.state === 'in-review';
119
+ const isApproved = workflow.state === 'approved';
120
+ const isIterating = workflow.state === 'iterating';
121
+ const isTerminal = workflow.state === 'applied' || workflow.state === 'cancelled';
122
+ const buttons: string[] = [];
123
+ buttons.push(html`<button class="er-btn er-btn-small" data-action="toggle-edit" type="button">Edit</button><span class="er-edit-mode-label" data-mode="preview">preview</span>`);
124
+ if (isActive) {
125
+ buttons.push(
126
+ shortcutChipWrap(
127
+ html`<button class="er-btn er-btn-small er-btn-approve" data-action="approve" type="button">Approve</button>`,
128
+ 'a',
129
+ ),
130
+ );
131
+ buttons.push(
132
+ shortcutChipWrap(
133
+ html`<button class="er-btn er-btn-small" data-action="iterate" type="button">Iterate</button>`,
134
+ 'i',
135
+ ),
136
+ );
137
+ buttons.push(
138
+ shortcutChipWrap(
139
+ html`<button class="er-btn er-btn-small er-btn-reject" data-action="reject" type="button">Reject</button>`,
140
+ 'r',
141
+ ),
142
+ );
143
+ }
144
+ if (isApproved) {
145
+ const applyCmd = pendingSkillCmd(workflow);
146
+ buttons.push(html`<span class="er-pending-state">awaiting apply…</span>`);
147
+ buttons.push(html`<button class="er-btn er-btn-small" data-action="copy-cmd" data-cmd="${applyCmd}" title="Copy ${applyCmd} to clipboard" type="button">copy <code>/deskwork:approve</code></button>`);
148
+ buttons.push(
149
+ shortcutChipWrap(
150
+ html`<button class="er-btn er-btn-small er-btn-reject" data-action="reject" type="button">Reject</button>`,
151
+ 'r',
152
+ ),
153
+ );
154
+ }
155
+ if (isIterating) {
156
+ const iterateCmd = pendingSkillCmd(workflow);
157
+ buttons.push(html`<span class="er-pending-state">agent iterating…</span>`);
158
+ buttons.push(html`<button class="er-btn er-btn-small" data-action="copy-cmd" data-cmd="${iterateCmd}" title="Copy ${iterateCmd} to clipboard" type="button">copy <code>/deskwork:iterate</code></button>`);
159
+ }
160
+ if (isTerminal) {
161
+ buttons.push(html`<span class="er-pending-state er-pending-state--filed">filed (${workflow.state})</span>`);
162
+ }
163
+ buttons.push(html`<button class="er-btn er-btn-small" data-action="shortcuts" type="button" aria-label="Show keyboard shortcuts" title="Keyboard shortcuts">?</button>`);
164
+ return unsafe(`<span class="er-strip-right">${buttons.join('')}</span>`);
165
+ }
166
+
167
+ function renderShortcutsOverlay(): RawHtml {
168
+ return unsafe(html`
169
+ <div class="er-shortcuts" data-shortcuts-overlay hidden role="dialog" aria-modal="true" aria-label="Keyboard shortcuts">
170
+ <div class="er-shortcuts-backdrop" data-shortcuts-backdrop></div>
171
+ <div class="er-shortcuts-panel">
172
+ <h2>Keyboard</h2>
173
+ <dl>
174
+ <dt><kbd>e</kbd> / dbl-click</dt><dd>toggle edit mode</dd>
175
+ <dt><kbd>a</kbd> <kbd>a</kbd></dt><dd>approve <em>— press twice within 500ms</em></dd>
176
+ <dt><kbd>i</kbd> <kbd>i</kbd></dt><dd>iterate <em>— press twice within 500ms</em></dd>
177
+ <dt><kbd>r</kbd> <kbd>r</kbd></dt><dd>reject <em>— press twice within 500ms</em></dd>
178
+ <dt><kbd>?</kbd></dt><dd>this panel</dd>
179
+ <dt><kbd>esc</kbd></dt><dd>close</dd>
180
+ </dl>
181
+ <p class="er-shortcuts-footer">Press <kbd>?</kbd> anytime.</p>
182
+ </div>
183
+ </div>`);
184
+ }
185
+
186
+ function renderEditToolbar(): RawHtml {
187
+ return unsafe(html`
188
+ <div class="er-edit-toolbar" data-edit-toolbar hidden>
189
+ <div class="er-edit-modes" role="tablist" aria-label="Editor mode">
190
+ <button class="er-edit-mode-btn" data-edit-view="source" type="button" aria-pressed="true">Source</button>
191
+ <button class="er-edit-mode-btn" data-edit-view="split" type="button" aria-pressed="false">Split</button>
192
+ <button class="er-edit-mode-btn" data-edit-view="preview" type="button" aria-pressed="false">Preview</button>
193
+ </div>
194
+ <div class="er-edit-actions">
195
+ <button class="er-btn er-btn-small" data-action="focus-mode" type="button" title="Distraction-free mode (Shift+F)" aria-pressed="false">Focus ⛶</button>
196
+ <button class="er-btn er-btn-primary" data-action="save-version" type="button">Save as new version</button>
197
+ <button class="er-btn" data-action="cancel-edit" type="button">Cancel</button>
198
+ <span class="er-edit-hint" data-edit-hint></span>
199
+ </div>
200
+ </div>`);
201
+ }
202
+
203
+ function renderEditPanes(): RawHtml {
204
+ return unsafe(html`
205
+ <div class="er-edit-mode" data-edit-panes-host hidden>
206
+ <div class="er-edit-panes" data-edit-panes data-view="source">
207
+ <div class="er-edit-source" data-edit-source aria-label="Markdown source"></div>
208
+ <div class="er-edit-preview" data-edit-preview aria-label="Rendered preview"></div>
209
+ </div>
210
+ <textarea id="draft-edit" data-draft-edit hidden></textarea>
211
+ <div class="er-focus-exit" data-focus-exit aria-hidden="true">
212
+ <button type="button" data-action="exit-focus" title="Exit focus (Esc)">← exit focus</button>
213
+ </div>
214
+ <div class="er-focus-save" data-focus-save aria-hidden="true">
215
+ <button type="button" class="er-btn er-btn-small er-btn-primary" data-action="save-version">Save</button>
216
+ <span class="er-focus-save-hint" data-focus-save-hint></span>
217
+ </div>
218
+ </div>`);
219
+ }
220
+
221
+ function renderError(workflowId: string, message: string): string {
222
+ const body = html`
223
+ <div data-review-ui="shortform">
224
+ ${renderEditorialFolio('shortform', `shortform · ${workflowId}`)}
225
+ <div class="er-error">
226
+ <h1>No galley to review.</h1>
227
+ <p><strong>Workflow:</strong> <code>${workflowId}</code></p>
228
+ <p>${message}</p>
229
+ <p style="margin-top: 2rem;"><a href="/dev/editorial-studio">← back to the studio</a></p>
230
+ </div>
231
+ </div>`;
232
+ return layout({
233
+ title: `Review — ${workflowId} — error`,
234
+ cssHrefs: [
235
+ '/static/css/editorial-review.css',
236
+ '/static/css/editorial-nav.css',
237
+ ],
238
+ bodyHtml: body,
239
+ scriptModules: [],
240
+ });
241
+ }
242
+
243
+ /**
244
+ * Render the shortform review surface for a workflow id. The bare-UUID
245
+ * route in `server.ts` calls this when `:id` resolves to a workflow
246
+ * record (i.e. shortform). Longform UUIDs 301-redirect to the
247
+ * entry-keyed surface instead.
248
+ */
249
+ export async function renderShortformReviewPage(
250
+ ctx: StudioContext,
251
+ workflowId: string,
252
+ query: ShortformReviewQuery,
253
+ ): Promise<string> {
254
+ const fetched = handleGetWorkflow(ctx.projectRoot, ctx.config, {
255
+ id: workflowId,
256
+ entryId: null,
257
+ site: null,
258
+ slug: null,
259
+ contentKind: null,
260
+ platform: null,
261
+ channel: null,
262
+ });
263
+
264
+ if (fetched.status !== 200 || !isSuccessBody(fetched.body)) {
265
+ return renderError(workflowId, errorFromBody(fetched.body));
266
+ }
267
+
268
+ const { workflow, versions } = fetched.body;
269
+ const slug = workflow.slug;
270
+
271
+ const requested = query.version ? parseInt(query.version, 10) : workflow.currentVersion;
272
+ const currentVersion =
273
+ versions.find((v) => v.version === requested) ?? versions[versions.length - 1];
274
+
275
+ if (!currentVersion) {
276
+ return renderError(workflowId, 'no current version on this workflow');
277
+ }
278
+
279
+ const { fm, bodyHtml } = await prepareShortformRender(currentVersion.markdown);
280
+ const draftState = { workflow, currentVersion, versions };
281
+ const titleField = stringField(fm.title) ?? `Draft: ${slug}`;
282
+
283
+ const shortformMeta: RawHtml = unsafe(html`
284
+ <div class="er-shortform-meta">
285
+ <span class="er-platform">${workflow.platform ?? 'other'}</span>
286
+ ${workflow.channel
287
+ ? unsafe(html`<span class="er-channel">${workflow.channel}</span>`)
288
+ : ''}
289
+ </div>`);
290
+
291
+ const folioSpine = `shortform · ${workflow.platform ?? '?'}${workflow.channel ? ` · ${workflow.channel}` : ''} · ${slug}`;
292
+
293
+ const pageGrid = html`
294
+ <div class="er-page-grid">
295
+ <div class="er-draft-frame">
296
+ <div id="draft-body" data-draft-body
297
+ title="Double-click to edit">${unsafe(bodyHtml)}</div>
298
+ ${renderEditPanes()}
299
+ </div>
300
+ </div>`;
301
+
302
+ const body = html`
303
+ <div data-review-ui="shortform" class="er-review-shell">
304
+ ${renderEditorialFolio('shortform', folioSpine)}
305
+ ${shortformMeta}
306
+ <div class="er-strip">
307
+ <div class="er-strip-inner">
308
+ <a class="er-strip-back" href="/dev/editorial-studio" title="Back to the editorial studio">← studio</a>
309
+ <span class="er-strip-galley">${gloss('galley')} <em>№ ${currentVersion.version}</em></span>
310
+ <span class="er-strip-slug">${workflow.site} / ${workflow.slug}</span>
311
+ ${renderVersionsStrip(versions, currentVersion)}
312
+ <span class="er-strip-center">
313
+ <span class="er-stamp er-stamp-big er-stamp-${workflow.state}" data-state-label>
314
+ ${stateLabel(workflow.state)}
315
+ </span>
316
+ <span class="er-strip-hint">double-click to edit · <kbd>?</kbd> for shortcuts</span>
317
+ </span>
318
+ ${renderControlsRight(workflow)}
319
+ </div>
320
+ </div>
321
+ ${renderEditToolbar()}
322
+ <article class="er-page">
323
+ ${unsafe(pageGrid)}
324
+ </article>
325
+ <div class="er-toast" data-toast hidden></div>
326
+ ${renderShortcutsOverlay()}
327
+ <div class="er-poll-indicator" data-poll>auto-refresh · 8s</div>
328
+ </div>`;
329
+
330
+ return layout({
331
+ title: `${titleField} — Review`,
332
+ cssHrefs: [
333
+ '/static/css/editorial-review.css',
334
+ '/static/css/editorial-nav.css',
335
+ '/static/css/blog-figure.css',
336
+ '/static/css/review-viewport.css',
337
+ ],
338
+ bodyHtml: body,
339
+ embeddedJson: [{ id: 'draft-state', data: draftState }],
340
+ scriptModules: ['editorial-review-client'],
341
+ });
342
+ }