@deskwork/studio 0.13.0 → 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.
- package/dist/pages/content-detail.d.ts.map +1 -1
- package/dist/pages/content-detail.js +5 -4
- package/dist/pages/content-detail.js.map +1 -1
- package/dist/pages/content-detail.ts +5 -4
- package/dist/pages/content.d.ts.map +1 -1
- package/dist/pages/content.js +4 -6
- package/dist/pages/content.js.map +1 -1
- package/dist/pages/content.ts +4 -6
- package/dist/pages/dashboard/affordances.d.ts +6 -1
- package/dist/pages/dashboard/affordances.d.ts.map +1 -1
- package/dist/pages/dashboard/affordances.js +17 -2
- package/dist/pages/dashboard/affordances.js.map +1 -1
- package/dist/pages/dashboard/affordances.ts +18 -2
- package/dist/pages/dashboard/header.js +2 -2
- package/dist/pages/dashboard/header.ts +2 -2
- package/dist/pages/dashboard/section.d.ts +2 -2
- package/dist/pages/dashboard/section.d.ts.map +1 -1
- package/dist/pages/dashboard/section.js +5 -5
- package/dist/pages/dashboard/section.js.map +1 -1
- package/dist/pages/dashboard/section.ts +9 -5
- package/dist/pages/dashboard.d.ts.map +1 -1
- package/dist/pages/dashboard.js +3 -2
- package/dist/pages/dashboard.js.map +1 -1
- package/dist/pages/dashboard.ts +3 -2
- package/dist/pages/entry-review/data.d.ts +54 -0
- package/dist/pages/entry-review/data.d.ts.map +1 -0
- package/dist/pages/entry-review/data.js +116 -0
- package/dist/pages/entry-review/data.js.map +1 -0
- package/dist/pages/entry-review/decision-strip.d.ts +37 -0
- package/dist/pages/entry-review/decision-strip.d.ts.map +1 -0
- package/dist/pages/entry-review/decision-strip.js +137 -0
- package/dist/pages/entry-review/decision-strip.js.map +1 -0
- package/dist/pages/entry-review/edit-panes.d.ts +12 -0
- package/dist/pages/entry-review/edit-panes.d.ts.map +1 -0
- package/dist/pages/entry-review/edit-panes.js +28 -0
- package/dist/pages/entry-review/edit-panes.js.map +1 -0
- package/dist/pages/entry-review/edit-toolbar.d.ts +20 -0
- package/dist/pages/entry-review/edit-toolbar.d.ts.map +1 -0
- package/dist/pages/entry-review/edit-toolbar.js +46 -0
- package/dist/pages/entry-review/edit-toolbar.js.map +1 -0
- package/dist/pages/entry-review/index.d.ts +50 -0
- package/dist/pages/entry-review/index.d.ts.map +1 -0
- package/dist/pages/entry-review/index.js +219 -0
- package/dist/pages/entry-review/index.js.map +1 -0
- package/dist/pages/entry-review/marginalia.d.ts +28 -0
- package/dist/pages/entry-review/marginalia.d.ts.map +1 -0
- package/dist/pages/entry-review/marginalia.js +67 -0
- package/dist/pages/entry-review/marginalia.js.map +1 -0
- package/dist/pages/entry-review/not-found.d.ts +9 -0
- package/dist/pages/entry-review/not-found.d.ts.map +1 -0
- package/dist/pages/entry-review/not-found.js +34 -0
- package/dist/pages/entry-review/not-found.js.map +1 -0
- package/dist/pages/entry-review/outline-drawer.d.ts +12 -0
- package/dist/pages/entry-review/outline-drawer.d.ts.map +1 -0
- package/dist/pages/entry-review/outline-drawer.js +28 -0
- package/dist/pages/entry-review/outline-drawer.js.map +1 -0
- package/dist/pages/entry-review/shortcuts.d.ts +14 -0
- package/dist/pages/entry-review/shortcuts.d.ts.map +1 -0
- package/dist/pages/entry-review/shortcuts.js +36 -0
- package/dist/pages/entry-review/shortcuts.js.map +1 -0
- package/dist/pages/entry-review/version-strip.d.ts +33 -0
- package/dist/pages/entry-review/version-strip.d.ts.map +1 -0
- package/dist/pages/entry-review/version-strip.js +81 -0
- package/dist/pages/entry-review/version-strip.js.map +1 -0
- package/dist/pages/entry-review.d.ts +6 -21
- package/dist/pages/entry-review.d.ts.map +1 -1
- package/dist/pages/entry-review.js +6 -144
- package/dist/pages/entry-review.js.map +1 -1
- package/dist/pages/entry-review.ts +11 -181
- package/dist/pages/index.d.ts.map +1 -1
- package/dist/pages/index.js +6 -10
- package/dist/pages/index.js.map +1 -1
- package/dist/pages/index.ts +6 -10
- package/dist/pages/scrapbook.d.ts.map +1 -1
- package/dist/pages/scrapbook.js +208 -11
- package/dist/pages/scrapbook.js.map +1 -1
- package/dist/pages/scrapbook.ts +209 -10
- package/dist/pages/shortform-review.d.ts +32 -0
- package/dist/pages/shortform-review.d.ts.map +1 -0
- package/dist/pages/shortform-review.js +270 -0
- package/dist/pages/shortform-review.js.map +1 -0
- package/dist/pages/shortform-review.ts +342 -0
- package/dist/routes/api.d.ts +8 -0
- package/dist/routes/api.d.ts.map +1 -1
- package/dist/routes/api.js +115 -0
- package/dist/routes/api.js.map +1 -1
- package/dist/routes/entry-annotation-body.d.ts +26 -0
- package/dist/routes/entry-annotation-body.d.ts.map +1 -0
- package/dist/routes/entry-annotation-body.js +152 -0
- package/dist/routes/entry-annotation-body.js.map +1 -0
- package/dist/server.d.ts +1 -1
- package/dist/server.d.ts.map +1 -1
- package/dist/server.js +56 -193
- package/dist/server.js.map +1 -1
- package/package.json +2 -2
|
@@ -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
|
+
}
|