@miadi/stateloom-web 0.3.1 → 0.3.4

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 (79) hide show
  1. package/package.json +1 -1
  2. package/server/.next/BUILD_ID +1 -1
  3. package/server/.next/build-manifest.json +5 -5
  4. package/server/.next/server/app/_global-error/page/build-manifest.json +3 -3
  5. package/server/.next/server/app/_global-error/page.js.nft.json +1 -1
  6. package/server/.next/server/app/_global-error/page_client-reference-manifest.js +1 -1
  7. package/server/.next/server/app/_global-error.html +2 -2
  8. package/server/.next/server/app/_global-error.rsc +1 -1
  9. package/server/.next/server/app/_global-error.segments/__PAGE__.segment.rsc +1 -1
  10. package/server/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
  11. package/server/.next/server/app/_global-error.segments/_head.segment.rsc +1 -1
  12. package/server/.next/server/app/_global-error.segments/_index.segment.rsc +1 -1
  13. package/server/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
  14. package/server/.next/server/app/_not-found/page/build-manifest.json +3 -3
  15. package/server/.next/server/app/_not-found/page.js.nft.json +1 -1
  16. package/server/.next/server/app/_not-found/page_client-reference-manifest.js +1 -1
  17. package/server/.next/server/app/_not-found.html +1 -1
  18. package/server/.next/server/app/_not-found.rsc +2 -2
  19. package/server/.next/server/app/_not-found.segments/_full.segment.rsc +2 -2
  20. package/server/.next/server/app/_not-found.segments/_head.segment.rsc +1 -1
  21. package/server/.next/server/app/_not-found.segments/_index.segment.rsc +2 -2
  22. package/server/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +1 -1
  23. package/server/.next/server/app/_not-found.segments/_not-found.segment.rsc +1 -1
  24. package/server/.next/server/app/_not-found.segments/_tree.segment.rsc +2 -2
  25. package/server/.next/server/app/api/docs/route.js.nft.json +1 -1
  26. package/server/.next/server/app/api/file/route.js.nft.json +1 -1
  27. package/server/.next/server/app/api/marks/route.js.nft.json +1 -1
  28. package/server/.next/server/app/api/rispec/route.js.nft.json +1 -1
  29. package/server/.next/server/app/api/watch/route.js.nft.json +1 -1
  30. package/server/.next/server/app/index.html +1 -1
  31. package/server/.next/server/app/index.rsc +3 -3
  32. package/server/.next/server/app/index.segments/__PAGE__.segment.rsc +2 -2
  33. package/server/.next/server/app/index.segments/_full.segment.rsc +3 -3
  34. package/server/.next/server/app/index.segments/_head.segment.rsc +1 -1
  35. package/server/.next/server/app/index.segments/_index.segment.rsc +2 -2
  36. package/server/.next/server/app/index.segments/_tree.segment.rsc +2 -2
  37. package/server/.next/server/app/page/build-manifest.json +3 -3
  38. package/server/.next/server/app/page.js.nft.json +1 -1
  39. package/server/.next/server/app/page_client-reference-manifest.js +1 -1
  40. package/server/.next/server/chunks/ssr/{2374f_next_dist_0c464f98._.js → 2374f_next_dist_491bc9ca._.js} +2 -2
  41. package/server/.next/server/chunks/ssr/{[root-of-the-server]__385c85a7._.js → [root-of-the-server]__0d719a8a._.js} +2 -2
  42. package/server/.next/server/chunks/ssr/[root-of-the-server]__199d50b6._.js +3 -0
  43. package/server/.next/server/chunks/ssr/[root-of-the-server]__5c28bab6._.js +3 -0
  44. package/server/.next/server/chunks/ssr/[root-of-the-server]__edbb995c._.js +6 -6
  45. package/server/.next/server/middleware-build-manifest.js +3 -3
  46. package/server/.next/server/pages/404.html +1 -1
  47. package/server/.next/server/pages/500.html +2 -2
  48. package/server/.next/static/chunks/33df24ad2d3a04e7.js +1 -0
  49. package/server/.next/static/chunks/6052c4bfdd66ef5c.css +2 -0
  50. package/server/.next/static/chunks/b0af2862e9bb3e18.js +14 -0
  51. package/server/.next/static/chunks/b71bf89a3bbb72e1.js +5 -0
  52. package/server/.next/static/chunks/{turbopack-b3cae9131ae5086f.js → turbopack-3a774f5a5a8a9023.js} +1 -1
  53. package/server/package-lock.json +2 -2
  54. package/server/package.json +1 -1
  55. package/server/src/app/globals.css +60 -2
  56. package/server/src/app/page.tsx +8 -6
  57. package/server/src/components/ChromeIcon.tsx +106 -0
  58. package/server/src/components/PropertiesPanel.tsx +3 -2
  59. package/server/src/components/Toolbar.tsx +37 -20
  60. package/server/src/components/erd/ErdWorkspace.tsx +2 -1
  61. package/server/src/components/sequence/SequenceWorkspace.tsx +10 -1
  62. package/server/src/components/stage/CompanionLayer.tsx +41 -6
  63. package/server/src/components/stage/StageLayer.tsx +287 -28
  64. package/server/src/components/stage/StageProvider.tsx +149 -25
  65. package/server/src/components/stage/useEpisodePresence.ts +71 -0
  66. package/server/src/components/system/SystemWorkspace.tsx +26 -12
  67. package/server/src/lib/markRequest.ts +24 -0
  68. package/server/src/lib/useNarrow.ts +30 -0
  69. package/server/stateloom-web.json +2 -2
  70. package/server/tsconfig.tsbuildinfo +1 -1
  71. package/server/.next/server/chunks/ssr/[root-of-the-server]__9e661f75._.js +0 -3
  72. package/server/.next/server/chunks/ssr/[root-of-the-server]__f25b51be._.js +0 -3
  73. package/server/.next/static/chunks/0d24f3e3dbdef25e.css +0 -2
  74. package/server/.next/static/chunks/662d1b0ce457c16b.js +0 -1
  75. package/server/.next/static/chunks/96818f5620f039a1.js +0 -5
  76. package/server/.next/static/chunks/ec729c54327cc597.js +0 -14
  77. /package/server/.next/static/{pbn7-8jVWdRn8FLy4fHnU → ZoijkpoWD861NG-cIGJrO}/_buildManifest.js +0 -0
  78. /package/server/.next/static/{pbn7-8jVWdRn8FLy4fHnU → ZoijkpoWD861NG-cIGJrO}/_clientMiddlewareManifest.json +0 -0
  79. /package/server/.next/static/{pbn7-8jVWdRn8FLy4fHnU → ZoijkpoWD861NG-cIGJrO}/_ssgManifest.js +0 -0
@@ -16,6 +16,7 @@ import SequenceWorkspace from "@/components/sequence/SequenceWorkspace";
16
16
  import SystemWorkspace from "@/components/system/SystemWorkspace";
17
17
  import SystemStrip from "@/components/system/SystemStrip";
18
18
  import BoardBoundary from "@/components/BoardBoundary";
19
+ import ChromeIcon from "@/components/ChromeIcon";
19
20
  import IssueIcon from "@/components/IssueIcon";
20
21
  import StageProvider from "@/components/stage/StageProvider";
21
22
  import MachineStageLayer from "@/components/stage/MachineStageLayer";
@@ -137,9 +138,10 @@ function MachineDesigner() {
137
138
  const setDrawSource = useDesignerStore((s) => s.setDrawSource);
138
139
 
139
140
  const tabs: { id: Tab; label: string; icon: ReactNode; badge?: number }[] = [
140
- { id: "properties", label: "Properties", icon: "🎛" },
141
- { id: "events", label: "Events", icon: "⚡" },
142
- { id: "settings", label: "Settings", icon: "⚙" },
141
+ // Drawn, not typed: 🎛 ⚡ ⚙ are colour emoji on iOS. See ChromeIcon.
142
+ { id: "properties", label: "Properties", icon: <ChromeIcon name="sliders" size={16} /> },
143
+ { id: "events", label: "Events", icon: <ChromeIcon name="bolt" size={16} /> },
144
+ { id: "settings", label: "Settings", icon: <ChromeIcon name="gear" size={16} /> },
143
145
  // Drawn, not typed: "⚠" is a bright yellow colour emoji on iOS. See IssueIcon.
144
146
  { id: "validation", label: "Errors", icon: <IssueIcon />, badge: errors.length || undefined },
145
147
  ];
@@ -305,7 +307,7 @@ function MachineDesigner() {
305
307
  hideContextMenu();
306
308
  }}
307
309
  >
308
- ↗ Draw transition from here
310
+ <ChromeIcon name="draw" /> Draw transition from here
309
311
  </button>
310
312
  <button
311
313
  className="block w-full text-left text-sm text-red-400 hover:bg-gray-800 px-4 py-3 md:text-xs md:px-3 md:py-1.5"
@@ -314,7 +316,7 @@ function MachineDesigner() {
314
316
  hideContextMenu();
315
317
  }}
316
318
  >
317
- 🗑 Delete state
319
+ <ChromeIcon name="trash" /> Delete state
318
320
  </button>
319
321
  </>
320
322
  ) : (
@@ -327,7 +329,7 @@ function MachineDesigner() {
327
329
  hideContextMenu();
328
330
  }}
329
331
  >
330
- + Add state here
332
+ <ChromeIcon name="plus" /> Add state here
331
333
  </button>
332
334
  </>
333
335
  )}
@@ -0,0 +1,106 @@
1
+ /**
2
+ * The loom's chrome icons — the toolbar, the phone's tab bar, the context menu and the
3
+ * delete buttons — drawn as outlines in the current text colour (lane U, finding U6).
4
+ *
5
+ * The typed symbols they replace (📂 💾 🔄 📥 🖼 ⚡ 📜 🗑 🎛 ⚙ ✏️ ↩ ↪ ↗) are colour emoji
6
+ * on iOS: saturated, larger than the words beside them, and the first thing the eye lands
7
+ * on around a board whose stage, marks and dock are already drawn. Same style as
8
+ * IssueIcon and KindIcon: a 24-unit box, a 1.6 stroke, round ends.
9
+ */
10
+ export type ChromeIconName =
11
+ | "undo"
12
+ | "redo"
13
+ | "arrange"
14
+ | "draw"
15
+ | "open"
16
+ | "save"
17
+ | "reload"
18
+ | "download"
19
+ | "image"
20
+ | "plus"
21
+ | "check"
22
+ | "bolt"
23
+ | "scroll"
24
+ | "code"
25
+ | "trash"
26
+ | "sliders"
27
+ | "gear";
28
+
29
+ export default function ChromeIcon({ name, size = 14 }: { name: ChromeIconName; size?: number }) {
30
+ return (
31
+ <svg
32
+ width={size}
33
+ height={size}
34
+ viewBox="0 0 24 24"
35
+ fill="none"
36
+ stroke="currentColor"
37
+ strokeWidth="1.6"
38
+ strokeLinecap="round"
39
+ strokeLinejoin="round"
40
+ aria-hidden="true"
41
+ focusable="false"
42
+ className="inline-block shrink-0 align-[-2px]"
43
+ >
44
+ {name === "undo" && <path d="M9 7 4.5 11.5 9 16M5 11.5h9.5a5 5 0 0 1 0 10H12" />}
45
+ {name === "redo" && <path d="M15 7l4.5 4.5L15 16M19 11.5H9.5a5 5 0 0 0 0 10H12" />}
46
+ {name === "arrange" && (
47
+ // Three boxes in layers.
48
+ <>
49
+ <rect x="8.5" y="3" width="7" height="5" rx="1.5" />
50
+ <rect x="3" y="16" width="7" height="5" rx="1.5" />
51
+ <rect x="14" y="16" width="7" height="5" rx="1.5" />
52
+ <path d="M12 8v4M6.5 16v-2a2 2 0 0 1 2-2h7a2 2 0 0 1 2 2v2" />
53
+ </>
54
+ )}
55
+ {name === "draw" && (
56
+ // An arrow drawn from one state to another.
57
+ <>
58
+ <circle cx="5.5" cy="18.5" r="2" />
59
+ <path d="M7 17 18.5 5.5M11.5 5.5h7v7" />
60
+ </>
61
+ )}
62
+ {name === "open" && <path d="M3.5 7.5V18a1.5 1.5 0 0 0 1.5 1.5h14a1.5 1.5 0 0 0 1.5-1.5V9.5A1.5 1.5 0 0 0 19 8h-7l-2-2.5H5A1.5 1.5 0 0 0 3.5 7z" />}
63
+ {name === "save" && (
64
+ <>
65
+ <path d="M5 4h11l3.5 3.5V19a1 1 0 0 1-1 1H5a1 1 0 0 1-1-1V5a1 1 0 0 1 1-1z" />
66
+ <path d="M8 4v5h7V4M8 20v-6h8v6" />
67
+ </>
68
+ )}
69
+ {name === "reload" && <path d="M19.5 12a7.5 7.5 0 1 1-2.2-5.3M19.5 4v4.5H15" />}
70
+ {name === "download" && <path d="M12 4v11M7.5 10.5 12 15l4.5-4.5M4.5 19.5h15" />}
71
+ {name === "image" && (
72
+ <>
73
+ <rect x="3.5" y="5" width="17" height="14" rx="1.5" />
74
+ <circle cx="9" cy="10" r="1.6" />
75
+ <path d="m4 17.5 5-5 4 4 2.5-2.5 4.5 4.5" />
76
+ </>
77
+ )}
78
+ {name === "plus" && <path d="M12 5v14M5 12h14" />}
79
+ {name === "check" && <path d="m5 12.5 4.5 4.5L19 7.5" />}
80
+ {name === "bolt" && <path d="M13 3 5.5 13.5h5.5L10 21l8-11h-5.5z" />}
81
+ {name === "scroll" && (
82
+ // A page with lines: a written specification.
83
+ <>
84
+ <path d="M6.5 3.5h8l4 4v12a1 1 0 0 1-1 1h-11a1 1 0 0 1-1-1v-15a1 1 0 0 1 1-1z" />
85
+ <path d="M14.5 3.5v4h4M8.5 12h7M8.5 15.5h7M8.5 9h3" />
86
+ </>
87
+ )}
88
+ {name === "code" && <path d="m8.5 7.5-5 4.5 5 4.5M15.5 7.5l5 4.5-5 4.5M13.5 5l-3 14" />}
89
+ {name === "trash" && <path d="M4.5 6.5h15M9.5 6.5V4.5h5v2M6.5 6.5l.9 12.6a1.5 1.5 0 0 0 1.5 1.4h6.2a1.5 1.5 0 0 0 1.5-1.4l.9-12.6M10 10.5v6M14 10.5v6" />}
90
+ {name === "sliders" && (
91
+ <>
92
+ <path d="M5 4v16M12 4v16M19 4v16" />
93
+ <circle cx="5" cy="9" r="2" fill="currentColor" stroke="none" />
94
+ <circle cx="12" cy="15" r="2" fill="currentColor" stroke="none" />
95
+ <circle cx="19" cy="7.5" r="2" fill="currentColor" stroke="none" />
96
+ </>
97
+ )}
98
+ {name === "gear" && (
99
+ <>
100
+ <circle cx="12" cy="12" r="3" />
101
+ <path d="M12 3.5v2.2M12 18.3v2.2M3.5 12h2.2M18.3 12h2.2M6 6l1.6 1.6M16.4 16.4 18 18M6 18l1.6-1.6M16.4 7.6 18 6" />
102
+ </>
103
+ )}
104
+ </svg>
105
+ );
106
+ }
@@ -1,6 +1,7 @@
1
1
  "use client";
2
2
 
3
3
  import { useDesignerStore, collectAllStates, collectEventIds } from "@/store/useDesignerStore";
4
+ import ChromeIcon from "@/components/ChromeIcon";
4
5
  import type { StateDef, ActionDef } from "@/types/definition";
5
6
 
6
7
  function ActionEditor({ label, actions, onChange }: { label: string; actions: ActionDef[]; onChange: (actions: ActionDef[]) => void }) {
@@ -175,7 +176,7 @@ export default function PropertiesPanel() {
175
176
  onClick={() => removeState(selection.id!)}
176
177
  className="text-red-500 hover:text-red-400 text-xs mt-2"
177
178
  >
178
- 🗑 Delete State
179
+ <ChromeIcon name="trash" /> Delete State
179
180
  </button>
180
181
  </div>
181
182
  );
@@ -235,7 +236,7 @@ export default function PropertiesPanel() {
235
236
  onClick={() => removeTransition(stateName, idx)}
236
237
  className="text-red-500 hover:text-red-400 text-xs"
237
238
  >
238
- 🗑 Delete Transition
239
+ <ChromeIcon name="trash" /> Delete Transition
239
240
  </button>
240
241
  </div>
241
242
  );
@@ -9,7 +9,9 @@ import {
9
9
  type ExportFormat,
10
10
  } from "@/lib/exportImage";
11
11
  import type { StateDef } from "@/types/definition";
12
+ import ChromeIcon from "./ChromeIcon";
12
13
  import DocSwitcher from "./DocSwitcher";
14
+ import IssueIcon from "./IssueIcon";
13
15
  import PresenceChips from "./PresenceChips";
14
16
  import { docQuery, useRequestedDoc } from "@/lib/docParam";
15
17
 
@@ -40,7 +42,7 @@ export default function Toolbar() {
40
42
  const applyRemote = useDesignerStore((s) => s.applyRemote);
41
43
 
42
44
  // Save and reload must follow the SAME document the page is viewing —
43
- // without this, 💾 on a switched document would silently write the default
45
+ // without this, Disk on a switched document would silently write the default
44
46
  // file (the exact mismatch the allowlist exists to prevent).
45
47
  const requested = useRequestedDoc();
46
48
 
@@ -219,7 +221,9 @@ export default function Toolbar() {
219
221
  <span className="text-[10px] text-amber-400" title={remoteMessage ?? ""}>↻ remote changed</span>
220
222
  )}
221
223
  {remoteStatus === "error" && (
222
- <span className="text-[10px] text-red-400" title={remoteMessage ?? ""}>⚠ disk error</span>
224
+ <span className="inline-flex items-center gap-0.5 text-[10px] text-red-400" title={remoteMessage ?? ""}>
225
+ <IssueIcon size={11} /> disk error
226
+ </span>
223
227
  )}
224
228
  {remoteStatus === "idle" && (
225
229
  <span className="text-[10px] text-gray-600" title={remoteMessage ?? "No file bound"}>○ no disk</span>
@@ -239,16 +243,18 @@ export default function Toolbar() {
239
243
  disabled={undoStack.length === 0}
240
244
  className="toolbar-btn disabled:opacity-30"
241
245
  title="Undo (Ctrl+Z)"
246
+ aria-label="Undo"
242
247
  >
243
- ↩
248
+ <ChromeIcon name="undo" />
244
249
  </button>
245
250
  <button
246
251
  onClick={redo}
247
252
  disabled={redoStack.length === 0}
248
253
  className="toolbar-btn disabled:opacity-30"
249
254
  title="Redo (Ctrl+Y)"
255
+ aria-label="Redo"
250
256
  >
251
- ↪
257
+ <ChromeIcon name="redo" />
252
258
  </button>
253
259
 
254
260
  <div className="w-px h-4 bg-gray-700" />
@@ -259,7 +265,7 @@ export default function Toolbar() {
259
265
  className="toolbar-btn"
260
266
  title="Arrange states into layers (auto-layout)"
261
267
  >
262
- ⤢ Arrange
268
+ <ChromeIcon name="arrange" /> Arrange
263
269
  </button>
264
270
 
265
271
  <div className="w-px h-4 bg-gray-700" />
@@ -270,14 +276,14 @@ export default function Toolbar() {
270
276
  className={`toolbar-btn ${drawMode === "transition" ? "!bg-amber-700 !text-white" : ""}`}
271
277
  title={drawMode === "transition" ? "Exit draw mode (Esc)" : "Draw transition mode"}
272
278
  >
273
- {drawMode === "transition" ? "✏️ Drawing" : "↗ Draw"}
279
+ <ChromeIcon name="draw" /> {drawMode === "transition" ? "Drawing" : "Draw"}
274
280
  </button>
275
281
 
276
282
  <div className="w-px h-4 bg-gray-700" />
277
283
 
278
284
  {/* File ops */}
279
- <button onClick={handleOpen} className="toolbar-btn" title="Open local .smdf.json (browser)">
280
- 📂
285
+ <button onClick={handleOpen} className="toolbar-btn" title="Open local .smdf.json (browser)" aria-label="Open a local file">
286
+ <ChromeIcon name="open" />
281
287
  </button>
282
288
  <button
283
289
  onClick={handleSaveDisk}
@@ -285,17 +291,18 @@ export default function Toolbar() {
285
291
  className="toolbar-btn disabled:opacity-40"
286
292
  title="Save to disk (STATELOOM_PROJECT_FILE)"
287
293
  >
288
- 💾 Disk
294
+ <ChromeIcon name="save" /> Disk
289
295
  </button>
290
296
  <button
291
297
  onClick={handleReloadFromDisk}
292
298
  className="toolbar-btn"
293
299
  title="Reload from disk"
300
+ aria-label="Reload from disk"
294
301
  >
295
- 🔄
302
+ <ChromeIcon name="reload" />
296
303
  </button>
297
- <button onClick={handleDownload} className="toolbar-btn" title="Download as file">
298
- 📥
304
+ <button onClick={handleDownload} className="toolbar-btn" title="Download as file" aria-label="Download as file">
305
+ <ChromeIcon name="download" />
299
306
  </button>
300
307
 
301
308
  {/* Export the board — pictures from the canvas as it stands, mermaid and
@@ -322,7 +329,13 @@ export default function Toolbar() {
322
329
  className={`toolbar-btn disabled:opacity-40 ${exportError ? "!text-red-400" : ""}`}
323
330
  title={exportError ?? `Export the board as ${FORMAT_LABEL[imageFormat]}`}
324
331
  >
325
- {exporting ? "…" : "🖼 Export"}
332
+ {exporting ? (
333
+ "…"
334
+ ) : (
335
+ <>
336
+ <ChromeIcon name="image" /> Export
337
+ </>
338
+ )}
326
339
  </button>
327
340
  {exported && !exportError && (
328
341
  <span className="text-[10px] text-emerald-500 truncate max-w-[40vw] md:max-w-[16rem]" title={exported}>
@@ -346,12 +359,16 @@ export default function Toolbar() {
346
359
  if (e.key === "Escape") setAddingState(false);
347
360
  }}
348
361
  />
349
- <button onClick={handleAddState} className="toolbar-btn-sm">✓</button>
350
- <button onClick={() => setAddingState(false)} className="toolbar-btn-sm">✕</button>
362
+ <button onClick={handleAddState} className="toolbar-btn-sm" aria-label="Add the state">
363
+ <ChromeIcon name="check" />
364
+ </button>
365
+ <button onClick={() => setAddingState(false)} className="toolbar-btn-sm" aria-label="Cancel">
366
+ ✕
367
+ </button>
351
368
  </div>
352
369
  ) : (
353
370
  <button onClick={() => setAddingState(true)} className="toolbar-btn">
354
- +State
371
+ <ChromeIcon name="plus" /> State
355
372
  </button>
356
373
  )}
357
374
 
@@ -359,7 +376,7 @@ export default function Toolbar() {
359
376
 
360
377
  {/* Validate & Generate */}
361
378
  <button onClick={() => validate()} className="toolbar-btn">
362
- ✓ Validate
379
+ <ChromeIcon name="check" /> Validate
363
380
  </button>
364
381
  <select
365
382
  value={generateLang}
@@ -370,16 +387,16 @@ export default function Toolbar() {
370
387
  <option value="typescript">TypeScript</option>
371
388
  </select>
372
389
  <button onClick={handleGenerate} className="toolbar-btn">
373
- ⚡ Generate
390
+ <ChromeIcon name="bolt" /> Generate
374
391
  </button>
375
392
  <button onClick={handleGenerateRispec} className="toolbar-btn" title="Generate RISE rispec markdown">
376
- 📜 RISE
393
+ <ChromeIcon name="scroll" /> RISE
377
394
  </button>
378
395
  <button
379
396
  onClick={() => setShowCodePreview(!showCodePreview)}
380
397
  className={`toolbar-btn ${showCodePreview ? "text-blue-400" : ""}`}
381
398
  >
382
- {"</>"} Code
399
+ <ChromeIcon name="code" /> Code
383
400
  </button>
384
401
  </div>
385
402
  </div>
@@ -55,6 +55,7 @@ import {
55
55
  normalizeNotes,
56
56
  } from "@miadi/stateloom-protocol";
57
57
  import BoardBoundary from "@/components/BoardBoundary";
58
+ import ChromeIcon from "@/components/ChromeIcon";
58
59
  import DocSwitcher from "@/components/DocSwitcher";
59
60
  import IssueIcon from "@/components/IssueIcon";
60
61
  import LockIcon from "@/components/LockIcon";
@@ -724,7 +725,7 @@ export default function ErdWorkspace() {
724
725
  setSelection(null);
725
726
  }}
726
727
  >
727
- 🗑 Delete {selected.name}
728
+ <ChromeIcon name="trash" /> Delete {selected.name}
728
729
  </button>
729
730
  </>
730
731
  )}
@@ -218,6 +218,15 @@ export default function SequenceWorkspace() {
218
218
  const view = useMemo(() => (def ? drawableSequence(def) : null), [def]);
219
219
  const layout = useMemo(() => (view ? sequenceLayout(view) : null), [view]);
220
220
  const stageBoxes = useMemo(() => (view && layout ? safely(() => sequenceElementBoxes(view, layout), []) : []), [view, layout]);
221
+ // A message is named on the marks by its number and its words, as the board shows it.
222
+ const stageLabels = useMemo(() => {
223
+ const out: Record<string, string> = {};
224
+ for (const { ref, message } of view ? safely(() => allMessages(view), []) : []) {
225
+ const words = message?.label ? `${ref}. ${message.label}` : `message ${ref}`;
226
+ out[`message:${ref}`] = words.length > 48 ? `${words.slice(0, 47)}…` : words;
227
+ }
228
+ return out;
229
+ }, [view]);
221
230
  const stageFocus = useUrlParam("focus");
222
231
  // A malformed document is reported as one issue, not a crash.
223
232
  const problems = useMemo<SqdValidationError[]>(() => {
@@ -586,7 +595,7 @@ export default function SequenceWorkspace() {
586
595
  {/* The stage (Spec 83). In the list view nothing is drawn to light, so a
587
596
  step that lights something here brings the diagram back. */}
588
597
  {def && layout && mode === "diagram" && (
589
- <StageLayer docPath={docPath || null} kind="sequence" viewport={viewport} setViewport={setViewport} boxes={stageBoxes} boardRef={boardRef} focus={stageFocus} />
598
+ <StageLayer docPath={docPath || null} kind="sequence" viewport={viewport} setViewport={setViewport} boxes={stageBoxes} boardRef={boardRef} focus={stageFocus} labels={stageLabels} />
590
599
  )}
591
600
  {def && layout && mode === "list" && <StageReveal docPath={docPath} onReveal={() => setMode("diagram")} />}
592
601
 
@@ -19,10 +19,17 @@
19
19
  * - once the person presses ▶ or ■, the waiting sequence is over: each reply plays when
20
20
  * they choose it.
21
21
  *
22
+ * The conversation holds the person's marks on the episode's drawings too (the service
23
+ * reads their sidecars): each mark with its thread, and tapping one opens its drawing at
24
+ * that mark's thread. On a phone the closed dock is the bar at the top of the sheet at the
25
+ * bottom of the board, and the open conversation takes the caption's place in that sheet;
26
+ * it and a mark's card take turns.
27
+ *
22
28
  * Everything reaches the service through `/api/companion/` (this designer's proxy to
23
29
  * `STATELOOM_COMPANION_URL`); a reply is never posted from here.
24
30
  */
25
31
  import { useCallback, useEffect, useMemo, useRef, useState } from "react";
32
+ import { createPortal } from "react-dom";
26
33
  import {
27
34
  browserVoice,
28
35
  createCompanionThread,
@@ -35,10 +42,11 @@ import {
35
42
  type TakeRecorder,
36
43
  type ThreadSnapshot,
37
44
  } from "@miadi/stateloom-react";
38
- import { CompanionDock } from "@miadi/stateloom-canvas";
45
+ import { CompanionDock, type DockMark } from "@miadi/stateloom-canvas";
39
46
  import { marksWaitingOn, readMarksFile } from "@miadi/stateloom-protocol";
40
47
  import { requestedDoc } from "@/lib/docParam";
41
- import { urlParam } from "@/lib/systemParam";
48
+ import { openInSystem, urlParam } from "@/lib/systemParam";
49
+ import { requestMark } from "@/lib/markRequest";
42
50
  import type { StageValue } from "./StageProvider";
43
51
 
44
52
  const BASE = "/api/companion/";
@@ -316,7 +324,7 @@ export default function CompanionLayer({ stage, defaultEpisode }: { stage: Stage
316
324
  const reply = threadRef.current?.getSnapshot().items.find((i): i is CompanionReply => i.kind === "reply" && i.id === id);
317
325
  if (!reply) return;
318
326
  if (busy(recRef.current.state)) {
319
- setNotice(`Send your take first. Then press ▶ to hear ${WHO}.`);
327
+ setNotice(`Send your take first. Then press play to hear ${WHO}.`);
320
328
  return;
321
329
  }
322
330
  queue.current = [];
@@ -350,11 +358,33 @@ export default function CompanionLayer({ stage, defaultEpisode }: { stage: Stage
350
358
 
351
359
  const waiting = thread.waiting ? { take: thread.waiting.take, seconds: Math.max(0, Math.floor((now - thread.waiting.since) / 1000)) } : null;
352
360
 
353
- return (
354
- <div className="stage-talk-host">
361
+ // ── The phone's sheet: the open conversation holds it in the caption's place ──
362
+ const { narrow, sheet: sheetOwner, claimSheet, releaseSheet } = stage;
363
+ useEffect(() => {
364
+ if (open) claimSheet("talk");
365
+ else releaseSheet("talk");
366
+ }, [open, claimSheet, releaseSheet]);
367
+ useEffect(() => () => releaseSheet("talk"), [releaseSheet]);
368
+ // A mark's card took the sheet: the conversation folds to its bar.
369
+ const [seenOwner, setSeenOwner] = useState(sheetOwner);
370
+ if (seenOwner !== sheetOwner) {
371
+ setSeenOwner(sheetOwner);
372
+ if (narrow && sheetOwner === "card") setOpenState(false);
373
+ }
374
+
375
+ // A mark in the conversation: its drawing, at its thread.
376
+ const onOpenMark = useCallback((mark: DockMark) => {
377
+ if (window.matchMedia("(max-width: 767px)").matches) setOpenState(false);
378
+ requestMark(mark.doc, mark.id);
379
+ if (stageRef.current.board !== mark.doc) openInSystem({ doc: mark.doc, focus: null });
380
+ }, []);
381
+
382
+ const dock = (
355
383
  <CompanionDock
356
384
  open={open}
357
385
  onOpenChange={setOpen}
386
+ variant={narrow ? "bar" : "fab"}
387
+ onOpenMark={onOpenMark}
358
388
  who={WHO}
359
389
  episode={episode}
360
390
  episodes={episodes}
@@ -377,6 +407,11 @@ export default function CompanionLayer({ stage, defaultEpisode }: { stage: Stage
377
407
  notice={notice}
378
408
  error={problem ?? thread.error}
379
409
  />
380
- </div>
381
410
  );
411
+ if (narrow) {
412
+ // The bar is the sheet's handle; the open conversation is its body.
413
+ const slot = open ? stage.sheetBody : stage.sheetHandle;
414
+ return slot ? createPortal(dock, slot) : null;
415
+ }
416
+ return <div className="stage-talk-host">{dock}</div>;
382
417
  }