@deskwork/studio 0.9.8 → 0.10.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.
@@ -17,6 +17,8 @@
17
17
  * Read-only — links to existing routes only. No editing capability here.
18
18
  */
19
19
 
20
+ import { readWorkflows } from '@deskwork/core/review/pipeline';
21
+ import type { DraftWorkflowItem } from '@deskwork/core/review/types';
20
22
  import type { StudioContext } from '../routes/api.ts';
21
23
  import { html, unsafe, type RawHtml } from './html.ts';
22
24
  import { layout } from './layout.ts';
@@ -29,8 +31,17 @@ interface IndexEntry {
29
31
  titleHtml: string;
30
32
  /** Plain-text fallback for accessibility (used as link text). */
31
33
  titleText: string;
32
- /** Route path. When `template` is set, no link renders. */
34
+ /** Route path. When `template` is set, the path is shown but the
35
+ * hyperlink target comes from `linkHref` instead. */
33
36
  route: string;
37
+ /**
38
+ * Explicit link target. When set, the title becomes a link to this
39
+ * URL — even for templated entries (where the visual route hint
40
+ * stays alongside as a placeholder). When omitted, behavior depends
41
+ * on `template`: non-templated entries link to `route`; templated
42
+ * entries render as plain text.
43
+ */
44
+ linkHref?: string;
34
45
  /**
35
46
  * For templated routes (longform reviews, scrapbook), this is the
36
47
  * placeholder text shown in red-pencil italic. The route string still
@@ -52,90 +63,139 @@ interface IndexSection {
52
63
  entries: IndexEntry[];
53
64
  }
54
65
 
55
- const SECTIONS: readonly IndexSection[] = [
56
- {
57
- ornament: '¶',
58
- name: 'Pipeline',
59
- count: 'i. 1 surface',
60
- entries: [
61
- {
62
- numeral: 'I',
63
- titleHtml: 'Dashboard',
64
- titleText: 'Dashboard',
65
- route: '/dev/editorial-studio',
66
- desc: 'Press-check. The calendar across all sites; awaiting press; recent proofs; voice-drift signal.',
67
- },
68
- ],
69
- },
70
- {
71
- ornament: '¶',
72
- name: 'Review desk',
73
- count: 'ii.–iii. 2 surfaces',
74
- entries: [
75
- {
76
- numeral: 'II',
77
- titleHtml: 'Shortform reviews',
78
- titleText: 'Shortform reviews',
79
- route: '/dev/editorial-review-shortform',
80
- desc: 'Cross-platform copy desk. Reddit, LinkedIn, YouTube, Instagram — galley slips, one per platform.',
81
- },
82
- {
83
- numeral: 'III',
84
- titleHtml: 'Longform reviews',
85
- titleText: 'Longform reviews',
86
- route: '/dev/editorial-review/<slug>',
87
- template: { prefix: '/dev/editorial-review/', placeholder: '<slug>' },
88
- desc: 'Per-entry margin notes, decisions, iterate flow.',
89
- hint: 'entry-by-entry',
90
- postHint: 'Reach via the Dashboard or Content view; each review is opened against a specific slug.',
91
- },
92
- ],
93
- },
94
- {
95
- ornament: '',
96
- name: 'Browse',
97
- count: 'iv.–v. — 2 surfaces',
98
- entries: [
99
- {
100
- numeral: 'IV',
101
- titleHtml: 'Content view',
102
- titleText: 'Content view',
103
- route: '/dev/content',
104
- desc: 'The shape of the work. A drillable tree of nodes; click any to read its head matter and browse its scrapbook.',
105
- },
106
- {
107
- numeral: 'V',
108
- titleHtml: 'Scrapbook',
109
- titleText: 'Scrapbook',
110
- route: '/dev/scrapbook/<site>/<path>',
111
- template: { prefix: '/dev/scrapbook/', placeholder: '<site>/<path>' },
112
- desc: 'Research, receipts, working notes. Addressed by hierarchical path; secret items appear in their own section.',
113
- hint: 'path-addressed',
114
- postHint: "Reach via the Content view's per-node drawer, or address directly.",
115
- },
116
- ],
117
- },
118
- {
119
- ornament: '',
120
- name: 'Reference',
121
- count: 'vi. — 1 surface',
122
- entries: [
123
- {
124
- numeral: 'VI',
125
- titleHtml: "The Compositor's <em>Manual</em>",
126
- titleText: "The Compositor's Manual",
127
- route: '/dev/editorial-help',
128
- desc: 'The workflow, the skill catalogue, the names of the things — read once, return when the work asks.',
129
- },
130
- ],
131
- },
132
- ];
66
+ /**
67
+ * Pick the workflow that should be the default Longform-reviews target —
68
+ * the most-recent open longform workflow (in-review or open). Returns
69
+ * null when no candidate exists; the caller falls back to the dashboard's
70
+ * Review section anchor.
71
+ */
72
+ function pickDefaultLongformWorkflow(
73
+ workflows: readonly DraftWorkflowItem[],
74
+ ): DraftWorkflowItem | null {
75
+ const candidates = workflows
76
+ .filter((w) => w.contentKind === 'longform')
77
+ .filter((w) => w.state === 'in-review' || w.state === 'open')
78
+ .slice()
79
+ .sort((a, b) => b.updatedAt.localeCompare(a.updatedAt));
80
+ return candidates[0] ?? null;
81
+ }
82
+
83
+ function buildSections(ctx: StudioContext): readonly IndexSection[] {
84
+ const workflows: readonly DraftWorkflowItem[] = (() => {
85
+ try {
86
+ return readWorkflows(ctx.projectRoot, ctx.config);
87
+ } catch {
88
+ return [];
89
+ }
90
+ })();
91
+ const longformDefault = pickDefaultLongformWorkflow(workflows);
92
+ // Issue #107: III links to the most-recent in-review longform when
93
+ // one exists, else falls back to the dashboard's Review section
94
+ // anchor (`#stage-review`, mounted in sub-phase D). The visual
95
+ // template hint stays alongside the link so adopters still see the
96
+ // URL shape — `<slug>` placeholder shown in red-pencil italic.
97
+ const longformLinkHref =
98
+ longformDefault !== null
99
+ ? `/dev/editorial-review/${longformDefault.id}`
100
+ : '/dev/editorial-studio#stage-review';
101
+
102
+ return [
103
+ {
104
+ ornament: '¶',
105
+ name: 'Pipeline',
106
+ count: 'i. — 1 surface',
107
+ entries: [
108
+ {
109
+ numeral: 'I',
110
+ titleHtml: 'Dashboard',
111
+ titleText: 'Dashboard',
112
+ route: '/dev/editorial-studio',
113
+ desc: 'Press-check. The calendar across all sites; awaiting press; recent proofs; voice-drift signal.',
114
+ },
115
+ ],
116
+ },
117
+ {
118
+ ornament: '',
119
+ name: 'Review desk',
120
+ count: 'ii.–iii. — 2 surfaces',
121
+ entries: [
122
+ {
123
+ numeral: 'II',
124
+ titleHtml: 'Shortform reviews',
125
+ titleText: 'Shortform reviews',
126
+ route: '/dev/editorial-review-shortform',
127
+ desc: 'Cross-platform copy desk. Reddit, LinkedIn, YouTube, Instagram — galley slips, one per platform.',
128
+ },
129
+ {
130
+ numeral: 'III',
131
+ titleHtml: 'Longform reviews',
132
+ titleText: 'Longform reviews',
133
+ route: '/dev/editorial-review/<slug>',
134
+ linkHref: longformLinkHref,
135
+ template: { prefix: '/dev/editorial-review/', placeholder: '<slug>' },
136
+ desc: 'Per-entry margin notes, decisions, iterate flow.',
137
+ hint: 'entry-by-entry',
138
+ postHint:
139
+ longformDefault !== null
140
+ ? `Defaults to the most-recent in-review longform (${longformDefault.slug}). Or reach via the Dashboard or Content view.`
141
+ : 'Defaults to the dashboard\'s Review section. Open a longform workflow to populate the per-entry deep-link.',
142
+ },
143
+ ],
144
+ },
145
+ {
146
+ ornament: '¶',
147
+ name: 'Browse',
148
+ count: 'iv.–v. — 2 surfaces',
149
+ entries: [
150
+ {
151
+ numeral: 'IV',
152
+ titleHtml: 'Content view',
153
+ titleText: 'Content view',
154
+ route: '/dev/content',
155
+ desc: 'The shape of the work. A drillable tree of nodes; click any to read its head matter and browse its scrapbook.',
156
+ },
157
+ {
158
+ numeral: 'V',
159
+ titleHtml: 'Scrapbook',
160
+ titleText: 'Scrapbook',
161
+ route: '/dev/scrapbook/<site>/<path>',
162
+ // Issue #107: scrapbook is reached by drilling into a content
163
+ // node. Default link points at the content view; the URL
164
+ // template hint stays so adopters see the addressing shape.
165
+ linkHref: '/dev/content',
166
+ template: { prefix: '/dev/scrapbook/', placeholder: '<site>/<path>' },
167
+ desc: 'Research, receipts, working notes. Addressed by hierarchical path; secret items appear in their own section.',
168
+ hint: 'path-addressed',
169
+ postHint: "Reach via the Content view's per-node drawer, or address directly.",
170
+ },
171
+ ],
172
+ },
173
+ {
174
+ ornament: '¶',
175
+ name: 'Reference',
176
+ count: 'vi. — 1 surface',
177
+ entries: [
178
+ {
179
+ numeral: 'VI',
180
+ titleHtml: "The Compositor's <em>Manual</em>",
181
+ titleText: "The Compositor's Manual",
182
+ route: '/dev/editorial-help',
183
+ desc: 'The workflow, the skill catalogue, the names of the things — read once, return when the work asks.',
184
+ },
185
+ ],
186
+ },
187
+ ];
188
+ }
133
189
 
134
190
  function renderEntryTitle(entry: IndexEntry): string {
135
- if (entry.template) {
191
+ // Explicit linkHref wins. Otherwise: non-templated entries link to
192
+ // their route; templated entries with no fallback render as plain
193
+ // text (the route is templated, can't be linked verbatim).
194
+ const href = entry.linkHref ?? (entry.template ? null : entry.route);
195
+ if (href === null) {
136
196
  return html`<span class="er-toc-entry__title">${unsafe(entry.titleHtml)}</span>`;
137
197
  }
138
- return html`<a class="er-toc-entry__title" href="${entry.route}">${unsafe(entry.titleHtml)}</a>`;
198
+ return html`<a class="er-toc-entry__title" href="${href}">${unsafe(entry.titleHtml)}</a>`;
139
199
  }
140
200
 
141
201
  function renderEntryRoute(entry: IndexEntry): string {
@@ -181,7 +241,8 @@ function renderSection(section: IndexSection): RawHtml {
181
241
  </section>`);
182
242
  }
183
243
 
184
- export function renderStudioIndex(_ctx: StudioContext): string {
244
+ export function renderStudioIndex(ctx: StudioContext): string {
245
+ const sections = buildSections(ctx);
185
246
  const body = html`
186
247
  ${renderEditorialFolio('index', 'index of the press')}
187
248
  <main class="er-toc-page">
@@ -193,9 +254,9 @@ export function renderStudioIndex(_ctx: StudioContext): string {
193
254
  Begin where the work is.
194
255
  </p>
195
256
  </header>
196
- ${SECTIONS.map(renderSection)}
257
+ ${sections.map(renderSection)}
197
258
  <footer class="er-toc-colophon">
198
- Pressed in the deskwork studio. Loopback only.<br>
259
+ Pressed in the deskwork studio.<br>
199
260
  <span class="er-toc-colophon__rule"></span>
200
261
  </footer>
201
262
  </main>`;
@@ -190,9 +190,9 @@ function renderShortcutsOverlay() {
190
190
  <dt><kbd>e</kbd> / dbl-click</dt><dd>toggle edit mode</dd>
191
191
  <dt>select text</dt><dd>leave a margin note</dd>
192
192
  <dt><kbd>⌘</kbd><kbd>↵</kbd> / <kbd>ctrl</kbd><kbd>↵</kbd></dt><dd>save margin note (in composer)</dd>
193
- <dt><kbd>a</kbd></dt><dd>approve</dd>
194
- <dt><kbd>i</kbd></dt><dd>iterate</dd>
195
- <dt><kbd>r</kbd></dt><dd>reject</dd>
193
+ <dt><kbd>a</kbd> <kbd>a</kbd></dt><dd>approve <em>— press twice within 500ms; first press arms, second fires</em></dd>
194
+ <dt><kbd>i</kbd> <kbd>i</kbd></dt><dd>iterate <em>— press twice within 500ms</em></dd>
195
+ <dt><kbd>r</kbd> <kbd>r</kbd></dt><dd>reject <em>— press twice within 500ms</em></dd>
196
196
  <dt><kbd>j</kbd> / <kbd>k</kbd></dt><dd>next / previous margin note</dd>
197
197
  <dt><kbd>?</kbd></dt><dd>this panel</dd>
198
198
  <dt><kbd>esc</kbd></dt><dd>close / cancel composer</dd>
@@ -275,9 +275,9 @@ function renderShortcutsOverlay(): RawHtml {
275
275
  <dt><kbd>e</kbd> / dbl-click</dt><dd>toggle edit mode</dd>
276
276
  <dt>select text</dt><dd>leave a margin note</dd>
277
277
  <dt><kbd>⌘</kbd><kbd>↵</kbd> / <kbd>ctrl</kbd><kbd>↵</kbd></dt><dd>save margin note (in composer)</dd>
278
- <dt><kbd>a</kbd></dt><dd>approve</dd>
279
- <dt><kbd>i</kbd></dt><dd>iterate</dd>
280
- <dt><kbd>r</kbd></dt><dd>reject</dd>
278
+ <dt><kbd>a</kbd> <kbd>a</kbd></dt><dd>approve <em>— press twice within 500ms; first press arms, second fires</em></dd>
279
+ <dt><kbd>i</kbd> <kbd>i</kbd></dt><dd>iterate <em>— press twice within 500ms</em></dd>
280
+ <dt><kbd>r</kbd> <kbd>r</kbd></dt><dd>reject <em>— press twice within 500ms</em></dd>
281
281
  <dt><kbd>j</kbd> / <kbd>k</kbd></dt><dd>next / previous margin note</dd>
282
282
  <dt><kbd>?</kbd></dt><dd>this panel</dd>
283
283
  <dt><kbd>esc</kbd></dt><dd>close / cancel composer</dd>
@@ -95,7 +95,7 @@ function renderEmptyState() {
95
95
  No short-form galleys on the desk.<br />
96
96
  Supported platforms: <em>${platformList}</em>.<br />
97
97
  Start a new shortform draft from the dashboard's
98
- <a href="/dev/editorial-studio">coverage matrix</a>.
98
+ <a href="/dev/editorial-studio#stage-drafting">Drafting list</a>.
99
99
  </div>`);
100
100
  }
101
101
  export function renderShortformPage(ctx) {
@@ -111,7 +111,7 @@ function renderEmptyState(): RawHtml {
111
111
  No short-form galleys on the desk.<br />
112
112
  Supported platforms: <em>${platformList}</em>.<br />
113
113
  Start a new shortform draft from the dashboard's
114
- <a href="/dev/editorial-studio">coverage matrix</a>.
114
+ <a href="/dev/editorial-studio#stage-drafting">Drafting list</a>.
115
115
  </div>`);
116
116
  }
117
117
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@deskwork/studio",
3
- "version": "0.9.8",
3
+ "version": "0.10.0",
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",
@@ -45,7 +45,7 @@
45
45
  "@codemirror/language": "^6.12.3",
46
46
  "@codemirror/state": "^6.6.0",
47
47
  "@codemirror/view": "^6.41.1",
48
- "@deskwork/core": "0.9.8",
48
+ "@deskwork/core": "0.10.0",
49
49
  "@hono/node-server": "^1.13.7",
50
50
  "@lezer/highlight": "^1.2.3",
51
51
  "esbuild": "^0.28.0",