@mosetta/editor 0.0.0-stage → 0.0.28

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 +185 -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,124 @@
1
+ /* the updates window: a fixed overlay over the canvas, the publish dialog's sibling */
2
+ .updates-modal__overlay {
3
+ position: fixed;
4
+ inset: 0;
5
+ z-index: 60;
6
+ display: flex;
7
+ align-items: flex-start;
8
+ justify-content: center;
9
+ padding-top: 10vh;
10
+ background: rgb(24 24 27 / 35%);
11
+ }
12
+
13
+ .updates-modal {
14
+ display: flex;
15
+ flex-direction: column;
16
+ gap: 10px;
17
+ width: min(520px, calc(100vw - 32px));
18
+ max-height: 76vh;
19
+ overflow-y: auto;
20
+ padding: 14px 16px 16px;
21
+ border-radius: 8px;
22
+ font: var(--mosetta-wb-font, 14px)/1.45 ui-sans-serif, system-ui, sans-serif;
23
+ color: #27272a;
24
+ background: #fff;
25
+ box-shadow: 0 12px 32px rgb(0 0 0 / 25%);
26
+ }
27
+
28
+ .updates-modal__title {
29
+ font-size: calc(var(--mosetta-wb-font, 14px) + 2px);
30
+ font-weight: 600;
31
+ color: #18181b;
32
+ }
33
+
34
+ .updates-modal__text {
35
+ color: #52525b;
36
+ font-size: var(--mosetta-wb-font-sm, 13px);
37
+ }
38
+
39
+ .updates-modal__group {
40
+ display: flex;
41
+ flex-direction: column;
42
+ gap: 6px;
43
+ padding: 10px 12px;
44
+ border: 1px solid #e4e4e7;
45
+ border-radius: 6px;
46
+ }
47
+
48
+ .updates-modal__group-title {
49
+ font-weight: 600;
50
+ }
51
+
52
+ .updates-modal__changes {
53
+ color: #52525b;
54
+ font-size: var(--mosetta-wb-font-sm, 13px);
55
+ }
56
+
57
+ .updates-modal__changes summary {
58
+ cursor: pointer;
59
+ }
60
+
61
+ .updates-modal__version {
62
+ margin-top: 6px;
63
+ }
64
+
65
+ .updates-modal__version-name {
66
+ font-weight: 600;
67
+ }
68
+
69
+ .updates-modal__version pre {
70
+ margin: 2px 0 0;
71
+ font: inherit;
72
+ white-space: pre-wrap;
73
+ }
74
+
75
+ .updates-modal__blocks {
76
+ display: flex;
77
+ flex-wrap: wrap;
78
+ gap: 4px 14px;
79
+ }
80
+
81
+ .updates-modal__block {
82
+ display: inline-flex;
83
+ align-items: center;
84
+ gap: 6px;
85
+ cursor: pointer;
86
+ }
87
+
88
+ .updates-modal__actions {
89
+ display: flex;
90
+ justify-content: flex-end;
91
+ gap: 8px;
92
+ margin-top: 4px;
93
+ }
94
+
95
+ .updates-modal__button {
96
+ height: calc(var(--mosetta-wb-icon-box, 24px) + 4px);
97
+ padding: 0 12px;
98
+ border: 1px solid #d4d4d8;
99
+ border-radius: 6px;
100
+ font: inherit;
101
+ color: #27272a;
102
+ background: #fff;
103
+ cursor: pointer;
104
+ }
105
+
106
+ .updates-modal__button:hover {
107
+ background: #f4f4f5;
108
+ }
109
+
110
+ .updates-modal__button--main {
111
+ border-color: #2563eb;
112
+ color: #fff;
113
+ background: #2563eb;
114
+ }
115
+
116
+ .updates-modal__button--main:hover {
117
+ border-color: #1d4ed8;
118
+ background: #1d4ed8;
119
+ }
120
+
121
+ .updates-modal__button:disabled {
122
+ opacity: 0.6;
123
+ cursor: default;
124
+ }
@@ -0,0 +1,110 @@
1
+ // "Updates on this page" (ADR-0061): the blocks of the page that have newer versions, by
2
+ // distribution — from which versions to which, what each version on the way says it changed, a
3
+ // checkbox for every block of it the page holds. "Update selected" moves them in ONE undoable step
4
+ // of the draft; the preview and the publication are the author's check, as for any edit.
5
+ // Portalled like the publish dialog, inside the hosting module's scope class.
6
+ import { useState } from 'react';
7
+ import { createPortal } from 'react-dom';
8
+ import { tileScopeClass, useComponent } from '@mosetta/react';
9
+ import type { IUpgradeGroup } from './blockUpgrades';
10
+ import './updatesModal.css';
11
+
12
+ /** a block of a distribution, as the window selects it */
13
+ export const upgradeKey = (namespace: string, name: string): string => `${namespace}/${name}`;
14
+
15
+ export function UpdatesModal({ host, groups, busy, onCancel, onConfirm }: {
16
+ /** the document the dialog goes into — the host module's own */
17
+ host: Document;
18
+ groups: readonly IUpgradeGroup[];
19
+ busy: boolean;
20
+ onCancel: () => void;
21
+ /** the blocks to update, by upgradeKey */
22
+ onConfirm: (selected: ReadonlySet<string>) => void;
23
+ }) {
24
+ const self = useComponent().reference;
25
+ const [selected, setSelected] = useState<ReadonlySet<string>>(() => new Set(groups.flatMap((g) => g.blocks.map((b) => upgradeKey(g.namespace, b.name)))));
26
+ const count = groups.reduce((sum, g) => sum + g.blocks.filter((b) => selected.has(upgradeKey(g.namespace, b.name))).reduce((n, b) => n + b.count, 0), 0);
27
+ const toggle = (key: string) => {
28
+ const next = new Set(selected);
29
+ if (next.has(key)) next.delete(key);
30
+ else next.add(key);
31
+ setSelected(next);
32
+ };
33
+ return createPortal(
34
+ <div className={tileScopeClass(self)}
35
+ style={{ display: 'contents' }}>
36
+ <div className="updates-modal__overlay"
37
+ data-test-id="updates-modal"
38
+ onClick={() => {
39
+ if (!busy) onCancel();
40
+ }}
41
+ onKeyDown={(e) => {
42
+ if (e.key === 'Escape' && !busy) onCancel();
43
+ }}>
44
+ <div className="updates-modal"
45
+ role="dialog"
46
+ aria-label="Updates on this page"
47
+ onClick={(e) => e.stopPropagation()}>
48
+ <div className="updates-modal__title">Updates on this page</div>
49
+ <div className="updates-modal__text">
50
+ The page keeps each block at the version it was placed with. Updated here, in the draft —
51
+ the preview shows the result, publishing makes it the page.
52
+ </div>
53
+ {groups.map((group) => (
54
+ <section key={group.namespace}
55
+ className="updates-modal__group"
56
+ data-test-id="updates-group"
57
+ data-namespace={group.namespace}>
58
+ <div className="updates-modal__group-title">
59
+ {`${group.title} ${group.fromVersions.join(', ')} → ${group.toVersion}`}
60
+ </div>
61
+ {group.changes.length ? (
62
+ <details className="updates-modal__changes">
63
+ <summary>What changed</summary>
64
+ {group.changes.map((entry) => (
65
+ <div key={entry.version}
66
+ className="updates-modal__version">
67
+ <div className="updates-modal__version-name">{entry.version}</div>
68
+ <pre>{entry.changes}</pre>
69
+ </div>
70
+ ))}
71
+ </details>
72
+ ) : null}
73
+ <div className="updates-modal__blocks">
74
+ {group.blocks.map((block) => {
75
+ const key = upgradeKey(group.namespace, block.name);
76
+ return (
77
+ <label key={key}
78
+ className="updates-modal__block">
79
+ <input type="checkbox"
80
+ data-test-id="updates-block"
81
+ data-block={key}
82
+ checked={selected.has(key)}
83
+ disabled={busy}
84
+ onChange={() => toggle(key)} />
85
+ {block.count > 1 ? `${block.name} ×${block.count}` : block.name}
86
+ </label>
87
+ );
88
+ })}
89
+ </div>
90
+ </section>
91
+ ))}
92
+ <div className="updates-modal__actions">
93
+ <button type="button"
94
+ className="updates-modal__button"
95
+ disabled={busy}
96
+ onClick={onCancel}>Cancel</button>
97
+ <button type="button"
98
+ className="updates-modal__button updates-modal__button--main"
99
+ data-test-id="updates-confirm"
100
+ disabled={busy || count === 0}
101
+ onClick={() => onConfirm(selected)}>
102
+ {busy ? 'Updating…' : `Update selected (${count})`}
103
+ </button>
104
+ </div>
105
+ </div>
106
+ </div>
107
+ </div>,
108
+ host.body,
109
+ );
110
+ }
@@ -0,0 +1,24 @@
1
+ // Whether Ctrl (or its Mac kin, Cmd) is held RIGHT NOW — the switch between the ordinary blue
2
+ // selection and the yellow multi-selection, shared by every widget that colours by it. Window
3
+ // blur clears the state: the key may well be released over another window, and a modifier that
4
+ // sticks is worse than one that drops.
5
+ import { useEffect, useState } from 'react';
6
+
7
+ export function useCtrlHeld(): boolean {
8
+ const [held, setHeld] = useState(false);
9
+
10
+ useEffect(() => {
11
+ const sync = (event: KeyboardEvent): void => setHeld(event.ctrlKey || event.metaKey);
12
+ const drop = (): void => setHeld(false);
13
+ window.addEventListener('keydown', sync);
14
+ window.addEventListener('keyup', sync);
15
+ window.addEventListener('blur', drop);
16
+ return () => {
17
+ window.removeEventListener('keydown', sync);
18
+ window.removeEventListener('keyup', sync);
19
+ window.removeEventListener('blur', drop);
20
+ };
21
+ }, []);
22
+
23
+ return held;
24
+ }
@@ -0,0 +1,115 @@
1
+ // Shared by the workbench's smart widgets (bundled per module — the SESSION instance is shared via
2
+ // DI, and the EditingSession class itself lives in the shared @mosetta/content dependency artifact).
3
+ //
4
+ // Touching the session also brings its AUTOSAVE, CONFLICT interceptor, HOTKEYS and CLIPBOARD into
5
+ // being. All are properties of the workbench rather than of any panel: whichever widget renders
6
+ // first creates them, the rest just poke at them, and closing a panel takes nothing with it. (The
7
+ // open tabs and the address are the WORKSPACE's — the shell creates it before any canvas.)
8
+ import { useDi, useDiOptional } from '@mosetta/react';
9
+ import { useCallback, useEffect, useSyncExternalStore } from 'react';
10
+ import { useComponent } from '@mosetta/react';
11
+ import { DI_CONTAINER_TOKEN } from '@mosetta/di';
12
+ import { DOCK_TAB_TOKEN } from '@mosetta/ui';
13
+ import { EditingSession, editingSessionHubOf, ensureEditingSession, IEditingState } from '@mosetta/content';
14
+ import { ensureEditingAutosave } from './editingAutosave';
15
+ import { ensureEditingHotkeys } from './editingHotkeys';
16
+ import { ensureEditingClipboard } from './editingClipboard';
17
+ import { ensureEditingConflicts } from './editingConflicts';
18
+
19
+ export function useEditingSession(): { session: EditingSession; state: IEditingState } {
20
+ const di = useDi(DI_CONTAINER_TOKEN);
21
+ const session = ensureEditingSession(di);
22
+ // the caller's own name carries the namespace and version the content server module lives under —
23
+ // the one thing about a server action a container cannot work out for itself
24
+ const reference = useComponent().reference;
25
+ ensureEditingAutosave(di, session, reference);
26
+ ensureEditingConflicts(di, session, reference);
27
+ ensureEditingHotkeys(di, session);
28
+ ensureEditingClipboard(di, session);
29
+ const state = useSyncExternalStore(session.subscribe, session.snapshot, session.snapshot);
30
+ return { session, state };
31
+ }
32
+
33
+ /**
34
+ * The view of editing anchored to the last SELECTION rather than to the active canvas: what the
35
+ * settings panel (and the palette — an insert lands at the picked place) holds. Hover ACTIVATES
36
+ * sessions, so the active facade changes under a pointer merely passing over another canvas —
37
+ * this one switches only when a selection actually changes somewhere. Outside a hub-aware realm
38
+ * it falls back to the plain shared session, like everything else.
39
+ */
40
+ export function useSelectionEditingSession(): { session: EditingSession; state: IEditingState } {
41
+ const di = useDi(DI_CONTAINER_TOKEN);
42
+ const globalSession = ensureEditingSession(di);
43
+ const reference = useComponent().reference;
44
+ ensureEditingAutosave(di, globalSession, reference);
45
+ ensureEditingConflicts(di, globalSession, reference);
46
+ ensureEditingHotkeys(di, globalSession);
47
+ ensureEditingClipboard(di, globalSession);
48
+ const hub = editingSessionHubOf(di);
49
+ const session = hub ? hub.selection : globalSession;
50
+ const state = useSyncExternalStore(session.subscribe, session.snapshot, session.snapshot);
51
+ return { session, state };
52
+ }
53
+
54
+ /**
55
+ * The CANVAS's view of editing: its OWN session, scoped to its tab (hub key = tab id) — every
56
+ * other consumer holds the hub's `active` facade and follows whichever canvas the user last
57
+ * touched. Binding ADOPTS the default session (work opened before any canvas existed rides
58
+ * along); mount (of the tab in front), tab activation and `markInteractive` (a pointer inside
59
+ * the canvas) make this canvas the active one. Outside a hub-aware realm (preview environments, no dock) everything
60
+ * falls back to the plain shared session.
61
+ */
62
+ export function useCanvasEditingSession(): {
63
+ session: EditingSession;
64
+ state: IEditingState;
65
+ markInteractive: () => void;
66
+ markHovered: () => void;
67
+ markLeft: () => void;
68
+ } {
69
+ const di = useDi(DI_CONTAINER_TOKEN);
70
+ const reference = useComponent().reference;
71
+ const globalSession = ensureEditingSession(di);
72
+ // the services stay bound to the ACTIVE facade: hotkeys and clipboard belong to focus, and the
73
+ // autosave/conflict pair capture their settle target per request (see editingAutosave)
74
+ ensureEditingAutosave(di, globalSession, reference);
75
+ ensureEditingConflicts(di, globalSession, reference);
76
+ ensureEditingHotkeys(di, globalSession);
77
+ ensureEditingClipboard(di, globalSession);
78
+ const hub = editingSessionHubOf(di);
79
+ const tab = useDiOptional(DOCK_TAB_TOKEN);
80
+ const session = hub && tab ? hub.sessionFor(tab.id) : globalSession;
81
+ useEffect(() => {
82
+ if (!hub || !tab) return;
83
+ // mounting activates — unless the host says the tab is behind another: a shell keeps every
84
+ // tab's canvas mounted, and the last one to mount (modules load in their own time) must
85
+ // not take the tree from the one in front
86
+ if (tab.front?.() ?? true) hub.activate(tab.id);
87
+ return tab.onDidActivate(() => hub.activate(tab.id));
88
+ }, [hub, tab]);
89
+ // ONE blue block per workspace: this canvas watches the selection anchor, and the moment the
90
+ // anchor lives elsewhere (a pick happened in another canvas) it SURRENDERS its own blue
91
+ // selection — quietly (retireSelection carries no act stamp), or the anchor would be stolen
92
+ // right back by the surrender itself
93
+ useEffect(() => {
94
+ if (!hub || !tab) return;
95
+ const surrender = (): void => {
96
+ if (hub.anchorKey() !== tab.id) hub.peek(tab.id)?.retireSelection();
97
+ };
98
+ surrender();
99
+ return hub.selection.subscribe(surrender);
100
+ }, [hub, tab]);
101
+ const markInteractive = useCallback(() => {
102
+ if (hub && tab) hub.activate(tab.id);
103
+ }, [hub, tab]);
104
+ // the hover pair: pointing at this canvas BORROWS the focus (tree follows while it lasts),
105
+ // leaving returns a still-borrowed focus to the selection anchor — at rest the tree shows
106
+ // the canvas of the selected node, not the last one the pointer happened to cross
107
+ const markHovered = useCallback(() => {
108
+ if (hub && tab) hub.borrow(tab.id);
109
+ }, [hub, tab]);
110
+ const markLeft = useCallback(() => {
111
+ if (hub && tab) hub.release(tab.id);
112
+ }, [hub, tab]);
113
+ const state = useSyncExternalStore(session.subscribe, session.snapshot, session.snapshot);
114
+ return { session, state, markInteractive, markHovered, markLeft };
115
+ }
@@ -0,0 +1,47 @@
1
+ // The react ADAPTER over the mounter (treeMounter.ts): the mechanism of keeping one hosted
2
+ // application lives in DI, and all this hook adds is react's own bookkeeping — a ref for the
3
+ // element, a failed flag, effects for the panel's lifetime. Shared by the two panels that host:
4
+ // the palette's preview bubble and the settings panel's form.
5
+ import { useEffect, useRef, useState } from 'react';
6
+ import { useDi } from '@mosetta/react';
7
+ import { DI_CONTAINER_TOKEN } from '@mosetta/di';
8
+ import type { DiContainer } from '@mosetta/di';
9
+ import { TILES_HOLDER_TOKEN } from '@mosetta/runtime/client';
10
+ import { ensureTreeMounter } from './treeMounter';
11
+ import { MountedTree } from './mountedTree';
12
+ import type { IMountSpec } from './mountedTree';
13
+
14
+ /**
15
+ * Keeps ONE application in the element the returned ref points at. A new tree is handed over
16
+ * (IMountHandle.render); only a different ROOT — a provider on another binding — is worth
17
+ * building a new application for. A null spec leaves the application as it is.
18
+ *
19
+ * The application's PARENT is the workbench's own container — or `parentDi`, the first one given:
20
+ * a host that means the mounted form to see something else (the block page's playground, whose
21
+ * settings are its own) hands it its own door.
22
+ */
23
+ export function useMountedTree(spec: IMountSpec | null, parentDi?: DiContainer): {
24
+ ref: React.MutableRefObject<HTMLDivElement | null>;
25
+ failed: boolean;
26
+ } {
27
+ const di = useDi(DI_CONTAINER_TOKEN);
28
+ const holder = useDi(TILES_HOLDER_TOKEN);
29
+ const [mounted] = useState(() => (parentDi ? new MountedTree(holder, parentDi) : ensureTreeMounter(di).create()));
30
+ const ref = useRef<HTMLDivElement | null>(null);
31
+ const [failed, setFailed] = useState(false);
32
+
33
+ useEffect(() => {
34
+ if (!spec) return undefined;
35
+ void mounted.show(spec, () => ref.current).then(
36
+ (showing) => {
37
+ if (showing) setFailed(false);
38
+ },
39
+ () => setFailed(true),
40
+ );
41
+ return () => mounted.cancel();
42
+ }, [mounted, spec]);
43
+
44
+ useEffect(() => () => mounted.dispose(), [mounted]);
45
+
46
+ return { ref, failed };
47
+ }
@@ -0,0 +1,27 @@
1
+ /* The workbench's TYPE AND ICON SCALE — the one place that decides how big the editor reads.
2
+ Every editor panel (pages, tree, palette, canvas chrome, settings) sizes its text and icons
3
+ from these variables, so tuning the two knobs below resizes the whole workbench at once.
4
+
5
+ Declared on every element of the panel (`*`), not on :root: a module's css is scoped under
6
+ the module's own class, so a panel can only style itself — each panel that imports this file
7
+ declares the same scale for its own subtree. The dock's tab row is not an editor panel: it has
8
+ its own port (--mosetta-dock-font-size, see the dock's stylesheet). */
9
+ * {
10
+ /* ---- the knobs ---- */
11
+ /* body text: rows, the tree, the palette, the canvas toolbar */
12
+ --mosetta-wb-font: 14px;
13
+ /* an icon glyph: row actions, toolbar buttons */
14
+ --mosetta-wb-icon: 16px;
15
+
16
+ /* ---- derived: tune the knobs, not these ---- */
17
+ /* secondary text: search fields, paths, panel headers */
18
+ --mosetta-wb-font-sm: calc(var(--mosetta-wb-font) - 1px);
19
+ /* chips and tags */
20
+ --mosetta-wb-font-xs: calc(var(--mosetta-wb-font) - 2px);
21
+ /* notes beside a label, the smallest marks */
22
+ --mosetta-wb-font-2xs: calc(var(--mosetta-wb-font) - 3px);
23
+ /* a compact glyph: chevrons, marks inside a row */
24
+ --mosetta-wb-icon-sm: calc(var(--mosetta-wb-icon) - 4px);
25
+ /* the square an icon button takes */
26
+ --mosetta-wb-icon-box: calc(var(--mosetta-wb-icon) + 8px);
27
+ }
@@ -0,0 +1,32 @@
1
+ // What the editor shows in place of anything it HOSTED that failed to render — a block's preview
2
+ // in the palette, a settings form in the settings panel.
3
+ //
4
+ // A fallback FACET rather than a message hard-coded into a panel: what a host mounts renders
5
+ // through the ordinary tree renderer, so a module that throws is caught by the ordinary error
6
+ // boundary — which renders the page's fallback. Every synthetic page the editor mounts names this
7
+ // one, and what cannot render says so in the editor's words instead of the exception's.
8
+ import { defineFallbackSide, markInternalTile } from '@mosetta/react';
9
+ import { useError } from '@mosetta/react';
10
+ import '../content/workbenchScale.css';
11
+ import './hostFallback.css';
12
+ import { TooltipHost } from '../content/tooltips';
13
+
14
+ export default markInternalTile(defineFallbackSide({
15
+ Component() {
16
+ const error = useError();
17
+ return (
18
+ // the message is for a content manager; the exception itself is one hover away, for whoever
19
+ // wrote the block
20
+ <TooltipHost>
21
+ <div className="host-fallback"
22
+ data-host-failed
23
+ data-tooltip={error?.message ?? ''}>
24
+ Could not render this
25
+ </div>
26
+ </TooltipHost>
27
+ );
28
+ },
29
+ defaultSettings() {
30
+ return {};
31
+ },
32
+ }));
@@ -0,0 +1,12 @@
1
+ /* Fills whatever box the host gave it, so a failure looks like a deliberate state rather than a
2
+ stray line of text in a corner. */
3
+ .host-fallback {
4
+ display: flex;
5
+ height: 100%;
6
+ align-items: center;
7
+ justify-content: center;
8
+ padding: 16px;
9
+ color: #a1a1aa;
10
+ font: var(--mosetta-wb-font)/1.4 ui-sans-serif, system-ui, sans-serif;
11
+ text-align: center;
12
+ }
@@ -0,0 +1,10 @@
1
+ import { defineEditorSide } from '@mosetta/react';
2
+
3
+ export default defineEditorSide({
4
+ // standalone — with no error to report — it renders its message box as-is: an honest specimen
5
+ // of what a failed block will look like
6
+ preview: 'self',
7
+ Component() {
8
+ return <div data-no-settings>This block has no settings.</div>;
9
+ },
10
+ });
@@ -0,0 +1,7 @@
1
+ import { defineRootComponentSide } from '@mosetta/react';
2
+ import { defaultReactRoot } from '@mosetta/react/start';
3
+
4
+ // The editor's ROOT component module: the framework's default root covers it fully — the panels
5
+ // get their server-action slice lazily from useServerAction, nothing editor-specific remains at
6
+ // the root. It doubles as the root the palette mounts previews with (ADR-0034 §5).
7
+ export default defineRootComponentSide(defaultReactRoot);
@@ -0,0 +1,5 @@
1
+ import { startAndMount } from '@mosetta/react/start';
2
+ // The editor root's script facet: the browser entry of an editor page — start() +
3
+ // unconditional hydrate of this module's Component.
4
+
5
+ void startAndMount();