@mosetta/editor 0.0.0-stage → 0.0.29

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 (115) hide show
  1. package/CHANGELOG.md +190 -0
  2. package/LICENSE +21 -0
  3. package/README.md +10 -2
  4. package/mosetta.json +6 -0
  5. package/package.json +35 -3
  6. package/prefabs/workbench/prefab.json +11 -0
  7. package/prefabs/workbench/versions/1.json +120 -0
  8. package/prefabs/workbench/versions/2.json +13 -0
  9. package/prefabs/workbench/versions/3.json +19 -0
  10. package/prefabs/workbench/versions/4.json +29 -0
  11. package/prefabs/workbench/versions/5.json +29 -0
  12. package/tiles/canvas/canvas.component.tsx +2038 -0
  13. package/tiles/canvas/canvas.css +973 -0
  14. package/tiles/canvas/canvasTools.ts +66 -0
  15. package/tiles/canvas/chromeNodes.ts +35 -0
  16. package/tiles/canvas/ghosts.ts +33 -0
  17. package/tiles/canvas/insertPopover.tsx +56 -0
  18. package/tiles/canvas/popoverPlacement.ts +104 -0
  19. package/tiles/canvas/popoverSize.ts +87 -0
  20. package/tiles/canvas/providerChrome.tsx +191 -0
  21. package/tiles/canvas/scrollWatch.ts +43 -0
  22. package/tiles/canvas/settingsPopover.tsx +257 -0
  23. package/tiles/canvas/settingsTabs.ts +78 -0
  24. package/tiles/canvas/toolPanel.tsx +86 -0
  25. package/tiles/content/autoBind.ts +61 -0
  26. package/tiles/content/blockPage.css +390 -0
  27. package/tiles/content/blockPage.tsx +379 -0
  28. package/tiles/content/blockPicker.css +215 -0
  29. package/tiles/content/blockPicker.tsx +501 -0
  30. package/tiles/content/blockUpgrades.ts +145 -0
  31. package/tiles/content/blockVariants.ts +185 -0
  32. package/tiles/content/blockVocabulary.ts +77 -0
  33. package/tiles/content/catalogVersions.ts +106 -0
  34. package/tiles/content/componentPreview.css +82 -0
  35. package/tiles/content/componentPreview.tsx +249 -0
  36. package/tiles/content/content.server.ts +593 -0
  37. package/tiles/content/contentCommon.ts +133 -0
  38. package/tiles/content/disconnected.ts +56 -0
  39. package/tiles/content/distributions.css +256 -0
  40. package/tiles/content/distributions.tsx +271 -0
  41. package/tiles/content/editingAutosave.ts +125 -0
  42. package/tiles/content/editingClipboard.ts +185 -0
  43. package/tiles/content/editingConflicts.ts +223 -0
  44. package/tiles/content/editingHotkeys.ts +59 -0
  45. package/tiles/content/editingNotices.ts +105 -0
  46. package/tiles/content/formRoot.ts +18 -0
  47. package/tiles/content/fuzzySearch.ts +121 -0
  48. package/tiles/content/mediaUpload.ts +55 -0
  49. package/tiles/content/moduleSiblings.ts +9 -0
  50. package/tiles/content/mountedTree.ts +136 -0
  51. package/tiles/content/nodeSettings.css +483 -0
  52. package/tiles/content/nodeSettings.tsx +963 -0
  53. package/tiles/content/pasteText.ts +131 -0
  54. package/tiles/content/prefabFields.tsx +158 -0
  55. package/tiles/content/prefabParts.ts +105 -0
  56. package/tiles/content/previewEnvironment.ts +46 -0
  57. package/tiles/content/projectConfig.ts +46 -0
  58. package/tiles/content/projectPrefabs.ts +57 -0
  59. package/tiles/content/publishModal.css +109 -0
  60. package/tiles/content/publishModal.tsx +92 -0
  61. package/tiles/content/refsEditing.ts +412 -0
  62. package/tiles/content/scaledStage.ts +63 -0
  63. package/tiles/content/searchField.css +32 -0
  64. package/tiles/content/searchField.tsx +32 -0
  65. package/tiles/content/settingsDraft.ts +178 -0
  66. package/tiles/content/settingsIssues.tsx +124 -0
  67. package/tiles/content/themeScope.ts +72 -0
  68. package/tiles/content/themeVariables.ts +47 -0
  69. package/tiles/content/tooltipPlacement.ts +17 -0
  70. package/tiles/content/tooltips.tsx +131 -0
  71. package/tiles/content/treeMounter.ts +33 -0
  72. package/tiles/content/updatesModal.css +124 -0
  73. package/tiles/content/updatesModal.tsx +110 -0
  74. package/tiles/content/useCtrlHeld.ts +24 -0
  75. package/tiles/content/useEditingSession.ts +115 -0
  76. package/tiles/content/useMountedTree.ts +47 -0
  77. package/tiles/content/workbenchScale.css +27 -0
  78. package/tiles/hostFallback/hostFallback.component.tsx +32 -0
  79. package/tiles/hostFallback/hostFallback.css +12 -0
  80. package/tiles/hostFallback/hostFallback.editor.tsx +10 -0
  81. package/tiles/page/page.component.tsx +7 -0
  82. package/tiles/page/page.script.tsx +5 -0
  83. package/tiles/pageModules/pageModules.component.tsx +740 -0
  84. package/tiles/pageModules/pageModules.css +383 -0
  85. package/tiles/pageModules/pageModules.icon.svg +1 -0
  86. package/tiles/pageModules/servedPage.ts +465 -0
  87. package/tiles/pagesPanel/pagesPanel.component.tsx +1645 -0
  88. package/tiles/pagesPanel/pagesPanel.css +713 -0
  89. package/tiles/pagesPanel/pagesPanel.editor.tsx +23 -0
  90. package/tiles/pagesPanel/pagesSearch.ts +194 -0
  91. package/tiles/pagesPanel/routeTree.ts +98 -0
  92. package/tiles/previewBridge/landing.ts +66 -0
  93. package/tiles/previewBridge/previewAgent.ts +457 -0
  94. package/tiles/previewBridge/previewBridge.component.tsx +137 -0
  95. package/tiles/previewBridge/previewBridge.editor.tsx +10 -0
  96. package/tiles/previewBridge/previewBridgeCommon.ts +172 -0
  97. package/tiles/previewBridge/standIn.ts +22 -0
  98. package/tiles/treeView/treeView.component.tsx +514 -0
  99. package/tiles/treeView/treeView.css +303 -0
  100. package/tiles/treeView/treeView.editor.tsx +12 -0
  101. package/tiles/workbench/assistant.css +401 -0
  102. package/tiles/workbench/assistant.tsx +516 -0
  103. package/tiles/workbench/assistantChat.ts +165 -0
  104. package/tiles/workbench/dictation.ts +354 -0
  105. package/tiles/workbench/projectInfo.ts +72 -0
  106. package/tiles/workbench/recent.ts +67 -0
  107. package/tiles/workbench/workbench.component.tsx +476 -0
  108. package/tiles/workbench/workbench.css +376 -0
  109. package/tiles/workbench/workbenchAddress.ts +202 -0
  110. package/tiles/workbench/workspace.ts +775 -0
  111. package/tiles/workbenchEditor/workbenchEditor.component.tsx +461 -0
  112. package/tiles/workbenchEditor/workbenchEditor.css +370 -0
  113. package/tiles/workbenchHome/workbenchHome.component.tsx +196 -0
  114. package/tiles/workbenchHome/workbenchHome.css +271 -0
  115. package/tsconfig.json +3 -0
@@ -0,0 +1,476 @@
1
+ // The SHELL of the workbench (ADR-0050, by pages since ADR-0051 §4): a sidebar — the project,
2
+ // Home / Pages / Prefabs, the documents recently opened, the account — and the content beside
3
+ // it: whatever page of the workbench the address names (Home, a list, the editor), as the page's
4
+ // own blocks under the shell's outlet. The shell is one block on three pages, under one node id,
5
+ // so a move between them keeps it mounted; its links move in place through the client router.
6
+ //
7
+ // TOOLS (ADR-0062): the blocks in the shell's `tools` slot are not drawn here — the shell hands
8
+ // them to the canvases below it (canvasTools.ts), each of which offers them on its bar.
9
+ import { defineComponentSide, Link, markInternalTile } from '@mosetta/react';
10
+ import { ProvideDiScope, useChildEntries, useChildren, useDi, useDiScope, useIsPreviewMode, useRoute } from '@mosetta/react';
11
+ import { BlockScope, DI_CONTAINER_TOKEN } from '@mosetta/di';
12
+ import { Mosaic } from '@mosetta/ui';
13
+ import type { ContentKind } from '@mosetta/content';
14
+ import { ReactNode, useCallback, useEffect, useRef, useState, useSyncExternalStore } from 'react';
15
+ import { TooltipHost } from '../content/tooltips';
16
+ import '../content/workbenchScale.css';
17
+ import './workbench.css';
18
+ import { basePathOf, parseWorkbenchPath, pathOfView, TWorkbenchView } from './workbenchAddress';
19
+ import { IRecentEntry, readRecent, subscribeRecent } from './recent';
20
+ import { IProjectInfo, useProjectInfo } from './projectInfo';
21
+ import { EDITOR_TOOLS_TOKEN, toolNodesOf } from '../canvas/canvasTools';
22
+ import { AssistantPanel } from './assistant';
23
+
24
+ const BackIcon = (
25
+ <svg viewBox="0 0 16 16"
26
+ aria-hidden="true">
27
+ <path d="M9.5 3.5L5 8l4.5 4.5M5.5 8H13"
28
+ fill="none"
29
+ stroke="currentColor"
30
+ strokeWidth="1.4"
31
+ strokeLinecap="round"
32
+ strokeLinejoin="round" />
33
+ </svg>
34
+ );
35
+
36
+ const DistributionsIcon = (
37
+ <svg viewBox="0 0 16 16"
38
+ aria-hidden="true">
39
+ <path d="M8 2.2l5.3 2.9v5.8L8 13.8l-5.3-2.9V5.1zM2.7 5.1L8 8l5.3-2.9M8 8v5.8"
40
+ fill="none"
41
+ stroke="currentColor"
42
+ strokeWidth="1.3"
43
+ strokeLinejoin="round" />
44
+ </svg>
45
+ );
46
+
47
+ const HomeIcon = (
48
+ <svg viewBox="0 0 16 16"
49
+ aria-hidden="true">
50
+ <path d="M2.5 7.5L8 2.8l5.5 4.7M4 6.8V13h3V9.5h2V13h3V6.8"
51
+ fill="none"
52
+ stroke="currentColor"
53
+ strokeWidth="1.4"
54
+ strokeLinejoin="round" />
55
+ </svg>
56
+ );
57
+
58
+ const PagesIcon = (
59
+ <svg viewBox="0 0 16 16"
60
+ aria-hidden="true">
61
+ <path d="M4 2.5h5.5L12.5 5.5V13.5H4z"
62
+ fill="none"
63
+ stroke="currentColor"
64
+ strokeWidth="1.4"
65
+ strokeLinejoin="round" />
66
+ <path d="M9.5 2.5v3h3M6 8.5h4M6 10.8h4"
67
+ fill="none"
68
+ stroke="currentColor"
69
+ strokeWidth="1.3" />
70
+ </svg>
71
+ );
72
+
73
+ const PrefabsIcon = (
74
+ <svg viewBox="0 0 16 16"
75
+ aria-hidden="true">
76
+ <path d="M8 2.2l5.2 3v5.6L8 13.8l-5.2-3V5.2z"
77
+ fill="none"
78
+ stroke="currentColor"
79
+ strokeWidth="1.4"
80
+ strokeLinejoin="round" />
81
+ <path d="M8 8l5.2-2.8M8 8L2.8 5.2M8 8v5.8"
82
+ fill="none"
83
+ stroke="currentColor"
84
+ strokeWidth="1.2" />
85
+ </svg>
86
+ );
87
+
88
+ const FoldIcon = (
89
+ <svg viewBox="0 0 16 16"
90
+ aria-hidden="true">
91
+ <path d="M9.5 3.5L5 8l4.5 4.5M13 3.5L8.5 8l4.5 4.5"
92
+ fill="none"
93
+ stroke="currentColor"
94
+ strokeWidth="1.5"
95
+ strokeLinecap="round"
96
+ strokeLinejoin="round" />
97
+ </svg>
98
+ );
99
+
100
+ const UnfoldIcon = (
101
+ <svg viewBox="0 0 16 16"
102
+ aria-hidden="true">
103
+ <path d="M6.5 3.5L11 8l-4.5 4.5M3 3.5L7.5 8 3 12.5"
104
+ fill="none"
105
+ stroke="currentColor"
106
+ strokeWidth="1.5"
107
+ strokeLinecap="round"
108
+ strokeLinejoin="round" />
109
+ </svg>
110
+ );
111
+
112
+ const PinIcon = (
113
+ <svg viewBox="0 0 16 16"
114
+ aria-hidden="true">
115
+ <path d="M9.8 2.2l4 4-1.4 1.4-.7-.7-2.6 2.6.3 2.8-1.4 1.4-2.6-2.6L2.6 14 2 13.4l2.9-2.8-2.6-2.6 1.4-1.4 2.8.3 2.6-2.6-.7-.7z"
116
+ fill="none"
117
+ stroke="currentColor"
118
+ strokeWidth="1.3"
119
+ strokeLinejoin="round" />
120
+ </svg>
121
+ );
122
+
123
+ const AssistantIcon = (
124
+ <svg viewBox="0 0 16 16"
125
+ aria-hidden="true">
126
+ <path d="M8 2.2l1.3 3.2 3.2 1.3-3.2 1.3L8 11.2 6.7 8 3.5 6.7l3.2-1.3zM12.5 10.5l.6 1.4 1.4.6-1.4.6-.6 1.4-.6-1.4-1.4-.6 1.4-.6z"
127
+ fill="none"
128
+ stroke="currentColor"
129
+ strokeWidth="1.2"
130
+ strokeLinejoin="round" />
131
+ </svg>
132
+ );
133
+
134
+ /** the sidebar pinned open — a preference of this browser */
135
+ const SIDEBAR_PINNED_KEY = 'mosetta-workbench-sidebar-pinned';
136
+
137
+ /** the assistant's column open — a preference of this browser */
138
+ const ASSISTANT_OPEN_KEY = 'mosetta-workbench-assistant-open';
139
+
140
+ function readFlag(key: string, fallback: boolean): boolean {
141
+ try {
142
+ const raw = window.localStorage.getItem(key);
143
+ return raw === null ? fallback : raw === '1';
144
+ } catch {
145
+ return fallback;
146
+ }
147
+ }
148
+
149
+ function writeFlag(key: string, value: boolean): void {
150
+ try {
151
+ window.localStorage.setItem(key, value ? '1' : '0');
152
+ } catch {
153
+ // a document that denies storage: the preference lives for the page only
154
+ }
155
+ }
156
+
157
+ const projectNameOf = (host: string): string => host.split(':')[0].split('.')[0] || host;
158
+
159
+ /** a line of the sidebar's navigation: a link, moving in place */
160
+ function NavItem({ active, href, icon, children, testId, tooltip }: { active: boolean; href: string; icon: ReactNode; children: ReactNode; testId: string; tooltip?: string }) {
161
+ return (
162
+ <Link className={`workbench__nav-item${active ? ' workbench__nav-item--active' : ''}`}
163
+ href={href}
164
+ data-test-id={testId}
165
+ data-tooltip={tooltip}
166
+ aria-current={active ? 'page' : undefined}>
167
+ <span className="workbench__nav-icon">{icon}</span>
168
+ <span className="workbench__nav-label">{children}</span>
169
+ </Link>
170
+ );
171
+ }
172
+
173
+ interface ISidebarProps {
174
+ view: TWorkbenchView;
175
+ base: string;
176
+ compact: boolean;
177
+ pinned: boolean;
178
+ recent: readonly IRecentEntry[];
179
+ project: IProjectInfo;
180
+ /** the assistant's column: null — the site has no assistant */
181
+ assistant: { readonly open: boolean; readonly onToggle: () => void } | null;
182
+ onCompact: (compact: boolean) => void;
183
+ onPinned: (pinned: boolean) => void;
184
+ }
185
+
186
+ function Sidebar({ view, base, compact, pinned, recent, project, assistant, onCompact, onPinned }: ISidebarProps) {
187
+ const listShown = (kind: ContentKind): boolean => (view.view === 'list' && view.kind === kind)
188
+ || (view.view === 'editor' && view.kind === kind);
189
+ const signOut = project.signedIn && project.authOrigin
190
+ ? `${project.authOrigin}/logout?return=${encodeURIComponent(`${window.location.origin}/`)}`
191
+ : null;
192
+ const name = projectNameOf(project.host);
193
+ const pathTo = (target: 'home' | 'distributions' | ContentKind): string => pathOfView(base, target === 'home' || target === 'distributions'
194
+ ? { view: target }
195
+ : { view: 'list', kind: target }).pathname;
196
+ return (
197
+ <nav className={`workbench__sidebar${compact ? ' workbench__sidebar--compact' : ''}`}
198
+ data-test-id="workbench-sidebar"
199
+ data-compact={compact || undefined}
200
+ data-pinned={pinned || undefined}>
201
+ {/* the way back to the platform: every project of this person, the dashboard they came from */}
202
+ {project.platformOrigin ? (
203
+ <a className="workbench__back"
204
+ href={project.platformOrigin}
205
+ data-test-id="workbench-all-projects"
206
+ data-tooltip={compact ? 'All projects' : undefined}
207
+ aria-label="All projects">
208
+ {BackIcon}
209
+ <span className="workbench__nav-label">All projects</span>
210
+ </a>
211
+ ) : null}
212
+ <div className="workbench__sidebar-head">
213
+ <Link className="workbench__project"
214
+ href={pathTo('home')}
215
+ data-test-id="workbench-project"
216
+ data-tooltip={compact ? `${name} — Home` : undefined}>
217
+ <Mosaic seed={name}
218
+ size={24} />
219
+ <span className="workbench__project-name">{name}</span>
220
+ </Link>
221
+ {compact ? null : (
222
+ <>
223
+ <button type="button"
224
+ className={`workbench__sidebar-button${pinned ? ' workbench__sidebar-button--on' : ''}`}
225
+ data-test-id="workbench-sidebar-pin"
226
+ aria-pressed={pinned}
227
+ aria-label={pinned ? 'Unpin the sidebar' : 'Pin the sidebar open'}
228
+ data-tooltip={pinned ? 'Unpin: the sidebar folds again when the editor opens' : 'Pin the sidebar open — it never folds on its own'}
229
+ onClick={() => onPinned(!pinned)}>
230
+ {PinIcon}
231
+ </button>
232
+ <button type="button"
233
+ className="workbench__sidebar-button"
234
+ data-test-id="workbench-sidebar-fold"
235
+ aria-label="Fold the sidebar"
236
+ data-tooltip="Fold the sidebar to its icons"
237
+ onClick={() => onCompact(true)}>
238
+ {FoldIcon}
239
+ </button>
240
+ </>
241
+ )}
242
+ </div>
243
+ {compact ? (
244
+ <button type="button"
245
+ className="workbench__sidebar-button workbench__sidebar-button--unfold"
246
+ data-test-id="workbench-sidebar-unfold"
247
+ aria-label="Unfold the sidebar"
248
+ data-tooltip="Unfold the sidebar"
249
+ onClick={() => onCompact(false)}>
250
+ {UnfoldIcon}
251
+ </button>
252
+ ) : null}
253
+ <div className="workbench__nav">
254
+ <NavItem active={view.view === 'home'}
255
+ href={pathTo('home')}
256
+ testId="workbench-nav-home"
257
+ icon={HomeIcon}
258
+ tooltip={compact ? 'Home' : undefined}>
259
+ Home
260
+ </NavItem>
261
+ <NavItem active={listShown('pages')}
262
+ href={pathTo('pages')}
263
+ testId="workbench-nav-pages"
264
+ icon={PagesIcon}
265
+ tooltip={compact ? 'Pages' : undefined}>
266
+ Pages
267
+ </NavItem>
268
+ <NavItem active={listShown('prefabs')}
269
+ href={pathTo('prefabs')}
270
+ testId="workbench-nav-prefabs"
271
+ icon={PrefabsIcon}
272
+ tooltip={compact ? 'Prefabs' : undefined}>
273
+ Prefabs
274
+ </NavItem>
275
+ <NavItem active={view.view === 'distributions'}
276
+ href={pathTo('distributions')}
277
+ testId="workbench-nav-distributions"
278
+ icon={DistributionsIcon}
279
+ tooltip={compact ? 'Tile palettes' : undefined}>
280
+ Tile palettes
281
+ </NavItem>
282
+ {assistant ? (
283
+ <button type="button"
284
+ className={`workbench__nav-item${assistant.open ? ' workbench__nav-item--active' : ''}`}
285
+ data-test-id="workbench-nav-assistant"
286
+ data-tooltip={compact ? 'Assistant (⌘J)' : '⌘J'}
287
+ aria-pressed={assistant.open}
288
+ onClick={assistant.onToggle}>
289
+ <span className="workbench__nav-icon">{AssistantIcon}</span>
290
+ <span className="workbench__nav-label">Assistant</span>
291
+ </button>
292
+ ) : null}
293
+ </div>
294
+ {recent.length > 0 && !compact ? (
295
+ <div className="workbench__recent"
296
+ data-test-id="workbench-recent">
297
+ <div className="workbench__recent-title">Recent</div>
298
+ {recent.map((entry) => (
299
+ <Link key={`${entry.kind}:${entry.page}`}
300
+ className="workbench__recent-item"
301
+ href={pathOfView(base, { view: 'editor', kind: entry.kind, page: entry.page }).pathname}
302
+ data-recent-kind={entry.kind}
303
+ data-recent-page={entry.page}
304
+ data-tooltip={entry.kind === 'prefabs' ? `Open the prefab ${entry.title}` : `Open ${entry.title}`}>
305
+ <span className="workbench__nav-icon">{entry.kind === 'prefabs' ? PrefabsIcon : PagesIcon}</span>
306
+ <span className="workbench__recent-label">{entry.title}</span>
307
+ </Link>
308
+ ))}
309
+ </div>
310
+ ) : null}
311
+ <div className="workbench__account"
312
+ data-test-id="workbench-account"
313
+ data-tooltip={compact ? (project.signedIn?.login ?? (project.described ? 'Not signed in' : 'Local workbench')) : undefined}>
314
+ {project.signedIn ? (
315
+ <>
316
+ <Mosaic seed={project.signedIn.login.toLowerCase()}
317
+ size={28}
318
+ className="workbench__account-mosaic" />
319
+ <span className="workbench__account-login">{project.signedIn.login}</span>
320
+ {signOut ? <a className="workbench__account-out"
321
+ href={signOut}>Sign out</a> : null}
322
+ </>
323
+ ) : (
324
+ <>
325
+ <Mosaic seed="mosetta"
326
+ size={28}
327
+ className="workbench__account-mosaic" />
328
+ <span className="workbench__account-login">{project.described ? 'Not signed in' : 'Local workbench'}</span>
329
+ </>
330
+ )}
331
+ </div>
332
+ </nav>
333
+ );
334
+ }
335
+
336
+ // ---- the shell ---------------------------------------------------------------------------------
337
+
338
+ /** the recent list as a store: this window's localStorage, re-read on every write */
339
+ function useRecent(): readonly IRecentEntry[] {
340
+ const [held, setHeld] = useState<readonly IRecentEntry[]>(() => readRecent(window));
341
+ useEffect(() => subscribeRecent(window, () => setHeld(readRecent(window))), []);
342
+ return held;
343
+ }
344
+
345
+ const subscribeToHistory = (listen: () => void): (() => void) => {
346
+ window.addEventListener('popstate', listen);
347
+ return () => window.removeEventListener('popstate', listen);
348
+ };
349
+ const readSearch = (): string => window.location.search;
350
+ const noSearch = (): string => '';
351
+
352
+ /** the shell's DI scope: the tools of its `tools` slot, read afresh by whoever asks */
353
+ function useToolsScope() {
354
+ const di = useDi(DI_CONTAINER_TOKEN);
355
+ const parentScope = useDiScope();
356
+ const tools = toolNodesOf(useChildEntries().map((entry) => entry.node));
357
+ const held = useRef(tools);
358
+ held.current = tools;
359
+ const [scope] = useState(() => {
360
+ const shellScope = new BlockScope(parentScope);
361
+ di.registerScope(shellScope);
362
+ di.registerValueOfToken(EDITOR_TOOLS_TOKEN, { list: () => held.current }, shellScope);
363
+ return shellScope;
364
+ });
365
+ useEffect(() => () => di.disposeScope(scope), [di, scope]);
366
+ return scope;
367
+ }
368
+
369
+ function Shell() {
370
+ const route = useRoute();
371
+ const project = useProjectInfo();
372
+ const children = useChildren({ max: 'many' });
373
+ const toolsScope = useToolsScope();
374
+ // which page of the workbench shows: the address says (the route's path, else the location's)
375
+ const pathname = route?.path ?? window.location.pathname;
376
+ const base = basePathOf(route?.pattern ?? null, pathname);
377
+ const search = useSyncExternalStore(subscribeToHistory, readSearch, noSearch);
378
+ const view = parseWorkbenchPath(pathname, base, new URLSearchParams(route?.search ?? search));
379
+ const editor = view.view === 'editor';
380
+ // the editor wants the room: entering it folds the sidebar to its icons, leaving it unfolds —
381
+ // unless the sidebar is pinned open; a fold or unfold by hand holds until the next crossing
382
+ const [pinned, setPinned] = useState(() => readFlag(SIDEBAR_PINNED_KEY, false));
383
+ const [compact, setCompact] = useState(() => editor && !pinned);
384
+ const [wasEditor, setWasEditor] = useState(editor);
385
+ if (wasEditor !== editor) {
386
+ setWasEditor(editor);
387
+ if (!pinned) setCompact(editor);
388
+ }
389
+ const recent = useRecent();
390
+ const onPinned = useCallback((next: boolean): void => {
391
+ setPinned(next);
392
+ writeFlag(SIDEBAR_PINNED_KEY, next);
393
+ if (next) setCompact(false);
394
+ }, []);
395
+ // the assistant: offered where the platform has one, open as this browser left it
396
+ const [assistantOpen, setAssistantOpen] = useState(() => readFlag(ASSISTANT_OPEN_KEY, true));
397
+ const [assistantWorking, setAssistantWorking] = useState(false);
398
+ const toggleAssistant = useCallback((): void => {
399
+ setAssistantOpen((open) => {
400
+ writeFlag(ASSISTANT_OPEN_KEY, !open);
401
+ return !open;
402
+ });
403
+ }, []);
404
+ const closeAssistant = useCallback((): void => {
405
+ setAssistantOpen(false);
406
+ writeFlag(ASSISTANT_OPEN_KEY, false);
407
+ }, []);
408
+ // ⌘J / Ctrl+J opens and closes it wherever the focus is
409
+ const hasAssistant = project.assistant;
410
+ useEffect(() => {
411
+ if (!hasAssistant) return undefined;
412
+ const onKey = (e: KeyboardEvent): void => {
413
+ if ((e.metaKey || e.ctrlKey) && !e.altKey && !e.shiftKey && e.key.toLowerCase() === 'j') {
414
+ e.preventDefault();
415
+ toggleAssistant();
416
+ }
417
+ };
418
+ window.addEventListener('keydown', onKey);
419
+ return () => window.removeEventListener('keydown', onKey);
420
+ }, [hasAssistant, toggleAssistant]);
421
+ return (
422
+ <TooltipHost>
423
+ <div className="workbench"
424
+ data-test-id="workbench"
425
+ data-view={view.view}
426
+ data-kind={view.view === 'list' || view.view === 'editor' ? view.kind : undefined}
427
+ data-assistant={project.assistant && assistantOpen ? (editor ? 'tree' : 'bottom') : undefined}>
428
+ <Sidebar view={view}
429
+ base={base}
430
+ compact={compact}
431
+ pinned={pinned}
432
+ recent={recent}
433
+ project={project}
434
+ assistant={project.assistant ? { open: assistantOpen, onToggle: toggleAssistant } : null}
435
+ onCompact={setCompact}
436
+ onPinned={onPinned} />
437
+ <ProvideDiScope scope={toolsScope}>
438
+ <main className="workbench__main"
439
+ data-test-id="workbench-main">
440
+ {children}
441
+ {/* the assistant at work: what it changes shows through, nothing can be changed under it */}
442
+ {assistantWorking ? (
443
+ <div className="workbench__frozen"
444
+ data-test-id="workbench-frozen">
445
+ <span className="workbench__frozen-note">The assistant is working…</span>
446
+ </div>
447
+ ) : null}
448
+ {/* an island over the page: over the tree in the editor, at the bottom elsewhere */}
449
+ {project.assistant && assistantOpen ? <AssistantPanel placement={editor ? 'tree' : 'bottom'}
450
+ transcription={project.transcription}
451
+ onClose={closeAssistant}
452
+ onWorking={setAssistantWorking} /> : null}
453
+ </main>
454
+ </ProvideDiScope>
455
+ </div>
456
+ </TooltipHost>
457
+ );
458
+ }
459
+
460
+ function WorkbenchBlock() {
461
+ // a workbench inside a preview must not open canvases of its own (an iframe in an iframe,
462
+ // forever, when the workbench page previews itself)
463
+ if (useIsPreviewMode()) {
464
+ return <div className="workbench workbench--bare"
465
+ data-workbench-preview-stub>A workbench inside a preview does not expand</div>;
466
+ }
467
+ if (typeof window === 'undefined') return null; // nothing to show a server: the shell is the browser's
468
+ return <Shell />;
469
+ }
470
+
471
+ export default markInternalTile(defineComponentSide({
472
+ Component: WorkbenchBlock,
473
+ defaultSettings() {
474
+ return {};
475
+ },
476
+ }));