@deskwork/studio 0.12.1 → 0.13.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (50) hide show
  1. package/dist/components/scrapbook-item.d.ts +9 -1
  2. package/dist/components/scrapbook-item.d.ts.map +1 -1
  3. package/dist/components/scrapbook-item.js +11 -2
  4. package/dist/components/scrapbook-item.js.map +1 -1
  5. package/dist/data/glossary.json +62 -0
  6. package/dist/lib/glossary-helper.d.ts +16 -0
  7. package/dist/lib/glossary-helper.d.ts.map +1 -0
  8. package/dist/lib/glossary-helper.js +26 -0
  9. package/dist/lib/glossary-helper.js.map +1 -0
  10. package/dist/pages/chrome.d.ts +24 -13
  11. package/dist/pages/chrome.d.ts.map +1 -1
  12. package/dist/pages/chrome.js +25 -24
  13. package/dist/pages/chrome.js.map +1 -1
  14. package/dist/pages/chrome.ts +38 -27
  15. package/dist/pages/content-detail.js +1 -1
  16. package/dist/pages/content-detail.js.map +1 -1
  17. package/dist/pages/content-detail.ts +1 -1
  18. package/dist/pages/entry-review.js +2 -2
  19. package/dist/pages/entry-review.js.map +1 -1
  20. package/dist/pages/entry-review.ts +2 -2
  21. package/dist/pages/html.d.ts +2 -0
  22. package/dist/pages/html.d.ts.map +1 -1
  23. package/dist/pages/html.js +2 -0
  24. package/dist/pages/html.js.map +1 -1
  25. package/dist/pages/html.ts +4 -0
  26. package/dist/pages/layout.d.ts.map +1 -1
  27. package/dist/pages/layout.js +6 -0
  28. package/dist/pages/layout.js.map +1 -1
  29. package/dist/pages/layout.ts +7 -0
  30. package/dist/pages/review-scrapbook-drawer.d.ts +7 -0
  31. package/dist/pages/review-scrapbook-drawer.d.ts.map +1 -1
  32. package/dist/pages/review-scrapbook-drawer.js +45 -6
  33. package/dist/pages/review-scrapbook-drawer.js.map +1 -1
  34. package/dist/pages/review-scrapbook-drawer.ts +50 -6
  35. package/dist/pages/review.d.ts.map +1 -1
  36. package/dist/pages/review.js +168 -41
  37. package/dist/pages/review.js.map +1 -1
  38. package/dist/pages/review.ts +192 -41
  39. package/dist/pages/scrapbook.d.ts +7 -14
  40. package/dist/pages/scrapbook.d.ts.map +1 -1
  41. package/dist/pages/scrapbook.js +352 -193
  42. package/dist/pages/scrapbook.js.map +1 -1
  43. package/dist/pages/scrapbook.ts +390 -222
  44. package/dist/pages/shortform.js +1 -1
  45. package/dist/pages/shortform.js.map +1 -1
  46. package/dist/pages/shortform.ts +1 -1
  47. package/dist/server.d.ts.map +1 -1
  48. package/dist/server.js +10 -13
  49. package/dist/server.js.map +1 -1
  50. package/package.json +4 -4
@@ -4,6 +4,7 @@
4
4
  */
5
5
  import { escapeHtml } from "./html.js";
6
6
  import { clientScriptTag, viteClientTag } from "../lib/client-script.js";
7
+ import glossary from '../data/glossary.json' with { type: 'json' };
7
8
  export function layout(options) {
8
9
  const { title, cssHrefs, bodyHtml, bodyAttrs, embeddedJson, scriptModules, } = options;
9
10
  const cssTags = cssHrefs
@@ -16,10 +17,14 @@ export function layout(options) {
16
17
  return ` <script type="application/json"${idPart}${attrPart}>${escapeForScriptTag(JSON.stringify(j.data))}</script>`;
17
18
  })
18
19
  .join('\n');
20
+ // Inline glossary data into every page so the tooltip client can access it.
21
+ const glossaryInline = `<script>window.__GLOSSARY__ = ${JSON.stringify(glossary).replace(/</g, '\\u003c')};</script>`;
19
22
  const hmrTag = viteClientTag();
23
+ const glossaryClientTag = clientScriptTag('glossary-tooltip');
20
24
  const scriptTags = [
21
25
  ...(hmrTag ? [` ${hmrTag}`] : []),
22
26
  ...scriptModules.map((name) => ` ${clientScriptTag(name)}`),
27
+ ` ${glossaryClientTag}`,
23
28
  ].join('\n');
24
29
  const bodyOpen = bodyAttrs ? `<body ${bodyAttrs}>` : '<body>';
25
30
  return `<!DOCTYPE html>
@@ -30,6 +35,7 @@ export function layout(options) {
30
35
  <meta name="robots" content="noindex">
31
36
  <title>${escapeHtml(title)}</title>
32
37
  ${cssTags}
38
+ ${glossaryInline}
33
39
  </head>
34
40
  ${bodyOpen}
35
41
  ${bodyHtml}
@@ -1 +1 @@
1
- {"version":3,"file":"layout.js","sourceRoot":"","sources":["../../src/pages/layout.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,OAAO,EAAE,UAAU,EAAE,MAAM,WAAW,CAAC;AACvC,OAAO,EAAE,eAAe,EAAE,aAAa,EAAE,MAAM,yBAAyB,CAAC;AAoCzE,MAAM,UAAU,MAAM,CAAC,OAAsB;IAC3C,MAAM,EACJ,KAAK,EACL,QAAQ,EACR,QAAQ,EACR,SAAS,EACT,YAAY,EACZ,aAAa,GACd,GAAG,OAAO,CAAC;IAEZ,MAAM,OAAO,GAAG,QAAQ;SACrB,GAAG,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,oCAAoC,UAAU,CAAC,IAAI,CAAC,IAAI,CAAC;SACvE,IAAI,CAAC,IAAI,CAAC,CAAC;IAEd,MAAM,QAAQ,GAAG,CAAC,YAAY,IAAI,EAAE,CAAC;SAClC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE;QACT,MAAM,QAAQ,GAAG,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,IAAI,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;QAC5C,MAAM,MAAM,GAAG,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,QAAQ,UAAU,CAAC,CAAC,CAAC,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,CAAC;QACvD,OAAO,sCAAsC,MAAM,GAAG,QAAQ,IAAI,kBAAkB,CAAC,IAAI,CAAC,SAAS,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,WAAW,CAAC;IAC1H,CAAC,CAAC;SACD,IAAI,CAAC,IAAI,CAAC,CAAC;IAEd,MAAM,MAAM,GAAG,aAAa,EAAE,CAAC;IAC/B,MAAM,UAAU,GAAG;QACjB,GAAG,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,OAAO,MAAM,EAAE,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC;QACpC,GAAG,aAAa,CAAC,GAAG,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,OAAO,eAAe,CAAC,IAAI,CAAC,EAAE,CAAC;KAC/D,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;IAEb,MAAM,QAAQ,GAAG,SAAS,CAAC,CAAC,CAAC,SAAS,SAAS,GAAG,CAAC,CAAC,CAAC,QAAQ,CAAC;IAE9D,OAAO;;;;;;aAMI,UAAU,CAAC,KAAK,CAAC;EAC5B,OAAO;;IAEL,QAAQ;EACV,QAAQ;EACR,QAAQ;EACR,UAAU;;;CAGX,CAAC;AACF,CAAC;AAED,SAAS,UAAU,CAAC,CAAS;IAC3B,OAAO,UAAU,CAAC,CAAC,CAAC,CAAC;AACvB,CAAC;AAED;;;;GAIG;AACH,SAAS,kBAAkB,CAAC,IAAY;IACtC,OAAO,IAAI,CAAC,OAAO,CAAC,mBAAmB,EAAE,QAAQ,CAAC,CAAC;AACrD,CAAC"}
1
+ {"version":3,"file":"layout.js","sourceRoot":"","sources":["../../src/pages/layout.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,OAAO,EAAE,UAAU,EAAE,MAAM,WAAW,CAAC;AACvC,OAAO,EAAE,eAAe,EAAE,aAAa,EAAE,MAAM,yBAAyB,CAAC;AACzE,OAAO,QAAQ,MAAM,uBAAuB,CAAC,OAAO,IAAI,EAAE,MAAM,EAAE,CAAC;AAoCnE,MAAM,UAAU,MAAM,CAAC,OAAsB;IAC3C,MAAM,EACJ,KAAK,EACL,QAAQ,EACR,QAAQ,EACR,SAAS,EACT,YAAY,EACZ,aAAa,GACd,GAAG,OAAO,CAAC;IAEZ,MAAM,OAAO,GAAG,QAAQ;SACrB,GAAG,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,oCAAoC,UAAU,CAAC,IAAI,CAAC,IAAI,CAAC;SACvE,IAAI,CAAC,IAAI,CAAC,CAAC;IAEd,MAAM,QAAQ,GAAG,CAAC,YAAY,IAAI,EAAE,CAAC;SAClC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE;QACT,MAAM,QAAQ,GAAG,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,IAAI,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;QAC5C,MAAM,MAAM,GAAG,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,QAAQ,UAAU,CAAC,CAAC,CAAC,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,CAAC;QACvD,OAAO,sCAAsC,MAAM,GAAG,QAAQ,IAAI,kBAAkB,CAAC,IAAI,CAAC,SAAS,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,WAAW,CAAC;IAC1H,CAAC,CAAC;SACD,IAAI,CAAC,IAAI,CAAC,CAAC;IAEd,4EAA4E;IAC5E,MAAM,cAAc,GAAG,iCAAiC,IAAI,CAAC,SAAS,CAAC,QAAQ,CAAC,CAAC,OAAO,CAAC,IAAI,EAAE,SAAS,CAAC,YAAY,CAAC;IAEtH,MAAM,MAAM,GAAG,aAAa,EAAE,CAAC;IAC/B,MAAM,iBAAiB,GAAG,eAAe,CAAC,kBAAkB,CAAC,CAAC;IAC9D,MAAM,UAAU,GAAG;QACjB,GAAG,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,OAAO,MAAM,EAAE,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC;QACpC,GAAG,aAAa,CAAC,GAAG,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,OAAO,eAAe,CAAC,IAAI,CAAC,EAAE,CAAC;QAC9D,OAAO,iBAAiB,EAAE;KAC3B,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;IAEb,MAAM,QAAQ,GAAG,SAAS,CAAC,CAAC,CAAC,SAAS,SAAS,GAAG,CAAC,CAAC,CAAC,QAAQ,CAAC;IAE9D,OAAO;;;;;;aAMI,UAAU,CAAC,KAAK,CAAC;EAC5B,OAAO;MACH,cAAc;;IAEhB,QAAQ;EACV,QAAQ;EACR,QAAQ;EACR,UAAU;;;CAGX,CAAC;AACF,CAAC;AAED,SAAS,UAAU,CAAC,CAAS;IAC3B,OAAO,UAAU,CAAC,CAAC,CAAC,CAAC;AACvB,CAAC;AAED;;;;GAIG;AACH,SAAS,kBAAkB,CAAC,IAAY;IACtC,OAAO,IAAI,CAAC,OAAO,CAAC,mBAAmB,EAAE,QAAQ,CAAC,CAAC;AACrD,CAAC"}
@@ -5,6 +5,7 @@
5
5
 
6
6
  import { escapeHtml } from './html.ts';
7
7
  import { clientScriptTag, viteClientTag } from '../lib/client-script.ts';
8
+ import glossary from '../data/glossary.json' with { type: 'json' };
8
9
 
9
10
  export interface EmbeddedJson {
10
11
  /** `id` attribute of the `<script type="application/json">` tag. */
@@ -62,10 +63,15 @@ export function layout(options: LayoutOptions): string {
62
63
  })
63
64
  .join('\n');
64
65
 
66
+ // Inline glossary data into every page so the tooltip client can access it.
67
+ const glossaryInline = `<script>window.__GLOSSARY__ = ${JSON.stringify(glossary).replace(/</g, '\\u003c')};</script>`;
68
+
65
69
  const hmrTag = viteClientTag();
70
+ const glossaryClientTag = clientScriptTag('glossary-tooltip');
66
71
  const scriptTags = [
67
72
  ...(hmrTag ? [` ${hmrTag}`] : []),
68
73
  ...scriptModules.map((name) => ` ${clientScriptTag(name)}`),
74
+ ` ${glossaryClientTag}`,
69
75
  ].join('\n');
70
76
 
71
77
  const bodyOpen = bodyAttrs ? `<body ${bodyAttrs}>` : '<body>';
@@ -78,6 +84,7 @@ export function layout(options: LayoutOptions): string {
78
84
  <meta name="robots" content="noindex">
79
85
  <title>${escapeHtml(title)}</title>
80
86
  ${cssTags}
87
+ ${glossaryInline}
81
88
  </head>
82
89
  ${bodyOpen}
83
90
  ${bodyHtml}
@@ -11,6 +11,13 @@
11
11
  * directory via the index. This makes writingcontrol-shape entries
12
12
  * (slug != fs path) list their items at the actual file location.
13
13
  * Falls back to slug-template addressing for unbound / legacy entries.
14
+ *
15
+ * Issue #154 Dispatch D: this drawer is now a real bottom-anchored
16
+ * expandable drawer. The handle (header) is a clickable role=button
17
+ * that toggles `body[data-drawer]`; the drawer height transitions
18
+ * 4rem (collapsed) → 22rem (expanded). The standalone-viewer link is
19
+ * demoted to a small inline affordance — the primary action is now
20
+ * "expand the drawer" rather than "navigate away."
14
21
  */
15
22
  import type { ContentIndex } from '@deskwork/core/content-index';
16
23
  import type { CalendarEntry } from '@deskwork/core/types';
@@ -1 +1 @@
1
- {"version":3,"file":"review-scrapbook-drawer.d.ts","sourceRoot":"","sources":["../../src/pages/review-scrapbook-drawer.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AAYH,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,8BAA8B,CAAC;AACjE,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,sBAAsB,CAAC;AAC1D,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,kBAAkB,CAAC;AAOtD,OAAO,EAAgB,KAAK,OAAO,EAAE,MAAM,WAAW,CAAC;AA2DvD,wBAAgB,qBAAqB,CACnC,GAAG,EAAE,aAAa,EAClB,IAAI,EAAE,MAAM,EACZ,KAAK,EAAE,aAAa,GAAG,IAAI,EAC3B,IAAI,EAAE,MAAM,EACZ,KAAK,CAAC,EAAE,YAAY,GACnB,OAAO,CAkDT"}
1
+ {"version":3,"file":"review-scrapbook-drawer.d.ts","sourceRoot":"","sources":["../../src/pages/review-scrapbook-drawer.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;GAoBG;AAYH,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,8BAA8B,CAAC;AACjE,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,sBAAsB,CAAC;AAC1D,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,kBAAkB,CAAC;AAOtD,OAAO,EAA4B,KAAK,OAAO,EAAE,MAAM,WAAW,CAAC;AAqFnE,wBAAgB,qBAAqB,CACnC,GAAG,EAAE,aAAa,EAClB,IAAI,EAAE,MAAM,EACZ,KAAK,EAAE,aAAa,GAAG,IAAI,EAC3B,IAAI,EAAE,MAAM,EACZ,KAAK,CAAC,EAAE,YAAY,GACnB,OAAO,CA6DT"}
@@ -11,13 +11,20 @@
11
11
  * directory via the index. This makes writingcontrol-shape entries
12
12
  * (slug != fs path) list their items at the actual file location.
13
13
  * Falls back to slug-template addressing for unbound / legacy entries.
14
+ *
15
+ * Issue #154 Dispatch D: this drawer is now a real bottom-anchored
16
+ * expandable drawer. The handle (header) is a clickable role=button
17
+ * that toggles `body[data-drawer]`; the drawer height transitions
18
+ * 4rem (collapsed) → 22rem (expanded). The standalone-viewer link is
19
+ * demoted to a small inline affordance — the primary action is now
20
+ * "expand the drawer" rather than "navigate away."
14
21
  */
15
22
  import { readFileSync } from 'node:fs';
16
23
  import { join } from 'node:path';
17
24
  import { listScrapbook, listScrapbookAtDir, scrapbookDirForEntry, } from '@deskwork/core/scrapbook';
18
25
  import { resolveContentDir } from '@deskwork/core/paths';
19
26
  import { renderEmptyScrapbookRow, renderReadOnlyScrapbookRow, scrapbookViewerUrl, } from "../components/scrapbook-item.js";
20
- import { html, unsafe } from "./html.js";
27
+ import { html, unsafe, escapeHtml } from "./html.js";
21
28
  /**
22
29
  * Build an inline-text loader for the shared scrapbook-item renderer.
23
30
  * Reads at most `maxBytes` from a file inside the scrapbook directory
@@ -48,11 +55,32 @@ function makeInlineTextLoader(ctx, site, entry, slug, index) {
48
55
  }
49
56
  function renderScrapbookDrawerItems(site, slug, items, loader) {
50
57
  if (items.length === 0) {
51
- return renderEmptyScrapbookRow();
58
+ return renderEmptyScrapbookRow({ site, path: slug });
52
59
  }
53
60
  const rows = items.map((item) => renderReadOnlyScrapbookRow({ site, path: slug }, item, { inlinePreviewLoader: loader }));
54
61
  return unsafe(rows.map((r) => r.__raw).join(''));
55
62
  }
63
+ /**
64
+ * Build the "peek" line shown in the collapsed handle — up to 3 item
65
+ * filenames separated by `·`, plus a `+ N more` suffix when there are
66
+ * additional items. Empty scrapbook renders an inline empty-state hint
67
+ * so the operator still sees the surface.
68
+ */
69
+ function renderPeek(items, secretItems) {
70
+ const all = [...items, ...secretItems];
71
+ if (all.length === 0) {
72
+ return '<span class="er-scrapbook-drawer-peek-empty">(empty — drop research here)</span>';
73
+ }
74
+ const shown = all
75
+ .slice(0, 3)
76
+ .map((i) => `<span>${escapeHtml(i.name)}</span>`)
77
+ .join('<span class="sep">·</span>');
78
+ const remaining = all.length - 3;
79
+ const suffix = remaining > 0
80
+ ? `<span class="sep">·</span><span>+ ${remaining} more</span>`
81
+ : '';
82
+ return shown + suffix;
83
+ }
56
84
  export function renderScrapbookDrawer(ctx, site, entry, slug, index) {
57
85
  const summary = (() => {
58
86
  try {
@@ -75,13 +103,24 @@ export function renderScrapbookDrawer(ctx, site, entry, slug, index) {
75
103
  const loader = makeInlineTextLoader(ctx, site, entry, slug, index);
76
104
  return unsafe(html `
77
105
  <aside class="er-scrapbook-drawer" data-scrapbook-drawer aria-label="Scrapbook for this entry">
78
- <header class="er-scrapbook-drawer-head">
79
- <span class="er-scrapbook-drawer-kicker">§ Scrapbook</span>
106
+ <header class="er-scrapbook-drawer-handle" data-drawer-toggle role="button" tabindex="0"
107
+ aria-expanded="false" aria-controls="er-scrapbook-drawer-body">
108
+ <span class="er-scrapbook-drawer-kicker"><em>§</em> Scrapbook</span>
80
109
  <span class="er-scrapbook-drawer-count">${total} ${total === 1 ? 'item' : 'items'}</span>
110
+ <span class="er-scrapbook-drawer-peek" aria-hidden="true">
111
+ ${unsafe(renderPeek(items, secretItems))}
112
+ </span>
81
113
  <a class="er-scrapbook-drawer-open" href="${scrapbookViewerUrl({ site, path: slug })}"
82
- title="Open the standalone scrapbook viewer">open ↗</a>
114
+ title="Open the standalone scrapbook viewer"
115
+ onclick="event.stopPropagation()">open viewer ↗</a>
116
+ <button class="er-scrapbook-drawer-toggle" type="button" data-drawer-toggle
117
+ aria-controls="er-scrapbook-drawer-body" tabindex="-1">
118
+ <span data-toggle-label>Expand</span>
119
+ <span class="chev" aria-hidden="true">▾</span>
120
+ </button>
83
121
  </header>
84
- <div class="er-scrapbook-drawer-body">
122
+ <div class="er-scrapbook-drawer-body" id="er-scrapbook-drawer-body"
123
+ role="region" aria-label="scrapbook items">
85
124
  ${renderScrapbookDrawerItems(site, slug, items, loader)}
86
125
  ${secretItems.length > 0
87
126
  ? unsafe(html `
@@ -1 +1 @@
1
- {"version":3,"file":"review-scrapbook-drawer.js","sourceRoot":"","sources":["../../src/pages/review-scrapbook-drawer.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"review-scrapbook-drawer.js","sourceRoot":"","sources":["../../src/pages/review-scrapbook-drawer.ts"],"names":[],"mappings":"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"}
@@ -11,6 +11,13 @@
11
11
  * directory via the index. This makes writingcontrol-shape entries
12
12
  * (slug != fs path) list their items at the actual file location.
13
13
  * Falls back to slug-template addressing for unbound / legacy entries.
14
+ *
15
+ * Issue #154 Dispatch D: this drawer is now a real bottom-anchored
16
+ * expandable drawer. The handle (header) is a clickable role=button
17
+ * that toggles `body[data-drawer]`; the drawer height transitions
18
+ * 4rem (collapsed) → 22rem (expanded). The standalone-viewer link is
19
+ * demoted to a small inline affordance — the primary action is now
20
+ * "expand the drawer" rather than "navigate away."
14
21
  */
15
22
 
16
23
  import { readFileSync } from 'node:fs';
@@ -32,7 +39,7 @@ import {
32
39
  scrapbookViewerUrl,
33
40
  type InlineTextLoader,
34
41
  } from '../components/scrapbook-item.ts';
35
- import { html, unsafe, type RawHtml } from './html.ts';
42
+ import { html, unsafe, escapeHtml, type RawHtml } from './html.ts';
36
43
 
37
44
  /**
38
45
  * Build an inline-text loader for the shared scrapbook-item renderer.
@@ -79,7 +86,7 @@ function renderScrapbookDrawerItems(
79
86
  loader: InlineTextLoader,
80
87
  ): RawHtml {
81
88
  if (items.length === 0) {
82
- return renderEmptyScrapbookRow();
89
+ return renderEmptyScrapbookRow({ site, path: slug });
83
90
  }
84
91
  const rows = items.map((item) =>
85
92
  renderReadOnlyScrapbookRow(
@@ -91,6 +98,32 @@ function renderScrapbookDrawerItems(
91
98
  return unsafe(rows.map((r) => r.__raw).join(''));
92
99
  }
93
100
 
101
+ /**
102
+ * Build the "peek" line shown in the collapsed handle — up to 3 item
103
+ * filenames separated by `·`, plus a `+ N more` suffix when there are
104
+ * additional items. Empty scrapbook renders an inline empty-state hint
105
+ * so the operator still sees the surface.
106
+ */
107
+ function renderPeek(
108
+ items: readonly ScrapbookItem[],
109
+ secretItems: readonly ScrapbookItem[],
110
+ ): string {
111
+ const all = [...items, ...secretItems];
112
+ if (all.length === 0) {
113
+ return '<span class="er-scrapbook-drawer-peek-empty">(empty — drop research here)</span>';
114
+ }
115
+ const shown = all
116
+ .slice(0, 3)
117
+ .map((i) => `<span>${escapeHtml(i.name)}</span>`)
118
+ .join('<span class="sep">·</span>');
119
+ const remaining = all.length - 3;
120
+ const suffix =
121
+ remaining > 0
122
+ ? `<span class="sep">·</span><span>+ ${remaining} more</span>`
123
+ : '';
124
+ return shown + suffix;
125
+ }
126
+
94
127
  export function renderScrapbookDrawer(
95
128
  ctx: StudioContext,
96
129
  site: string,
@@ -126,13 +159,24 @@ export function renderScrapbookDrawer(
126
159
 
127
160
  return unsafe(html`
128
161
  <aside class="er-scrapbook-drawer" data-scrapbook-drawer aria-label="Scrapbook for this entry">
129
- <header class="er-scrapbook-drawer-head">
130
- <span class="er-scrapbook-drawer-kicker">§ Scrapbook</span>
162
+ <header class="er-scrapbook-drawer-handle" data-drawer-toggle role="button" tabindex="0"
163
+ aria-expanded="false" aria-controls="er-scrapbook-drawer-body">
164
+ <span class="er-scrapbook-drawer-kicker"><em>§</em> Scrapbook</span>
131
165
  <span class="er-scrapbook-drawer-count">${total} ${total === 1 ? 'item' : 'items'}</span>
166
+ <span class="er-scrapbook-drawer-peek" aria-hidden="true">
167
+ ${unsafe(renderPeek(items, secretItems))}
168
+ </span>
132
169
  <a class="er-scrapbook-drawer-open" href="${scrapbookViewerUrl({ site, path: slug })}"
133
- title="Open the standalone scrapbook viewer">open ↗</a>
170
+ title="Open the standalone scrapbook viewer"
171
+ onclick="event.stopPropagation()">open viewer ↗</a>
172
+ <button class="er-scrapbook-drawer-toggle" type="button" data-drawer-toggle
173
+ aria-controls="er-scrapbook-drawer-body" tabindex="-1">
174
+ <span data-toggle-label>Expand</span>
175
+ <span class="chev" aria-hidden="true">▾</span>
176
+ </button>
134
177
  </header>
135
- <div class="er-scrapbook-drawer-body">
178
+ <div class="er-scrapbook-drawer-body" id="er-scrapbook-drawer-body"
179
+ role="region" aria-label="scrapbook items">
136
180
  ${renderScrapbookDrawerItems(site, slug, items, loader)}
137
181
  ${
138
182
  secretItems.length > 0
@@ -1 +1 @@
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;AAiU7C,wBAAsB,gBAAgB,CACpC,GAAG,EAAE,aAAa,EAClB,MAAM,EAAE,YAAY,EACpB,KAAK,EAAE,WAAW,EAClB,QAAQ,CAAC,EAAE,iBAAiB,GAC3B,OAAO,CAAC,MAAM,CAAC,CAuJjB"}
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"}
@@ -28,7 +28,7 @@ import { splitOutline } from '@deskwork/core/outline-split';
28
28
  import { html, unsafe } from "./html.js";
29
29
  import { layout } from "./layout.js";
30
30
  import { renderEditorialFolio } from "./chrome.js";
31
- import { escapeHtml } from "./html.js";
31
+ import { escapeHtml, gloss } from "./html.js";
32
32
  import { renderScrapbookDrawer } from "./review-scrapbook-drawer.js";
33
33
  import { existsSync } from 'node:fs';
34
34
  import { resolveCalendarPath } from '@deskwork/core/paths';
@@ -123,23 +123,46 @@ function pendingSkillCmd(workflow) {
123
123
  }
124
124
  return '';
125
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
+ }
126
144
  function renderControlsRight(workflow) {
127
145
  const isActive = workflow.state === 'open' || workflow.state === 'in-review';
128
146
  const isApproved = workflow.state === 'approved';
129
147
  const isIterating = workflow.state === 'iterating';
130
148
  const isTerminal = workflow.state === 'applied' || workflow.state === 'cancelled';
131
149
  const buttons = [];
132
- buttons.push(html `<button class="er-btn er-btn-small" data-action="toggle-edit" type="button">Edit</button>`);
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>`);
133
155
  if (isActive) {
134
- buttons.push(html `<button class="er-btn er-btn-small er-btn-approve" data-action="approve" type="button">Approve</button>`);
135
- buttons.push(html `<button class="er-btn er-btn-small" data-action="iterate" type="button">Iterate</button>`);
136
- buttons.push(html `<button class="er-btn er-btn-small er-btn-reject" data-action="reject" type="button">Reject</button>`);
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'));
137
160
  }
138
161
  if (isApproved) {
139
162
  const applyCmd = pendingSkillCmd(workflow);
140
163
  buttons.push(html `<span class="er-pending-state">awaiting apply…</span>`);
141
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>`);
142
- buttons.push(html `<button class="er-btn er-btn-small er-btn-reject" data-action="reject" type="button">Reject</button>`);
165
+ buttons.push(shortcutChipWrap(html `<button class="er-btn er-btn-small er-btn-reject" data-action="reject" type="button">Reject</button>`, 'r'));
143
166
  }
144
167
  if (isIterating) {
145
168
  const iterateCmd = pendingSkillCmd(workflow);
@@ -160,7 +183,7 @@ function renderError(slug, site, contentKind, message) {
160
183
  : `/deskwork:review-start --site ${site} ${slug}`;
161
184
  const body = html `
162
185
  <div data-review-ui="longform">
163
- ${renderEditorialFolio('reviews', `longform · ${slug}`)}
186
+ ${renderEditorialFolio('longform', `longform · ${slug}`)}
164
187
  <div class="er-error">
165
188
  <h1>No galley to review.</h1>
166
189
  <p><strong>Slug:</strong> <code>${slug}</code></p>
@@ -194,6 +217,8 @@ function renderShortcutsOverlay() {
194
217
  <dt><kbd>i</kbd> <kbd>i</kbd></dt><dd>iterate <em>— press twice within 500ms</em></dd>
195
218
  <dt><kbd>r</kbd> <kbd>r</kbd></dt><dd>reject <em>— press twice within 500ms</em></dd>
196
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>
197
222
  <dt><kbd>?</kbd></dt><dd>this panel</dd>
198
223
  <dt><kbd>esc</kbd></dt><dd>close / cancel composer</dd>
199
224
  </dl>
@@ -201,11 +226,44 @@ function renderShortcutsOverlay() {
201
226
  </div>
202
227
  </div>`);
203
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
+ }
204
257
  function renderMarginalia() {
205
258
  return unsafe(html `
206
259
  <aside class="er-marginalia" data-comments-sidebar aria-label="Margin notes">
207
- <p class="er-marginalia-head">Margin notes</p>
208
- <p class="er-marginalia-empty" data-sidebar-empty>Select text in the draft, then either click the floating <em>Mark</em> pencil above your selection — or click anywhere here in the margin to open the note.</p>
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>
209
267
  <section class="er-marginalia-composer" data-comment-composer hidden aria-label="New margin note">
210
268
  <p class="er-marginalia-composer-head">New mark</p>
211
269
  <div class="er-marginalia-composer-quote" data-composer-quote></div>
@@ -230,24 +288,52 @@ function renderMarginalia() {
230
288
  <ol class="er-marginalia-list" data-sidebar-list></ol>
231
289
  </aside>`);
232
290
  }
233
- function renderEditMode(outlineHasContent) {
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) {
234
308
  const outlineBtnAttrs = outlineHasContent ? '' : ' hidden';
235
309
  return unsafe(html `
236
- <div class="er-edit-mode" data-edit-toolbar hidden>
237
- <div class="er-edit-chrome">
238
- <div class="er-edit-modes" role="tablist" aria-label="Editor mode">
239
- <button class="er-edit-mode-btn" data-edit-view="source" type="button" aria-pressed="true">Source</button>
240
- <button class="er-edit-mode-btn" data-edit-view="split" type="button" aria-pressed="false">Split</button>
241
- <button class="er-edit-mode-btn" data-edit-view="preview" type="button" aria-pressed="false">Preview</button>
242
- </div>
243
- <div class="er-edit-actions">
244
- <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>
245
- <button class="er-btn er-btn-small" data-action="focus-mode" type="button" title="Distraction-free mode (Shift+F)" aria-pressed="false">Focus ⛶</button>
246
- <button class="er-btn er-btn-primary" data-action="save-version" type="button">Save as new version</button>
247
- <button class="er-btn" data-action="cancel-edit" type="button">Cancel</button>
248
- <span class="er-edit-hint" data-edit-hint></span>
249
- </div>
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>
250
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>
251
337
  <div class="er-edit-panes" data-edit-panes data-view="source">
252
338
  <div class="er-edit-source" data-edit-source aria-label="Markdown source"></div>
253
339
  <div class="er-edit-preview" data-edit-preview aria-label="Rendered preview"></div>
@@ -385,29 +471,70 @@ export async function renderReviewPage(ctx, lookup, query, getIndex) {
385
471
  const folioSpine = isShortform
386
472
  ? `shortform · ${workflow.platform ?? '?'}${workflow.channel ? ` · ${workflow.channel}` : ''} · ${slug}`
387
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>`;
388
514
  const body = html `
389
515
  <div data-review-ui="${reviewUiAttr}" class="er-review-shell">
390
- ${renderEditorialFolio('reviews', folioSpine)}
516
+ ${renderEditorialFolio(folioActive, folioSpine)}
391
517
  ${shortformMeta}
392
- <div class="er-draft-frame">
393
- <div id="draft-body" data-draft-body
394
- title="Double-click to edit · select text to leave a margin note">${unsafe(bodyHtml)}</div>
395
- ${renderEditMode(outlineHtml.length > 0)}
396
- </div>
397
518
  <div class="er-strip">
398
- <a class="er-strip-back" href="/dev/editorial-studio" title="Back to the editorial studio">← studio</a>
399
- <span class="er-strip-galley">Galley <em>№ ${currentVersion.version}</em></span>
400
- <span class="er-strip-slug">${workflow.site} / ${workflow.slug}</span>
401
- ${renderVersionsStrip(versions, resolvedSite, contentKind, currentVersion)}
402
- <span class="er-strip-center">
403
- <span class="er-stamp er-stamp-big er-stamp-${workflow.state}" data-state-label>
404
- ${stateLabel(workflow.state)}
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>
405
529
  </span>
406
- <span class="er-strip-hint" aria-hidden="true">select text to mark · double-click to edit · <kbd>?</kbd> for shortcuts</span>
407
- </span>
408
- ${renderControlsRight(workflow)}
530
+ ${renderControlsRight(workflow)}
531
+ </div>
409
532
  </div>
410
- ${renderMarginalia()}
533
+ ${renderEditToolbar(outlineHtml.length > 0)}
534
+ <article class="er-page">
535
+ ${unsafe(pageGrid)}
536
+ </article>
537
+ ${isShortform ? unsafe('') : renderMarginaliaTab()}
411
538
  <button class="er-pencil-btn" data-add-comment-btn hidden type="button">Mark</button>
412
539
  ${isShortform ? unsafe('') : renderOutlineDrawer(outlineHtml)}
413
540
  ${isShortform