@deskwork/studio 0.14.0 → 0.14.1
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/package.json +2 -2
- package/dist/pages/review.d.ts +0 -68
- package/dist/pages/review.d.ts.map +0 -1
- package/dist/pages/review.js +0 -561
- package/dist/pages/review.js.map +0 -1
- package/dist/pages/review.ts +0 -710
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@deskwork/studio",
|
|
3
|
-
"version": "0.14.
|
|
3
|
+
"version": "0.14.1",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Editorial review studio — local web UI for the deskwork plugin",
|
|
6
6
|
"homepage": "https://github.com/audiocontrol-org/deskwork#readme",
|
|
@@ -46,7 +46,7 @@
|
|
|
46
46
|
"@codemirror/language": "^6.12.3",
|
|
47
47
|
"@codemirror/state": "^6.6.0",
|
|
48
48
|
"@codemirror/view": "^6.41.1",
|
|
49
|
-
"@deskwork/core": "0.14.
|
|
49
|
+
"@deskwork/core": "0.14.1",
|
|
50
50
|
"@hono/node-server": "^1.13.7",
|
|
51
51
|
"@lezer/highlight": "^1.2.3",
|
|
52
52
|
"esbuild": "^0.28.0",
|
package/dist/pages/review.d.ts
DELETED
|
@@ -1,68 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Per-post review page — `/dev/editorial-review/:slug`.
|
|
3
|
-
*
|
|
4
|
-
* Renders one workflow's current draft inside a margin-note review
|
|
5
|
-
* shell. The body markdown is rendered server-side (so the operator
|
|
6
|
-
* sees content immediately, no flash of unrendered text), with the
|
|
7
|
-
* frontmatter description injected as a dek paragraph after the
|
|
8
|
-
* body's repeated H1.
|
|
9
|
-
*
|
|
10
|
-
* Ported from `pages/dev/editorial-review/[slug].astro`. Differences:
|
|
11
|
-
* - The full <BlogLayout> Astro component went away; we render the
|
|
12
|
-
* review shell as the page body. Site-specific blog chrome was
|
|
13
|
-
* never useful in the review surface (the review is about the
|
|
14
|
-
* prose, not the page chrome).
|
|
15
|
-
* - Site defaults to `config.defaultSite` rather than the hardcoded
|
|
16
|
-
* `'editorialcontrol'` upstream used.
|
|
17
|
-
* - The outline-split helper lives under the plugin tree's `public/src/`
|
|
18
|
-
* (it's a browser module) but it's pure TS, so it can run server-side
|
|
19
|
-
* for the initial render too. After the marketplace-install fix
|
|
20
|
-
* (issue #4), `public/` was relocated from packages/studio/ into
|
|
21
|
-
* plugins/deskwork-studio/, hence the long relative import below.
|
|
22
|
-
*/
|
|
23
|
-
import type { ContentIndex } from '@deskwork/core/content-index';
|
|
24
|
-
import type { StudioContext } from '../routes/api.ts';
|
|
25
|
-
/**
|
|
26
|
-
* Per-request content-index getter. The route layer wires this to the
|
|
27
|
-
* Hono context's memoized cache so a single review render only builds
|
|
28
|
-
* the index once per site even though both the inline-text loader and
|
|
29
|
-
* the scrapbook drawer ask for it. When omitted, callers fall back to
|
|
30
|
-
* slug-template path resolution.
|
|
31
|
-
*/
|
|
32
|
-
export type ReviewIndexGetter = (site: string) => ContentIndex;
|
|
33
|
-
interface ReviewQuery {
|
|
34
|
-
/** ?site=<slug> override; null falls back to config.defaultSite. */
|
|
35
|
-
site: string | null;
|
|
36
|
-
/** ?v=<n>; null shows the workflow's currentVersion. */
|
|
37
|
-
version: string | null;
|
|
38
|
-
/** ?kind=outline | longform; null defaults to longform. */
|
|
39
|
-
kind?: string | null;
|
|
40
|
-
}
|
|
41
|
-
/**
|
|
42
|
-
* How the route resolved the request. Phase 19d added the canonical
|
|
43
|
-
* id-based URL; the legacy slug URL still resolves to a render via the
|
|
44
|
-
* 302-redirect path (the redirect target lands here as `kind: 'id'`).
|
|
45
|
-
*
|
|
46
|
-
* `kind: 'slug'` is reserved for the legacy fallback when the calendar
|
|
47
|
-
* entry has no id stamped on it yet — pre-doctor state, not a "fallback"
|
|
48
|
-
* the project rules forbid but the migration path the plan calls out.
|
|
49
|
-
*
|
|
50
|
-
* Phase 21c added `kind: 'workflow'` so the dashboard can deep-link
|
|
51
|
-
* straight to a specific workflow id without first knowing the entry
|
|
52
|
-
* id — shortform cells use this to land on the unified review surface
|
|
53
|
-
* after `start-shortform` returns the new workflow.
|
|
54
|
-
*/
|
|
55
|
-
export type ReviewLookup = {
|
|
56
|
-
kind: 'id';
|
|
57
|
-
entryId: string;
|
|
58
|
-
slug: string;
|
|
59
|
-
} | {
|
|
60
|
-
kind: 'slug';
|
|
61
|
-
slug: string;
|
|
62
|
-
} | {
|
|
63
|
-
kind: 'workflow';
|
|
64
|
-
workflowId: string;
|
|
65
|
-
};
|
|
66
|
-
export declare function renderReviewPage(ctx: StudioContext, lookup: ReviewLookup, query: ReviewQuery, getIndex?: ReviewIndexGetter): Promise<string>;
|
|
67
|
-
export {};
|
|
68
|
-
//# sourceMappingURL=review.d.ts.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"review.d.ts","sourceRoot":"","sources":["../../src/pages/review.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;GAqBG;AAcH,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,8BAA8B,CAAC;AAEjE,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,kBAAkB,CAAC;AAStD;;;;;;GAMG;AACH,MAAM,MAAM,iBAAiB,GAAG,CAAC,IAAI,EAAE,MAAM,KAAK,YAAY,CAAC;AAE/D,UAAU,WAAW;IACnB,oEAAoE;IACpE,IAAI,EAAE,MAAM,GAAG,IAAI,CAAC;IACpB,wDAAwD;IACxD,OAAO,EAAE,MAAM,GAAG,IAAI,CAAC;IACvB,2DAA2D;IAC3D,IAAI,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;CACtB;AAED;;;;;;;;;;;;;GAaG;AACH,MAAM,MAAM,YAAY,GACpB;IAAE,IAAI,EAAE,IAAI,CAAC;IAAC,OAAO,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAA;CAAE,GAC7C;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAA;CAAE,GAC9B;IAAE,IAAI,EAAE,UAAU,CAAC;IAAC,UAAU,EAAE,MAAM,CAAA;CAAE,CAAC;AA8a7C,wBAAsB,gBAAgB,CACpC,GAAG,EAAE,aAAa,EAClB,MAAM,EAAE,YAAY,EACpB,KAAK,EAAE,WAAW,EAClB,QAAQ,CAAC,EAAE,iBAAiB,GAC3B,OAAO,CAAC,MAAM,CAAC,CAiMjB"}
|
package/dist/pages/review.js
DELETED
|
@@ -1,561 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Per-post review page — `/dev/editorial-review/:slug`.
|
|
3
|
-
*
|
|
4
|
-
* Renders one workflow's current draft inside a margin-note review
|
|
5
|
-
* shell. The body markdown is rendered server-side (so the operator
|
|
6
|
-
* sees content immediately, no flash of unrendered text), with the
|
|
7
|
-
* frontmatter description injected as a dek paragraph after the
|
|
8
|
-
* body's repeated H1.
|
|
9
|
-
*
|
|
10
|
-
* Ported from `pages/dev/editorial-review/[slug].astro`. Differences:
|
|
11
|
-
* - The full <BlogLayout> Astro component went away; we render the
|
|
12
|
-
* review shell as the page body. Site-specific blog chrome was
|
|
13
|
-
* never useful in the review surface (the review is about the
|
|
14
|
-
* prose, not the page chrome).
|
|
15
|
-
* - Site defaults to `config.defaultSite` rather than the hardcoded
|
|
16
|
-
* `'editorialcontrol'` upstream used.
|
|
17
|
-
* - The outline-split helper lives under the plugin tree's `public/src/`
|
|
18
|
-
* (it's a browser module) but it's pure TS, so it can run server-side
|
|
19
|
-
* for the initial render too. After the marketplace-install fix
|
|
20
|
-
* (issue #4), `public/` was relocated from packages/studio/ into
|
|
21
|
-
* plugins/deskwork-studio/, hence the long relative import below.
|
|
22
|
-
*/
|
|
23
|
-
import { handleGetWorkflow } from '@deskwork/core/review/handlers';
|
|
24
|
-
import { parseDraftFrontmatter, renderMarkdownToHtml, } from '@deskwork/core/review/render';
|
|
25
|
-
import { readCalendar } from '@deskwork/core/calendar';
|
|
26
|
-
import { findEntry, findEntryById } from '@deskwork/core/calendar-mutations';
|
|
27
|
-
import { splitOutline } from '@deskwork/core/outline-split';
|
|
28
|
-
import { html, unsafe } from "./html.js";
|
|
29
|
-
import { layout } from "./layout.js";
|
|
30
|
-
import { renderEditorialFolio } from "./chrome.js";
|
|
31
|
-
import { escapeHtml, gloss } from "./html.js";
|
|
32
|
-
import { renderScrapbookDrawer } from "./review-scrapbook-drawer.js";
|
|
33
|
-
import { existsSync } from 'node:fs';
|
|
34
|
-
import { resolveCalendarPath } from '@deskwork/core/paths';
|
|
35
|
-
function isSuccessBody(body) {
|
|
36
|
-
if (typeof body !== 'object' || body === null)
|
|
37
|
-
return false;
|
|
38
|
-
return 'workflow' in body && 'versions' in body;
|
|
39
|
-
}
|
|
40
|
-
function errorFromBody(body) {
|
|
41
|
-
if (typeof body === 'object' && body !== null) {
|
|
42
|
-
const value = Reflect.get(body, 'error');
|
|
43
|
-
if (typeof value === 'string')
|
|
44
|
-
return value;
|
|
45
|
-
}
|
|
46
|
-
return 'unknown error';
|
|
47
|
-
}
|
|
48
|
-
function pickContentKind(rawKind) {
|
|
49
|
-
if (rawKind === 'outline')
|
|
50
|
-
return 'outline';
|
|
51
|
-
if (rawKind === 'shortform')
|
|
52
|
-
return 'shortform';
|
|
53
|
-
return 'longform';
|
|
54
|
-
}
|
|
55
|
-
function pickSite(ctx, raw) {
|
|
56
|
-
if (raw && raw in ctx.config.sites)
|
|
57
|
-
return raw;
|
|
58
|
-
return ctx.config.defaultSite;
|
|
59
|
-
}
|
|
60
|
-
function stringField(v) {
|
|
61
|
-
return typeof v === 'string' ? v : undefined;
|
|
62
|
-
}
|
|
63
|
-
async function prepareRender(markdown, contentKind) {
|
|
64
|
-
const parsed = parseDraftFrontmatter(markdown);
|
|
65
|
-
const fm = parsed.frontmatter;
|
|
66
|
-
// Outline + shortform render the body as-is (no outline-split). Only
|
|
67
|
-
// longform pulls the optional briefing-sheet drawer out of the body.
|
|
68
|
-
const split = contentKind !== 'longform'
|
|
69
|
-
? { body: parsed.body, outline: '', present: false, startLine: -1, endLine: -1 }
|
|
70
|
-
: splitOutline(parsed.body);
|
|
71
|
-
const bodyHtml = await renderMarkdownToHtml(split.body);
|
|
72
|
-
// Inject the description as a dek after the body's first <h1>.
|
|
73
|
-
const description = stringField(fm.description);
|
|
74
|
-
const dekHtml = description
|
|
75
|
-
? `<p class="er-dispatch-dek">${escapeHtml(description)}</p>`
|
|
76
|
-
: '';
|
|
77
|
-
const h1Close = bodyHtml.indexOf('</h1>');
|
|
78
|
-
const renderedHtml = dekHtml && h1Close >= 0
|
|
79
|
-
? bodyHtml.slice(0, h1Close + 5) + dekHtml + bodyHtml.slice(h1Close + 5)
|
|
80
|
-
: dekHtml + bodyHtml;
|
|
81
|
-
const outlineHtml = split.outline
|
|
82
|
-
? await renderMarkdownToHtml(split.outline)
|
|
83
|
-
: '';
|
|
84
|
-
return { fm, bodyHtml: renderedHtml, outlineHtml };
|
|
85
|
-
}
|
|
86
|
-
function stateLabel(state) {
|
|
87
|
-
return (state ?? '').replace('-', ' ');
|
|
88
|
-
}
|
|
89
|
-
function renderVersionsStrip(versions, site, contentKind, current) {
|
|
90
|
-
if (versions.length <= 1)
|
|
91
|
-
return unsafe('');
|
|
92
|
-
const kindBit = contentKind === 'outline'
|
|
93
|
-
? '&kind=outline'
|
|
94
|
-
: contentKind === 'shortform'
|
|
95
|
-
? '&kind=shortform'
|
|
96
|
-
: '';
|
|
97
|
-
const links = versions
|
|
98
|
-
.map((v) => {
|
|
99
|
-
const isActive = v.version === current.version;
|
|
100
|
-
const href = `?site=${site}${kindBit}&v=${v.version}`;
|
|
101
|
-
return html `<a href="${href}" class="${isActive ? 'active' : ''}">v${v.version}</a>`;
|
|
102
|
-
})
|
|
103
|
-
.join('');
|
|
104
|
-
return unsafe(html `<span class="er-strip-versions">${unsafe(links)}</span>`);
|
|
105
|
-
}
|
|
106
|
-
/**
|
|
107
|
-
* Build the slash command that the operator pastes into Claude Code to
|
|
108
|
-
* advance the workflow from its current pending state. Mirrors the
|
|
109
|
-
* client-side button-handler logic so server-rendered "copy again"
|
|
110
|
-
* affordances stay consistent.
|
|
111
|
-
*/
|
|
112
|
-
function pendingSkillCmd(workflow) {
|
|
113
|
-
const { site, slug, contentKind, state } = workflow;
|
|
114
|
-
if (state === 'iterating') {
|
|
115
|
-
return contentKind === 'outline'
|
|
116
|
-
? `/deskwork:iterate --kind outline --site ${site} ${slug}`
|
|
117
|
-
: `/deskwork:iterate --site ${site} ${slug}`;
|
|
118
|
-
}
|
|
119
|
-
if (state === 'approved') {
|
|
120
|
-
// Outline-approve semantics still TBD (see editorial-review-client.ts);
|
|
121
|
-
// for now both kinds emit the same /deskwork:approve.
|
|
122
|
-
return `/deskwork:approve --site ${site} ${slug}`;
|
|
123
|
-
}
|
|
124
|
-
return '';
|
|
125
|
-
}
|
|
126
|
-
/**
|
|
127
|
-
* Wrap an action button in a `.er-shortcut-chip-wrap` span carrying a
|
|
128
|
-
* small chord chip beneath the button. The chord style mirrors the
|
|
129
|
-
* shortcuts modal's verbatim two-tap rendering (e.g. `<kbd>a</kbd>
|
|
130
|
-
* <kbd>a</kbd>` for approve) — the destructive-shortcut UX, post-#108,
|
|
131
|
-
* is bare-letter double-tap (no Cmd/Ctrl modifier; verified in the
|
|
132
|
-
* keybinding handler at editorial-review-client.ts).
|
|
133
|
-
*
|
|
134
|
-
* The chip is hidden on narrow viewports via the cross-surface CSS
|
|
135
|
-
* media query — the wrap stays in the markup at every breakpoint so
|
|
136
|
-
* the column flex it triggers (`.er-strip-right > *:has(.er-shortcut-chip)`)
|
|
137
|
-
* is consistent with the chip's visibility state.
|
|
138
|
-
*
|
|
139
|
-
* Issue 5 — keyboard-shortcut chips on action buttons.
|
|
140
|
-
*/
|
|
141
|
-
function shortcutChipWrap(buttonHtml, letter) {
|
|
142
|
-
return html `<span class="er-shortcut-chip-wrap">${unsafe(buttonHtml)}<small class="er-shortcut-chip"><kbd>${letter}</kbd><kbd>${letter}</kbd></small></span>`;
|
|
143
|
-
}
|
|
144
|
-
function renderControlsRight(workflow) {
|
|
145
|
-
const isActive = workflow.state === 'open' || workflow.state === 'in-review';
|
|
146
|
-
const isApproved = workflow.state === 'approved';
|
|
147
|
-
const isIterating = workflow.state === 'iterating';
|
|
148
|
-
const isTerminal = workflow.state === 'applied' || workflow.state === 'cancelled';
|
|
149
|
-
const buttons = [];
|
|
150
|
-
// Issue 7 — emit the edit-mode disclosure label next to the Edit
|
|
151
|
-
// button. The client (editorial-review-client.ts) flips both the
|
|
152
|
-
// `data-mode` attribute AND inner text on each toggle. Initial state
|
|
153
|
-
// matches the surface's initial mode (preview).
|
|
154
|
-
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>`);
|
|
155
|
-
if (isActive) {
|
|
156
|
-
// Issue 5 — wrap each destructive action button with its chord chip.
|
|
157
|
-
buttons.push(shortcutChipWrap(html `<button class="er-btn er-btn-small er-btn-approve" data-action="approve" type="button">Approve</button>`, 'a'));
|
|
158
|
-
buttons.push(shortcutChipWrap(html `<button class="er-btn er-btn-small" data-action="iterate" type="button">Iterate</button>`, 'i'));
|
|
159
|
-
buttons.push(shortcutChipWrap(html `<button class="er-btn er-btn-small er-btn-reject" data-action="reject" type="button">Reject</button>`, 'r'));
|
|
160
|
-
}
|
|
161
|
-
if (isApproved) {
|
|
162
|
-
const applyCmd = pendingSkillCmd(workflow);
|
|
163
|
-
buttons.push(html `<span class="er-pending-state">awaiting apply…</span>`);
|
|
164
|
-
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>`);
|
|
165
|
-
buttons.push(shortcutChipWrap(html `<button class="er-btn er-btn-small er-btn-reject" data-action="reject" type="button">Reject</button>`, 'r'));
|
|
166
|
-
}
|
|
167
|
-
if (isIterating) {
|
|
168
|
-
const iterateCmd = pendingSkillCmd(workflow);
|
|
169
|
-
buttons.push(html `<span class="er-pending-state">agent iterating…</span>`);
|
|
170
|
-
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>`);
|
|
171
|
-
}
|
|
172
|
-
if (isTerminal) {
|
|
173
|
-
buttons.push(html `<span class="er-pending-state er-pending-state--filed">filed (${workflow.state})</span>`);
|
|
174
|
-
}
|
|
175
|
-
buttons.push(html `<button class="er-btn er-btn-small" data-action="shortcuts" type="button" aria-label="Show keyboard shortcuts" title="Keyboard shortcuts">?</button>`);
|
|
176
|
-
return unsafe(`<span class="er-strip-right">${buttons.join('')}</span>`);
|
|
177
|
-
}
|
|
178
|
-
function renderError(slug, site, contentKind, message) {
|
|
179
|
-
const startCmd = contentKind === 'outline'
|
|
180
|
-
? `/deskwork:outline --site ${site} ${slug}`
|
|
181
|
-
: contentKind === 'shortform'
|
|
182
|
-
? `/deskwork:shortform-start --site ${site} ${slug} <platform>`
|
|
183
|
-
: `/deskwork:review-start --site ${site} ${slug}`;
|
|
184
|
-
const body = html `
|
|
185
|
-
<div data-review-ui="longform">
|
|
186
|
-
${renderEditorialFolio('longform', `longform · ${slug}`)}
|
|
187
|
-
<div class="er-error">
|
|
188
|
-
<h1>No galley to review.</h1>
|
|
189
|
-
<p><strong>Slug:</strong> <code>${slug}</code></p>
|
|
190
|
-
<p>${message}</p>
|
|
191
|
-
<p>Start one with:</p>
|
|
192
|
-
<p><code>${startCmd}</code></p>
|
|
193
|
-
<p style="margin-top: 2rem;"><a href="/dev/editorial-studio">← back to the studio</a></p>
|
|
194
|
-
</div>
|
|
195
|
-
</div>`;
|
|
196
|
-
return layout({
|
|
197
|
-
title: `Review — ${slug} — error`,
|
|
198
|
-
cssHrefs: [
|
|
199
|
-
'/static/css/editorial-review.css',
|
|
200
|
-
'/static/css/editorial-nav.css',
|
|
201
|
-
],
|
|
202
|
-
bodyHtml: body,
|
|
203
|
-
scriptModules: [],
|
|
204
|
-
});
|
|
205
|
-
}
|
|
206
|
-
function renderShortcutsOverlay() {
|
|
207
|
-
return unsafe(html `
|
|
208
|
-
<div class="er-shortcuts" data-shortcuts-overlay hidden role="dialog" aria-modal="true" aria-label="Keyboard shortcuts">
|
|
209
|
-
<div class="er-shortcuts-backdrop" data-shortcuts-backdrop></div>
|
|
210
|
-
<div class="er-shortcuts-panel">
|
|
211
|
-
<h2>Keyboard</h2>
|
|
212
|
-
<dl>
|
|
213
|
-
<dt><kbd>e</kbd> / dbl-click</dt><dd>toggle edit mode</dd>
|
|
214
|
-
<dt>select text</dt><dd>leave a margin note</dd>
|
|
215
|
-
<dt><kbd>⌘</kbd><kbd>↵</kbd> / <kbd>ctrl</kbd><kbd>↵</kbd></dt><dd>save margin note (in composer)</dd>
|
|
216
|
-
<dt><kbd>a</kbd> <kbd>a</kbd></dt><dd>approve <em>— press twice within 500ms; first press arms, second fires</em></dd>
|
|
217
|
-
<dt><kbd>i</kbd> <kbd>i</kbd></dt><dd>iterate <em>— press twice within 500ms</em></dd>
|
|
218
|
-
<dt><kbd>r</kbd> <kbd>r</kbd></dt><dd>reject <em>— press twice within 500ms</em></dd>
|
|
219
|
-
<dt><kbd>j</kbd> / <kbd>k</kbd></dt><dd>next / previous margin note</dd>
|
|
220
|
-
<dt><kbd>shift</kbd><kbd>F</kbd></dt><dd>focus mode <em>(edit mode only)</em></dd>
|
|
221
|
-
<dt><kbd>shift</kbd><kbd>M</kbd></dt><dd>show / hide margin notes column <em>— or click the chevron in the head when visible, or the pull tab on the right edge when stowed</em></dd>
|
|
222
|
-
<dt><kbd>?</kbd></dt><dd>this panel</dd>
|
|
223
|
-
<dt><kbd>esc</kbd></dt><dd>close / cancel composer</dd>
|
|
224
|
-
</dl>
|
|
225
|
-
<p class="er-shortcuts-footer">Press <kbd>?</kbd> anytime.</p>
|
|
226
|
-
</div>
|
|
227
|
-
</div>`);
|
|
228
|
-
}
|
|
229
|
-
/* Issue #159 — marginalia stow affordance.
|
|
230
|
-
*
|
|
231
|
-
* The toggle for "show / hide the margin-notes column" lives ON the
|
|
232
|
-
* marginalia component, not in a generic toolbar. Two paired
|
|
233
|
-
* affordances drive the same state:
|
|
234
|
-
*
|
|
235
|
-
* - `.er-marginalia-stow` — chevron button INSIDE the marginalia
|
|
236
|
-
* head (next to "Margin notes" label). Clicking it stows the
|
|
237
|
-
* column. Visible only when marginalia is visible (the head is
|
|
238
|
-
* inside `.er-marginalia`, which is `display: none` when stowed).
|
|
239
|
-
*
|
|
240
|
-
* - `.er-marginalia-tab` — pull tab on the right edge of the
|
|
241
|
-
* viewport, mirroring `.er-outline-tab` on the left edge. Visible
|
|
242
|
-
* ONLY when marginalia is stowed (CSS rule `body[data-marginalia=
|
|
243
|
-
* "hidden"] .er-marginalia-tab { display: block }`). Clicking it
|
|
244
|
-
* unstows.
|
|
245
|
-
*
|
|
246
|
-
* Both affordances + Shift+M dispatch through the same client-side
|
|
247
|
-
* toggleMarginalia handler. Mirrors the outline-drawer's pull-tab
|
|
248
|
-
* pattern so the project's affordance vocabulary stays consistent.
|
|
249
|
-
*/
|
|
250
|
-
function renderMarginaliaTab() {
|
|
251
|
-
return unsafe(html `
|
|
252
|
-
<button class="er-marginalia-tab" data-action="toggle-marginalia" type="button" aria-pressed="true" aria-label="Show margin notes (Shift+M)" title="Show margin notes (Shift+M)">
|
|
253
|
-
<span class="er-marginalia-tab-glyph" aria-hidden="true">‹</span>
|
|
254
|
-
<span class="er-marginalia-tab-label">Notes</span>
|
|
255
|
-
</button>`);
|
|
256
|
-
}
|
|
257
|
-
function renderMarginalia() {
|
|
258
|
-
return unsafe(html `
|
|
259
|
-
<aside class="er-marginalia" data-comments-sidebar aria-label="Margin notes">
|
|
260
|
-
<p class="er-marginalia-head">
|
|
261
|
-
<button class="er-marginalia-stow" data-action="toggle-marginalia" type="button" aria-pressed="false" aria-label="Hide margin notes (Shift+M)" title="Hide margin notes (Shift+M)">
|
|
262
|
-
<span aria-hidden="true">›</span>
|
|
263
|
-
</button>
|
|
264
|
-
<span class="er-marginalia-head-label">Margin notes</span>
|
|
265
|
-
</p>
|
|
266
|
-
<p class="er-marginalia-empty" data-sidebar-empty>Select text in the draft to leave a <em>margin note</em>.</p>
|
|
267
|
-
<section class="er-marginalia-composer" data-comment-composer hidden aria-label="New margin note">
|
|
268
|
-
<p class="er-marginalia-composer-head">New mark</p>
|
|
269
|
-
<div class="er-marginalia-composer-quote" data-composer-quote></div>
|
|
270
|
-
<label class="er-marginalia-composer-label" for="comment-category">Mark as</label>
|
|
271
|
-
<select id="comment-category" class="er-marginalia-composer-select" data-comment-category>
|
|
272
|
-
<option value="other" selected>other</option>
|
|
273
|
-
<option value="voice-drift">voice-drift</option>
|
|
274
|
-
<option value="missing-receipt">missing-receipt</option>
|
|
275
|
-
<option value="tutorial-framing">tutorial-framing</option>
|
|
276
|
-
<option value="saas-vocabulary">saas-vocabulary</option>
|
|
277
|
-
<option value="fake-authority">fake-authority</option>
|
|
278
|
-
<option value="structural">structural</option>
|
|
279
|
-
</select>
|
|
280
|
-
<label class="er-marginalia-composer-label" for="comment-text">Note</label>
|
|
281
|
-
<textarea id="comment-text" class="er-marginalia-composer-textarea" data-comment-text rows="4"
|
|
282
|
-
placeholder="What needs attention here?"></textarea>
|
|
283
|
-
<div class="er-marginalia-composer-actions">
|
|
284
|
-
<button type="button" class="er-btn er-btn-small" data-action="cancel-comment">Cancel</button>
|
|
285
|
-
<button type="button" class="er-btn er-btn-small er-btn-primary" data-action="submit-comment">Leave mark</button>
|
|
286
|
-
</div>
|
|
287
|
-
</section>
|
|
288
|
-
<ol class="er-marginalia-list" data-sidebar-list></ol>
|
|
289
|
-
</aside>`);
|
|
290
|
-
}
|
|
291
|
-
/**
|
|
292
|
-
* Issue #154 Dispatch C — the edit-mode chrome was previously a single
|
|
293
|
-
* `.er-edit-mode` block rendered inside `.er-draft-frame` (below
|
|
294
|
-
* `#draft-body`). With the page-grid in place, the natural layout is:
|
|
295
|
-
*
|
|
296
|
-
* - the toolbar (Source/Split/Preview tabs + Outline/Focus/Save/
|
|
297
|
-
* Cancel actions) sticks above `.er-page`, replacing the strip's
|
|
298
|
-
* right-side action buttons;
|
|
299
|
-
* - the source/preview panes take over the article column where
|
|
300
|
-
* `#draft-body` was.
|
|
301
|
-
*
|
|
302
|
-
* `renderEditToolbar` emits the bar that lives ABOVE `.er-page`; the
|
|
303
|
-
* client toggles its `[hidden]` attribute on enter/exit. Keeps
|
|
304
|
-
* `data-edit-toolbar` on the wrapper so `editorial-review-client.ts`'s
|
|
305
|
-
* existing `q('[data-edit-toolbar]')` lookup keeps working.
|
|
306
|
-
*/
|
|
307
|
-
function renderEditToolbar(outlineHasContent) {
|
|
308
|
-
const outlineBtnAttrs = outlineHasContent ? '' : ' hidden';
|
|
309
|
-
return unsafe(html `
|
|
310
|
-
<div class="er-edit-toolbar" data-edit-toolbar hidden>
|
|
311
|
-
<div class="er-edit-modes" role="tablist" aria-label="Editor mode">
|
|
312
|
-
<button class="er-edit-mode-btn" data-edit-view="source" type="button" aria-pressed="true">Source</button>
|
|
313
|
-
<button class="er-edit-mode-btn" data-edit-view="split" type="button" aria-pressed="false">Split</button>
|
|
314
|
-
<button class="er-edit-mode-btn" data-edit-view="preview" type="button" aria-pressed="false">Preview</button>
|
|
315
|
-
</div>
|
|
316
|
-
<div class="er-edit-actions">
|
|
317
|
-
<button class="er-btn er-btn-small" data-action="outline-drawer" type="button" title="Show the outline for reference (O)" aria-pressed="false"${unsafe(outlineBtnAttrs)}>Outline ↗</button>
|
|
318
|
-
<button class="er-btn er-btn-small" data-action="focus-mode" type="button" title="Distraction-free mode (Shift+F)" aria-pressed="false">Focus ⛶</button>
|
|
319
|
-
<button class="er-btn er-btn-primary" data-action="save-version" type="button">Save as new version</button>
|
|
320
|
-
<button class="er-btn" data-action="cancel-edit" type="button">Cancel</button>
|
|
321
|
-
<span class="er-edit-hint" data-edit-hint></span>
|
|
322
|
-
</div>
|
|
323
|
-
</div>`);
|
|
324
|
-
}
|
|
325
|
-
/**
|
|
326
|
-
* Issue #154 Dispatch C — the source/preview panes (and supporting
|
|
327
|
-
* focus-mode affordances + backing textarea) live inside the article
|
|
328
|
-
* column, replacing `#draft-body`. The wrapper keeps the
|
|
329
|
-
* `er-edit-mode` class so existing CSS (panes-host paper-2 background,
|
|
330
|
-
* focus-mode full-viewport canvas) cascades unchanged. Adds
|
|
331
|
-
* `data-edit-panes-host` so the client can flip `[hidden]` on the
|
|
332
|
-
* panes wrapper independently of the toolbar.
|
|
333
|
-
*/
|
|
334
|
-
function renderEditPanes() {
|
|
335
|
-
return unsafe(html `
|
|
336
|
-
<div class="er-edit-mode" data-edit-panes-host hidden>
|
|
337
|
-
<div class="er-edit-panes" data-edit-panes data-view="source">
|
|
338
|
-
<div class="er-edit-source" data-edit-source aria-label="Markdown source"></div>
|
|
339
|
-
<div class="er-edit-preview" data-edit-preview aria-label="Rendered preview"></div>
|
|
340
|
-
</div>
|
|
341
|
-
<textarea id="draft-edit" data-draft-edit hidden></textarea>
|
|
342
|
-
<div class="er-focus-exit" data-focus-exit aria-hidden="true">
|
|
343
|
-
<button type="button" data-action="exit-focus" title="Exit focus (Esc)">← exit focus</button>
|
|
344
|
-
</div>
|
|
345
|
-
<div class="er-focus-save" data-focus-save aria-hidden="true">
|
|
346
|
-
<button type="button" class="er-btn er-btn-small er-btn-primary" data-action="save-version">Save</button>
|
|
347
|
-
<span class="er-focus-save-hint" data-focus-save-hint></span>
|
|
348
|
-
</div>
|
|
349
|
-
</div>`);
|
|
350
|
-
}
|
|
351
|
-
function renderOutlineDrawer(outlineHtml) {
|
|
352
|
-
const hidden = outlineHtml ? '' : ' hidden';
|
|
353
|
-
return unsafe(html `
|
|
354
|
-
<button class="er-outline-tab" data-outline-tab type="button" aria-label="Show outline"${unsafe(hidden)}>
|
|
355
|
-
<span class="er-outline-tab-label">Outline</span>
|
|
356
|
-
</button>
|
|
357
|
-
<aside class="er-outline-drawer" data-outline-drawer aria-label="Outline reference" hidden>
|
|
358
|
-
<header class="er-outline-drawer-head">
|
|
359
|
-
<span class="er-outline-drawer-kicker">Briefing sheet</span>
|
|
360
|
-
<button type="button" class="er-outline-drawer-close" data-outline-close aria-label="Close outline (O or Esc)">×</button>
|
|
361
|
-
</header>
|
|
362
|
-
<div class="er-outline-drawer-body" data-outline-drawer-body>${unsafe(outlineHtml)}</div>
|
|
363
|
-
<footer class="er-outline-drawer-foot">
|
|
364
|
-
<span>Read-only · edit via <code>/editorial-iterate --kind outline</code></span>
|
|
365
|
-
</footer>
|
|
366
|
-
</aside>`);
|
|
367
|
-
}
|
|
368
|
-
/**
|
|
369
|
-
* Resolve the calendar entry that backs this review surface. Callers
|
|
370
|
-
* have either an `entryId` (id-canonical route) or a slug (legacy
|
|
371
|
-
* route) to work with. Returns `null` when no calendar entry matches —
|
|
372
|
-
* ad-hoc workflows + pre-doctor entries fall through to the slug-
|
|
373
|
-
* template legacy path elsewhere.
|
|
374
|
-
*
|
|
375
|
-
* Failures (calendar absent, parse error) are swallowed to null so a
|
|
376
|
-
* transient calendar issue never blocks the review render.
|
|
377
|
-
*/
|
|
378
|
-
function lookupReviewEntry(ctx, site, lookup, fallbackSlug) {
|
|
379
|
-
try {
|
|
380
|
-
const calendarPath = resolveCalendarPath(ctx.projectRoot, ctx.config, site);
|
|
381
|
-
if (!existsSync(calendarPath))
|
|
382
|
-
return null;
|
|
383
|
-
const cal = readCalendar(calendarPath);
|
|
384
|
-
if (lookup.kind === 'id') {
|
|
385
|
-
const byId = findEntryById(cal, lookup.entryId);
|
|
386
|
-
if (byId !== undefined)
|
|
387
|
-
return byId;
|
|
388
|
-
}
|
|
389
|
-
const slug = lookup.kind === 'workflow' ? fallbackSlug : lookup.slug;
|
|
390
|
-
const bySlug = findEntry(cal, slug);
|
|
391
|
-
return bySlug ?? null;
|
|
392
|
-
}
|
|
393
|
-
catch {
|
|
394
|
-
return null;
|
|
395
|
-
}
|
|
396
|
-
}
|
|
397
|
-
export async function renderReviewPage(ctx, lookup, query, getIndex) {
|
|
398
|
-
const queryKind = pickContentKind(query.kind ?? null);
|
|
399
|
-
// Workflow-id lookup short-circuits site + entryId resolution: the
|
|
400
|
-
// workflow record carries everything we need to render. Phase 21c
|
|
401
|
-
// added this path so the dashboard's shortform matrix (and any other
|
|
402
|
-
// surface that knows a workflow id) can deep-link to the unified
|
|
403
|
-
// review surface without first knowing the calendar entry id.
|
|
404
|
-
const fetched = lookup.kind === 'workflow'
|
|
405
|
-
? handleGetWorkflow(ctx.projectRoot, ctx.config, {
|
|
406
|
-
id: lookup.workflowId,
|
|
407
|
-
entryId: null,
|
|
408
|
-
site: null,
|
|
409
|
-
slug: null,
|
|
410
|
-
contentKind: null,
|
|
411
|
-
platform: null,
|
|
412
|
-
channel: null,
|
|
413
|
-
})
|
|
414
|
-
: handleGetWorkflow(ctx.projectRoot, ctx.config, {
|
|
415
|
-
id: null,
|
|
416
|
-
entryId: lookup.kind === 'id' ? lookup.entryId : null,
|
|
417
|
-
site: pickSite(ctx, query.site),
|
|
418
|
-
slug: lookup.slug,
|
|
419
|
-
contentKind: queryKind,
|
|
420
|
-
platform: null,
|
|
421
|
-
channel: null,
|
|
422
|
-
});
|
|
423
|
-
// Slug used in error messages and the title fallback. For
|
|
424
|
-
// workflow-id lookups we don't know the slug until the fetch
|
|
425
|
-
// succeeds, so use the id as a placeholder for any pre-fetch error.
|
|
426
|
-
const lookupSlug = lookup.kind === 'workflow' ? lookup.workflowId : lookup.slug;
|
|
427
|
-
// Site for the chrome / outbound links. Workflow-id lookups carry
|
|
428
|
-
// their own site through the fetched workflow record.
|
|
429
|
-
let resolvedSite = pickSite(ctx, query.site);
|
|
430
|
-
if (fetched.status !== 200 || !isSuccessBody(fetched.body)) {
|
|
431
|
-
return renderError(lookupSlug, resolvedSite, queryKind, errorFromBody(fetched.body));
|
|
432
|
-
}
|
|
433
|
-
const { workflow, versions } = fetched.body;
|
|
434
|
-
// Workflow-id paths drive contentKind from the workflow itself —
|
|
435
|
-
// it's the source of truth, not the URL kind hint.
|
|
436
|
-
const contentKind = lookup.kind === 'workflow' ? workflow.contentKind : queryKind;
|
|
437
|
-
if (lookup.kind === 'workflow')
|
|
438
|
-
resolvedSite = workflow.site;
|
|
439
|
-
const slug = workflow.slug;
|
|
440
|
-
const requested = query.version ? parseInt(query.version, 10) : workflow.currentVersion;
|
|
441
|
-
const currentVersion = versions.find((v) => v.version === requested) ?? versions[versions.length - 1];
|
|
442
|
-
if (!currentVersion) {
|
|
443
|
-
return renderError(slug, resolvedSite, contentKind, 'no current version on this workflow');
|
|
444
|
-
}
|
|
445
|
-
const { fm, bodyHtml, outlineHtml } = await prepareRender(currentVersion.markdown, contentKind);
|
|
446
|
-
const draftState = { workflow, currentVersion, versions };
|
|
447
|
-
const titleField = stringField(fm.title) ?? `Draft: ${slug}`;
|
|
448
|
-
// Phase 19c+: look up the calendar entry so the scrapbook drawer +
|
|
449
|
-
// inline-text loader can resolve the on-disk scrapbook directory via
|
|
450
|
-
// the content index when a frontmatter-id binding exists. Falls back
|
|
451
|
-
// to slug-template addressing when no entry / no id is present.
|
|
452
|
-
// Shortform skips the scrapbook drawer entirely — different surface
|
|
453
|
-
// shape, no margin-note workflow.
|
|
454
|
-
const reviewEntry = lookupReviewEntry(ctx, resolvedSite, lookup, slug);
|
|
455
|
-
const reviewIndex = getIndex ? getIndex(resolvedSite) : undefined;
|
|
456
|
-
const isShortform = contentKind === 'shortform';
|
|
457
|
-
// Phase 21c — shortform header. Renders above the editor on the
|
|
458
|
-
// unified review surface so the operator sees the platform (and
|
|
459
|
-
// channel, if any) at a glance. Reuses existing `--er-*` design
|
|
460
|
-
// tokens; no new CSS introduced.
|
|
461
|
-
const shortformMeta = isShortform
|
|
462
|
-
? unsafe(html `
|
|
463
|
-
<div class="er-shortform-meta">
|
|
464
|
-
<span class="er-platform">${workflow.platform ?? 'other'}</span>
|
|
465
|
-
${workflow.channel
|
|
466
|
-
? unsafe(html `<span class="er-channel">${workflow.channel}</span>`)
|
|
467
|
-
: ''}
|
|
468
|
-
</div>`)
|
|
469
|
-
: unsafe('');
|
|
470
|
-
const reviewUiAttr = isShortform ? 'shortform' : 'longform';
|
|
471
|
-
const folioSpine = isShortform
|
|
472
|
-
? `shortform · ${workflow.platform ?? '?'}${workflow.channel ? ` · ${workflow.channel}` : ''} · ${slug}`
|
|
473
|
-
: `longform · ${slug}`;
|
|
474
|
-
// Issue 4 — shortform reviews highlight the "Shortform" nav item;
|
|
475
|
-
// longform reviews don't match any nav-item (no longform desk
|
|
476
|
-
// exists). Pre-Issue-4, longform mistakenly highlighted shortform
|
|
477
|
-
// because the chrome treated all review surfaces as 'reviews'.
|
|
478
|
-
const folioActive = isShortform
|
|
479
|
-
? 'shortform'
|
|
480
|
-
: 'longform';
|
|
481
|
-
// Issue #154 Dispatch A — `.er-page` wraps the draft frame +
|
|
482
|
-
// marginalia inside a CSS Grid composition so marginalia sits next
|
|
483
|
-
// to the prose it annotates rather than pinned to the viewport.
|
|
484
|
-
// Shortform reviews skip the marginalia column (no margin-note
|
|
485
|
-
// workflow on shortform), so the page collapses to the draft frame
|
|
486
|
-
// alone for that surface — keeping the same `.er-page` shell
|
|
487
|
-
// preserves the desk metaphor across longform/shortform.
|
|
488
|
-
// Issue #154 Dispatch C — edit-mode panes-host lives inside the
|
|
489
|
-
// article column (in place of #draft-body when editing); the
|
|
490
|
-
// toolbar that drives it lives ABOVE `.er-page` (rendered below,
|
|
491
|
-
// outside the grid). Shortform never enters edit mode on this
|
|
492
|
-
// surface, so the panes-host is rendered but stays hidden — keeps
|
|
493
|
-
// the JS hooks present for forward compatibility without flipping
|
|
494
|
-
// any visible chrome.
|
|
495
|
-
const pageGrid = isShortform
|
|
496
|
-
? html `
|
|
497
|
-
<div class="er-page-grid">
|
|
498
|
-
<div class="er-draft-frame">
|
|
499
|
-
<div id="draft-body" data-draft-body
|
|
500
|
-
title="Double-click to edit · select text to leave a margin note">${unsafe(bodyHtml)}</div>
|
|
501
|
-
${renderEditPanes()}
|
|
502
|
-
</div>
|
|
503
|
-
</div>`
|
|
504
|
-
: html `
|
|
505
|
-
<div class="er-page-grid">
|
|
506
|
-
<div class="er-draft-frame">
|
|
507
|
-
<div id="draft-body" data-draft-body
|
|
508
|
-
title="Double-click to edit · select text to leave a margin note">${unsafe(bodyHtml)}</div>
|
|
509
|
-
${renderEditPanes()}
|
|
510
|
-
</div>
|
|
511
|
-
<div class="er-page-gutter" aria-hidden="true"></div>
|
|
512
|
-
${renderMarginalia()}
|
|
513
|
-
</div>`;
|
|
514
|
-
const body = html `
|
|
515
|
-
<div data-review-ui="${reviewUiAttr}" class="er-review-shell">
|
|
516
|
-
${renderEditorialFolio(folioActive, folioSpine)}
|
|
517
|
-
${shortformMeta}
|
|
518
|
-
<div class="er-strip">
|
|
519
|
-
<div class="er-strip-inner">
|
|
520
|
-
<a class="er-strip-back" href="/dev/editorial-studio" title="Back to the editorial studio">← studio</a>
|
|
521
|
-
<span class="er-strip-galley">${gloss('galley')} <em>№ ${currentVersion.version}</em></span>
|
|
522
|
-
<span class="er-strip-slug">${workflow.site} / ${workflow.slug}</span>
|
|
523
|
-
${renderVersionsStrip(versions, resolvedSite, contentKind, currentVersion)}
|
|
524
|
-
<span class="er-strip-center">
|
|
525
|
-
<span class="er-stamp er-stamp-big er-stamp-${workflow.state}" data-state-label>
|
|
526
|
-
${stateLabel(workflow.state)}
|
|
527
|
-
</span>
|
|
528
|
-
<span class="er-strip-hint">select text to <span class="er-gloss" data-term="marginalia" tabindex="0" role="button" aria-describedby="glossary-marginalia">mark</span> · double-click to edit · <kbd>?</kbd> for shortcuts</span>
|
|
529
|
-
</span>
|
|
530
|
-
${renderControlsRight(workflow)}
|
|
531
|
-
</div>
|
|
532
|
-
</div>
|
|
533
|
-
${renderEditToolbar(outlineHtml.length > 0)}
|
|
534
|
-
<article class="er-page">
|
|
535
|
-
${unsafe(pageGrid)}
|
|
536
|
-
</article>
|
|
537
|
-
${isShortform ? unsafe('') : renderMarginaliaTab()}
|
|
538
|
-
<button class="er-pencil-btn" data-add-comment-btn hidden type="button">Mark</button>
|
|
539
|
-
${isShortform ? unsafe('') : renderOutlineDrawer(outlineHtml)}
|
|
540
|
-
${isShortform
|
|
541
|
-
? unsafe('')
|
|
542
|
-
: renderScrapbookDrawer(ctx, resolvedSite, reviewEntry, workflow.slug, reviewIndex)}
|
|
543
|
-
<div class="er-toast" data-toast hidden></div>
|
|
544
|
-
${renderShortcutsOverlay()}
|
|
545
|
-
<div class="er-poll-indicator" data-poll>auto-refresh · 8s</div>
|
|
546
|
-
</div>`;
|
|
547
|
-
return layout({
|
|
548
|
-
title: `${titleField} — Review`,
|
|
549
|
-
cssHrefs: [
|
|
550
|
-
'/static/css/editorial-review.css',
|
|
551
|
-
'/static/css/editorial-nav.css',
|
|
552
|
-
'/static/css/blog-figure.css',
|
|
553
|
-
'/static/css/review-viewport.css',
|
|
554
|
-
'/static/css/scrap-row.css',
|
|
555
|
-
],
|
|
556
|
-
bodyHtml: body,
|
|
557
|
-
embeddedJson: [{ id: 'draft-state', data: draftState }],
|
|
558
|
-
scriptModules: ['editorial-review-client'],
|
|
559
|
-
});
|
|
560
|
-
}
|
|
561
|
-
//# sourceMappingURL=review.js.map
|