@deskwork/studio 0.20.0 → 0.22.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/dashboard/adjacent-section.d.ts +38 -0
- package/dist/pages/dashboard/adjacent-section.d.ts.map +1 -0
- package/dist/pages/dashboard/adjacent-section.js +61 -0
- package/dist/pages/dashboard/adjacent-section.js.map +1 -0
- package/dist/pages/dashboard/adjacent-section.ts +71 -0
- package/dist/pages/dashboard/data.d.ts +35 -4
- package/dist/pages/dashboard/data.d.ts.map +1 -1
- package/dist/pages/dashboard/data.js +69 -5
- package/dist/pages/dashboard/data.js.map +1 -1
- package/dist/pages/dashboard/data.ts +84 -5
- package/dist/pages/dashboard/section.d.ts.map +1 -1
- package/dist/pages/dashboard/section.js +7 -0
- package/dist/pages/dashboard/section.js.map +1 -1
- package/dist/pages/dashboard/section.ts +7 -0
- package/dist/pages/dashboard/shortform-section.d.ts +86 -0
- package/dist/pages/dashboard/shortform-section.d.ts.map +1 -0
- package/dist/pages/dashboard/shortform-section.js +189 -0
- package/dist/pages/dashboard/shortform-section.js.map +1 -0
- package/dist/pages/dashboard/shortform-section.ts +228 -0
- package/dist/pages/dashboard.d.ts.map +1 -1
- package/dist/pages/dashboard.js +34 -1
- package/dist/pages/dashboard.js.map +1 -1
- package/dist/pages/dashboard.ts +40 -1
- package/dist/pages/entry-review/index.d.ts.map +1 -1
- package/dist/pages/entry-review/index.js +24 -2
- package/dist/pages/entry-review/index.js.map +1 -1
- package/dist/pages/entry-review/mobile-sheet.d.ts +65 -0
- package/dist/pages/entry-review/mobile-sheet.d.ts.map +1 -0
- package/dist/pages/entry-review/mobile-sheet.js +170 -0
- package/dist/pages/entry-review/mobile-sheet.js.map +1 -0
- package/dist/pages/masthead-menu.d.ts +38 -0
- package/dist/pages/masthead-menu.d.ts.map +1 -0
- package/dist/pages/masthead-menu.js +126 -0
- package/dist/pages/masthead-menu.js.map +1 -0
- package/dist/pages/masthead-menu.ts +128 -0
- package/dist/pages/masthead.d.ts +85 -0
- package/dist/pages/masthead.d.ts.map +1 -0
- package/dist/pages/masthead.js +99 -0
- package/dist/pages/masthead.js.map +1 -0
- package/dist/pages/masthead.ts +155 -0
- package/dist/pages/mobile-bar.d.ts +72 -0
- package/dist/pages/mobile-bar.d.ts.map +1 -0
- package/dist/pages/mobile-bar.js +88 -0
- package/dist/pages/mobile-bar.js.map +1 -0
- package/dist/pages/mobile-bar.ts +129 -0
- package/dist/pages/shortform-review-mobile-sheet.d.ts +76 -0
- package/dist/pages/shortform-review-mobile-sheet.d.ts.map +1 -0
- package/dist/pages/shortform-review-mobile-sheet.js +159 -0
- package/dist/pages/shortform-review-mobile-sheet.js.map +1 -0
- package/dist/pages/shortform-review-mobile-sheet.ts +185 -0
- package/dist/pages/shortform-review.d.ts +18 -0
- package/dist/pages/shortform-review.d.ts.map +1 -1
- package/dist/pages/shortform-review.js +62 -111
- package/dist/pages/shortform-review.js.map +1 -1
- package/dist/pages/shortform-review.ts +68 -140
- package/dist/pages/shortform.d.ts.map +1 -1
- package/dist/pages/shortform.js +0 -1
- package/dist/pages/shortform.js.map +1 -1
- package/dist/pages/shortform.ts +0 -1
- package/dist/server.js +41 -1
- package/dist/server.js.map +1 -1
- package/package.json +2 -2
|
@@ -15,13 +15,35 @@
|
|
|
15
15
|
*
|
|
16
16
|
* Workflow-keyed wording in this file is documenting that deliberate
|
|
17
17
|
* deferral; do not flag in audits.
|
|
18
|
+
*
|
|
19
|
+
* Step 2.2.10 (v7 universal chrome + state-machine compliance):
|
|
20
|
+
* - Server-side: er-strip / er-stamp / er-pending-state / shortcut
|
|
21
|
+
* overlay / state-branched control rendering all REMOVED. The
|
|
22
|
+
* universal `renderMobileBar` primitive + a shortform-specific
|
|
23
|
+
* sheet host (`./shortform-review-mobile-sheet.ts`) replace them.
|
|
24
|
+
* - Per DESIGN-STANDARDS.md § Universal bar contract: this surface
|
|
25
|
+
* composes its `Cell[]` mechanically (TOC / Versions / Actions)
|
|
26
|
+
* and passes it to `renderMobileBar`. No bespoke chrome shape.
|
|
27
|
+
* - Per DESKWORK-STATE-MACHINE.md Commandment III: no review-state
|
|
28
|
+
* labels on the page (the stamp + pending pills were the
|
|
29
|
+
* violations).
|
|
30
|
+
* - The desktop edit-mode panes (`renderEditPanes`) stay — they're
|
|
31
|
+
* gated by the existing `data-edit-mode` body attribute and the
|
|
32
|
+
* Edit toolbar is still part of the desktop chrome. Mobile edit
|
|
33
|
+
* entry-point is deferred to a future task (the workplan keeps
|
|
34
|
+
* the broader shortform state-machine migration explicitly out
|
|
35
|
+
* of scope for this step).
|
|
18
36
|
*/
|
|
19
37
|
import { handleGetWorkflow } from '@deskwork/core/review/handlers';
|
|
38
|
+
import { extractToc } from '@deskwork/core/review/toc';
|
|
20
39
|
import { parseDraftFrontmatter, renderMarkdownToHtml, } from '@deskwork/core/review/render';
|
|
21
|
-
import { html, unsafe } from "./html.js";
|
|
40
|
+
import { escapeHtml, html, unsafe } from "./html.js";
|
|
22
41
|
import { layout } from "./layout.js";
|
|
23
42
|
import { renderEditorialFolio } from "./chrome.js";
|
|
24
|
-
import {
|
|
43
|
+
import { renderMasthead } from "./masthead.js";
|
|
44
|
+
import { renderMastheadMenu } from "./masthead-menu.js";
|
|
45
|
+
import { renderMobileBar } from "./mobile-bar.js";
|
|
46
|
+
import { getShortformBarCells, renderShortformMobileSheet, } from "./shortform-review-mobile-sheet.js";
|
|
25
47
|
function isSuccessBody(body) {
|
|
26
48
|
if (typeof body !== 'object' || body === null)
|
|
27
49
|
return false;
|
|
@@ -38,9 +60,6 @@ function errorFromBody(body) {
|
|
|
38
60
|
function stringField(v) {
|
|
39
61
|
return typeof v === 'string' ? v : undefined;
|
|
40
62
|
}
|
|
41
|
-
function stateLabel(state) {
|
|
42
|
-
return (state ?? '').replace('-', ' ');
|
|
43
|
-
}
|
|
44
63
|
async function prepareShortformRender(markdown) {
|
|
45
64
|
const parsed = parseDraftFrontmatter(markdown);
|
|
46
65
|
const fm = parsed.frontmatter;
|
|
@@ -56,94 +75,12 @@ async function prepareShortformRender(markdown) {
|
|
|
56
75
|
: dekHtml + bodyHtml;
|
|
57
76
|
return { fm, bodyHtml: renderedHtml };
|
|
58
77
|
}
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
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
|
-
}
|
|
78
|
+
/**
|
|
79
|
+
* Desktop-only edit panes. The `data-edit-mode` body attribute is the
|
|
80
|
+
* gate (set by the existing client toggle-edit handler). On mobile,
|
|
81
|
+
* edit mode is not currently surfaced from this surface — the broader
|
|
82
|
+
* shortform state-machine migration is the right place to design that.
|
|
83
|
+
*/
|
|
147
84
|
function renderEditPanes() {
|
|
148
85
|
return unsafe(html `
|
|
149
86
|
<div class="er-edit-mode" data-edit-panes-host hidden>
|
|
@@ -211,6 +148,9 @@ export async function renderShortformReviewPage(ctx, workflowId, query) {
|
|
|
211
148
|
const { fm, bodyHtml } = await prepareShortformRender(currentVersion.markdown);
|
|
212
149
|
const draftState = { workflow, currentVersion, versions };
|
|
213
150
|
const titleField = stringField(fm.title) ?? `Draft: ${slug}`;
|
|
151
|
+
// #244 — extract TOC from the rendered body. `rehype-slug` already
|
|
152
|
+
// gave every h2/h3/h4 an `id`; `extractToc` reads the slugs + text.
|
|
153
|
+
const tocEntries = extractToc(bodyHtml);
|
|
214
154
|
const shortformMeta = unsafe(html `
|
|
215
155
|
<div class="er-shortform-meta">
|
|
216
156
|
<span class="er-platform">${workflow.platform ?? 'other'}</span>
|
|
@@ -219,6 +159,30 @@ export async function renderShortformReviewPage(ctx, workflowId, query) {
|
|
|
219
159
|
: ''}
|
|
220
160
|
</div>`);
|
|
221
161
|
const folioSpine = `shortform · ${workflow.platform ?? '?'}${workflow.channel ? ` · ${workflow.channel}` : ''} · ${slug}`;
|
|
162
|
+
// v7 universal masthead (mobile-only at this commit). Kicker carries
|
|
163
|
+
// the platform badge + channel as inline markup; the meta tag holds
|
|
164
|
+
// the galley number. Slug occupies the bottom row.
|
|
165
|
+
const platformLabel = workflow.platform ?? 'other';
|
|
166
|
+
const channelLabel = workflow.channel ? ` ${workflow.channel}` : '';
|
|
167
|
+
const mastheadKicker = unsafe(html `<span class="platform">${platformLabel}</span>${channelLabel}`);
|
|
168
|
+
const masthead = renderMasthead({
|
|
169
|
+
kickerHtml: mastheadKicker,
|
|
170
|
+
slug,
|
|
171
|
+
metaInline: `№ ${currentVersion.version}`,
|
|
172
|
+
isHub: false,
|
|
173
|
+
});
|
|
174
|
+
// v7 universal mobile bar. The cell list is composed mechanically
|
|
175
|
+
// from the surface's TOC + Versions + (always) Actions per
|
|
176
|
+
// DESIGN-STANDARDS.md § Universal bar contract. The Actions cell
|
|
177
|
+
// guarantees the bar is never empty.
|
|
178
|
+
const barCells = getShortformBarCells({ tocEntries, versions });
|
|
179
|
+
const mobileBar = renderMobileBar({ contextual: barCells });
|
|
180
|
+
const mobileSheet = renderShortformMobileSheet({
|
|
181
|
+
tocEntries,
|
|
182
|
+
versions,
|
|
183
|
+
workflow,
|
|
184
|
+
currentVersion,
|
|
185
|
+
});
|
|
222
186
|
const pageGrid = html `
|
|
223
187
|
<div class="er-page-grid">
|
|
224
188
|
<div class="er-draft-frame">
|
|
@@ -229,30 +193,16 @@ export async function renderShortformReviewPage(ctx, workflowId, query) {
|
|
|
229
193
|
</div>`;
|
|
230
194
|
const body = html `
|
|
231
195
|
<div data-review-ui="shortform" class="er-review-shell">
|
|
196
|
+
${masthead}
|
|
197
|
+
${renderMastheadMenu()}
|
|
232
198
|
${renderEditorialFolio('shortform', folioSpine)}
|
|
233
199
|
${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
200
|
<article class="er-page">
|
|
251
201
|
${unsafe(pageGrid)}
|
|
252
202
|
</article>
|
|
203
|
+
${mobileBar}
|
|
204
|
+
${mobileSheet}
|
|
253
205
|
<div class="er-toast" data-toast hidden></div>
|
|
254
|
-
${renderShortcutsOverlay()}
|
|
255
|
-
<div class="er-poll-indicator" data-poll>auto-refresh · 8s</div>
|
|
256
206
|
</div>`;
|
|
257
207
|
return layout({
|
|
258
208
|
title: `${titleField} — Review`,
|
|
@@ -261,6 +211,7 @@ export async function renderShortformReviewPage(ctx, workflowId, query) {
|
|
|
261
211
|
'/static/css/editorial-nav.css',
|
|
262
212
|
'/static/css/blog-figure.css',
|
|
263
213
|
'/static/css/review-viewport.css',
|
|
214
|
+
'/static/css/mobile-shell.css',
|
|
264
215
|
],
|
|
265
216
|
bodyHtml: body,
|
|
266
217
|
embeddedJson: [{ id: 'draft-state', data: draftState }],
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"shortform-review.js","sourceRoot":"","sources":["../../src/pages/shortform-review.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"shortform-review.js","sourceRoot":"","sources":["../../src/pages/shortform-review.ts"],"names":[],"mappings":"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"}
|
|
@@ -15,9 +15,28 @@
|
|
|
15
15
|
*
|
|
16
16
|
* Workflow-keyed wording in this file is documenting that deliberate
|
|
17
17
|
* deferral; do not flag in audits.
|
|
18
|
+
*
|
|
19
|
+
* Step 2.2.10 (v7 universal chrome + state-machine compliance):
|
|
20
|
+
* - Server-side: er-strip / er-stamp / er-pending-state / shortcut
|
|
21
|
+
* overlay / state-branched control rendering all REMOVED. The
|
|
22
|
+
* universal `renderMobileBar` primitive + a shortform-specific
|
|
23
|
+
* sheet host (`./shortform-review-mobile-sheet.ts`) replace them.
|
|
24
|
+
* - Per DESIGN-STANDARDS.md § Universal bar contract: this surface
|
|
25
|
+
* composes its `Cell[]` mechanically (TOC / Versions / Actions)
|
|
26
|
+
* and passes it to `renderMobileBar`. No bespoke chrome shape.
|
|
27
|
+
* - Per DESKWORK-STATE-MACHINE.md Commandment III: no review-state
|
|
28
|
+
* labels on the page (the stamp + pending pills were the
|
|
29
|
+
* violations).
|
|
30
|
+
* - The desktop edit-mode panes (`renderEditPanes`) stay — they're
|
|
31
|
+
* gated by the existing `data-edit-mode` body attribute and the
|
|
32
|
+
* Edit toolbar is still part of the desktop chrome. Mobile edit
|
|
33
|
+
* entry-point is deferred to a future task (the workplan keeps
|
|
34
|
+
* the broader shortform state-machine migration explicitly out
|
|
35
|
+
* of scope for this step).
|
|
18
36
|
*/
|
|
19
37
|
|
|
20
38
|
import { handleGetWorkflow } from '@deskwork/core/review/handlers';
|
|
39
|
+
import { extractToc } from '@deskwork/core/review/toc';
|
|
21
40
|
import type {
|
|
22
41
|
DraftVersion,
|
|
23
42
|
DraftWorkflowItem,
|
|
@@ -27,10 +46,16 @@ import {
|
|
|
27
46
|
renderMarkdownToHtml,
|
|
28
47
|
} from '@deskwork/core/review/render';
|
|
29
48
|
import type { StudioContext } from '../routes/api.ts';
|
|
30
|
-
import { html, unsafe, type RawHtml } from './html.ts';
|
|
49
|
+
import { escapeHtml, html, unsafe, type RawHtml } from './html.ts';
|
|
31
50
|
import { layout } from './layout.ts';
|
|
32
51
|
import { renderEditorialFolio } from './chrome.ts';
|
|
33
|
-
import {
|
|
52
|
+
import { renderMasthead } from './masthead.ts';
|
|
53
|
+
import { renderMastheadMenu } from './masthead-menu.ts';
|
|
54
|
+
import { renderMobileBar } from './mobile-bar.ts';
|
|
55
|
+
import {
|
|
56
|
+
getShortformBarCells,
|
|
57
|
+
renderShortformMobileSheet,
|
|
58
|
+
} from './shortform-review-mobile-sheet.ts';
|
|
34
59
|
|
|
35
60
|
interface ShortformReviewQuery {
|
|
36
61
|
/** ?v=<n>; null shows the workflow's currentVersion. */
|
|
@@ -56,10 +81,6 @@ function stringField(v: unknown): string | undefined {
|
|
|
56
81
|
return typeof v === 'string' ? v : undefined;
|
|
57
82
|
}
|
|
58
83
|
|
|
59
|
-
function stateLabel(state?: string): string {
|
|
60
|
-
return (state ?? '').replace('-', ' ');
|
|
61
|
-
}
|
|
62
|
-
|
|
63
84
|
interface PreparedRender {
|
|
64
85
|
fm: Record<string, unknown>;
|
|
65
86
|
bodyHtml: string;
|
|
@@ -84,122 +105,12 @@ async function prepareShortformRender(markdown: string): Promise<PreparedRender>
|
|
|
84
105
|
return { fm, bodyHtml: renderedHtml };
|
|
85
106
|
}
|
|
86
107
|
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
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
|
-
|
|
108
|
+
/**
|
|
109
|
+
* Desktop-only edit panes. The `data-edit-mode` body attribute is the
|
|
110
|
+
* gate (set by the existing client toggle-edit handler). On mobile,
|
|
111
|
+
* edit mode is not currently surfaced from this surface — the broader
|
|
112
|
+
* shortform state-machine migration is the right place to design that.
|
|
113
|
+
*/
|
|
203
114
|
function renderEditPanes(): RawHtml {
|
|
204
115
|
return unsafe(html`
|
|
205
116
|
<div class="er-edit-mode" data-edit-panes-host hidden>
|
|
@@ -280,6 +191,10 @@ export async function renderShortformReviewPage(
|
|
|
280
191
|
const draftState = { workflow, currentVersion, versions };
|
|
281
192
|
const titleField = stringField(fm.title) ?? `Draft: ${slug}`;
|
|
282
193
|
|
|
194
|
+
// #244 — extract TOC from the rendered body. `rehype-slug` already
|
|
195
|
+
// gave every h2/h3/h4 an `id`; `extractToc` reads the slugs + text.
|
|
196
|
+
const tocEntries = extractToc(bodyHtml);
|
|
197
|
+
|
|
283
198
|
const shortformMeta: RawHtml = unsafe(html`
|
|
284
199
|
<div class="er-shortform-meta">
|
|
285
200
|
<span class="er-platform">${workflow.platform ?? 'other'}</span>
|
|
@@ -290,6 +205,32 @@ export async function renderShortformReviewPage(
|
|
|
290
205
|
|
|
291
206
|
const folioSpine = `shortform · ${workflow.platform ?? '?'}${workflow.channel ? ` · ${workflow.channel}` : ''} · ${slug}`;
|
|
292
207
|
|
|
208
|
+
// v7 universal masthead (mobile-only at this commit). Kicker carries
|
|
209
|
+
// the platform badge + channel as inline markup; the meta tag holds
|
|
210
|
+
// the galley number. Slug occupies the bottom row.
|
|
211
|
+
const platformLabel = workflow.platform ?? 'other';
|
|
212
|
+
const channelLabel = workflow.channel ? ` ${workflow.channel}` : '';
|
|
213
|
+
const mastheadKicker = unsafe(html`<span class="platform">${platformLabel}</span>${channelLabel}`);
|
|
214
|
+
const masthead = renderMasthead({
|
|
215
|
+
kickerHtml: mastheadKicker,
|
|
216
|
+
slug,
|
|
217
|
+
metaInline: `№ ${currentVersion.version}`,
|
|
218
|
+
isHub: false,
|
|
219
|
+
});
|
|
220
|
+
|
|
221
|
+
// v7 universal mobile bar. The cell list is composed mechanically
|
|
222
|
+
// from the surface's TOC + Versions + (always) Actions per
|
|
223
|
+
// DESIGN-STANDARDS.md § Universal bar contract. The Actions cell
|
|
224
|
+
// guarantees the bar is never empty.
|
|
225
|
+
const barCells = getShortformBarCells({ tocEntries, versions });
|
|
226
|
+
const mobileBar = renderMobileBar({ contextual: barCells });
|
|
227
|
+
const mobileSheet = renderShortformMobileSheet({
|
|
228
|
+
tocEntries,
|
|
229
|
+
versions,
|
|
230
|
+
workflow,
|
|
231
|
+
currentVersion,
|
|
232
|
+
});
|
|
233
|
+
|
|
293
234
|
const pageGrid = html`
|
|
294
235
|
<div class="er-page-grid">
|
|
295
236
|
<div class="er-draft-frame">
|
|
@@ -301,30 +242,16 @@ export async function renderShortformReviewPage(
|
|
|
301
242
|
|
|
302
243
|
const body = html`
|
|
303
244
|
<div data-review-ui="shortform" class="er-review-shell">
|
|
245
|
+
${masthead}
|
|
246
|
+
${renderMastheadMenu()}
|
|
304
247
|
${renderEditorialFolio('shortform', folioSpine)}
|
|
305
248
|
${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
249
|
<article class="er-page">
|
|
323
250
|
${unsafe(pageGrid)}
|
|
324
251
|
</article>
|
|
252
|
+
${mobileBar}
|
|
253
|
+
${mobileSheet}
|
|
325
254
|
<div class="er-toast" data-toast hidden></div>
|
|
326
|
-
${renderShortcutsOverlay()}
|
|
327
|
-
<div class="er-poll-indicator" data-poll>auto-refresh · 8s</div>
|
|
328
255
|
</div>`;
|
|
329
256
|
|
|
330
257
|
return layout({
|
|
@@ -334,6 +261,7 @@ export async function renderShortformReviewPage(
|
|
|
334
261
|
'/static/css/editorial-nav.css',
|
|
335
262
|
'/static/css/blog-figure.css',
|
|
336
263
|
'/static/css/review-viewport.css',
|
|
264
|
+
'/static/css/mobile-shell.css',
|
|
337
265
|
],
|
|
338
266
|
bodyHtml: body,
|
|
339
267
|
embeddedJson: [{ id: 'draft-state', data: draftState }],
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"shortform.d.ts","sourceRoot":"","sources":["../../src/pages/shortform.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AAIH,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,kBAAkB,CAAC;
|
|
1
|
+
{"version":3,"file":"shortform.d.ts","sourceRoot":"","sources":["../../src/pages/shortform.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AAIH,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,kBAAkB,CAAC;AAmGtD,wBAAgB,mBAAmB,CAAC,GAAG,EAAE,aAAa,GAAG,MAAM,CA4C9D"}
|
package/dist/pages/shortform.js
CHANGED
|
@@ -72,7 +72,6 @@ function renderRow(w, now) {
|
|
|
72
72
|
<span class="er-row-site er-row-site--${w.site}" title="${w.site}">${siteLabel(w.site)}</span>
|
|
73
73
|
<span class="er-row-slug">${w.slug}</span>
|
|
74
74
|
${channelMarkup}
|
|
75
|
-
<span class="er-stamp er-stamp-${w.state}">${w.state.replace('-', ' ')}</span>
|
|
76
75
|
<span class="er-row-ts">v${w.currentVersion} · ${fmtRelTime(w.updatedAt, now)}</span>
|
|
77
76
|
<span class="er-row-hint">Open in review →</span>
|
|
78
77
|
</a>`);
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"shortform.js","sourceRoot":"","sources":["../../src/pages/shortform.ts"],"names":[],"mappings":"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
|
|
1
|
+
{"version":3,"file":"shortform.js","sourceRoot":"","sources":["../../src/pages/shortform.ts"],"names":[],"mappings":"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"}
|
package/dist/pages/shortform.ts
CHANGED
|
@@ -82,7 +82,6 @@ function renderRow(w: DraftWorkflowItem, now: Date): RawHtml {
|
|
|
82
82
|
<span class="er-row-site er-row-site--${w.site}" title="${w.site}">${siteLabel(w.site)}</span>
|
|
83
83
|
<span class="er-row-slug">${w.slug}</span>
|
|
84
84
|
${channelMarkup}
|
|
85
|
-
<span class="er-stamp er-stamp-${w.state}">${w.state.replace('-', ' ')}</span>
|
|
86
85
|
<span class="er-row-ts">v${w.currentVersion} · ${fmtRelTime(w.updatedAt, now)}</span>
|
|
87
86
|
<span class="er-row-hint">Open in review →</span>
|
|
88
87
|
</a>`);
|
package/dist/server.js
CHANGED
|
@@ -474,10 +474,50 @@ async function main() {
|
|
|
474
474
|
const { createServer: createViteServer } = await import('vite');
|
|
475
475
|
const { getRequestListener } = await import('@hono/node-server');
|
|
476
476
|
const http = await import('node:http');
|
|
477
|
+
const net = await import('node:net');
|
|
477
478
|
const { join } = await import('node:path');
|
|
479
|
+
// Vite's HMR WebSocket needs its own port. Default is 24678. When
|
|
480
|
+
// multiple worktrees run dev studios simultaneously, the second
|
|
481
|
+
// worktree's Vite cannot bind 24678 — it silently fails to spin
|
|
482
|
+
// up an HMR WS server, and the browser ends up trying to connect
|
|
483
|
+
// to whatever process IS on 24678 (the OTHER worktree's Vite).
|
|
484
|
+
// The handshake fails with HTTP 426; the Vite client falls back
|
|
485
|
+
// to "polling for restart" mode which page-reloads several times
|
|
486
|
+
// per second. That presents to the operator as "pathologically
|
|
487
|
+
// refreshing as fast as possible." Walk forward to find a free
|
|
488
|
+
// port so each concurrent worktree gets its own HMR slot.
|
|
489
|
+
const findFreePort = async (base, max) => {
|
|
490
|
+
for (let p = base; p <= max; p += 1) {
|
|
491
|
+
const free = await new Promise((resolve) => {
|
|
492
|
+
const probe = net.createServer();
|
|
493
|
+
probe.once('error', () => resolve(false));
|
|
494
|
+
probe.once('listening', () => {
|
|
495
|
+
probe.close(() => resolve(true));
|
|
496
|
+
});
|
|
497
|
+
// Bind to all interfaces (dual-stack IPv4+IPv6) so the probe
|
|
498
|
+
// matches what Vite actually does. A previous version of this
|
|
499
|
+
// helper bound 127.0.0.1 only and got false-positives when
|
|
500
|
+
// another worktree's Vite was on IPv6 wildcard (*:24678) —
|
|
501
|
+
// the IPv4 probe succeeded, then Vite's actual bind failed.
|
|
502
|
+
probe.listen(p);
|
|
503
|
+
});
|
|
504
|
+
if (free)
|
|
505
|
+
return p;
|
|
506
|
+
}
|
|
507
|
+
throw new Error(`deskwork-studio: no free HMR port in [${base}, ${max}]. ` +
|
|
508
|
+
`Another worktree's dev studio is likely holding the range.`);
|
|
509
|
+
};
|
|
510
|
+
const hmrPort = await findFreePort(24678, 24777);
|
|
478
511
|
const viteRoot = join(pluginRoot(), 'public');
|
|
479
512
|
const vite = await createViteServer({
|
|
480
|
-
server: {
|
|
513
|
+
server: {
|
|
514
|
+
middlewareMode: true,
|
|
515
|
+
allowedHosts: true,
|
|
516
|
+
// Pin the HMR WS to the port we just verified free. Without
|
|
517
|
+
// this, Vite uses its default 24678 and silently loses to
|
|
518
|
+
// any other worktree that grabbed it first.
|
|
519
|
+
hmr: { port: hmrPort },
|
|
520
|
+
},
|
|
481
521
|
appType: 'custom',
|
|
482
522
|
root: viteRoot,
|
|
483
523
|
});
|