@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.
- package/CHANGELOG.md +185 -0
- package/LICENSE +21 -0
- package/README.md +10 -2
- package/mosetta.json +6 -0
- package/package.json +35 -3
- package/prefabs/workbench/prefab.json +11 -0
- package/prefabs/workbench/versions/1.json +120 -0
- package/prefabs/workbench/versions/2.json +13 -0
- package/prefabs/workbench/versions/3.json +19 -0
- package/prefabs/workbench/versions/4.json +29 -0
- package/prefabs/workbench/versions/5.json +29 -0
- package/tiles/canvas/canvas.component.tsx +2038 -0
- package/tiles/canvas/canvas.css +973 -0
- package/tiles/canvas/canvasTools.ts +66 -0
- package/tiles/canvas/chromeNodes.ts +35 -0
- package/tiles/canvas/ghosts.ts +33 -0
- package/tiles/canvas/insertPopover.tsx +56 -0
- package/tiles/canvas/popoverPlacement.ts +104 -0
- package/tiles/canvas/popoverSize.ts +87 -0
- package/tiles/canvas/providerChrome.tsx +191 -0
- package/tiles/canvas/scrollWatch.ts +43 -0
- package/tiles/canvas/settingsPopover.tsx +257 -0
- package/tiles/canvas/settingsTabs.ts +78 -0
- package/tiles/canvas/toolPanel.tsx +86 -0
- package/tiles/content/autoBind.ts +61 -0
- package/tiles/content/blockPage.css +390 -0
- package/tiles/content/blockPage.tsx +379 -0
- package/tiles/content/blockPicker.css +215 -0
- package/tiles/content/blockPicker.tsx +501 -0
- package/tiles/content/blockUpgrades.ts +145 -0
- package/tiles/content/blockVariants.ts +185 -0
- package/tiles/content/blockVocabulary.ts +77 -0
- package/tiles/content/catalogVersions.ts +106 -0
- package/tiles/content/componentPreview.css +82 -0
- package/tiles/content/componentPreview.tsx +249 -0
- package/tiles/content/content.server.ts +593 -0
- package/tiles/content/contentCommon.ts +133 -0
- package/tiles/content/disconnected.ts +56 -0
- package/tiles/content/distributions.css +256 -0
- package/tiles/content/distributions.tsx +271 -0
- package/tiles/content/editingAutosave.ts +125 -0
- package/tiles/content/editingClipboard.ts +185 -0
- package/tiles/content/editingConflicts.ts +223 -0
- package/tiles/content/editingHotkeys.ts +59 -0
- package/tiles/content/editingNotices.ts +105 -0
- package/tiles/content/formRoot.ts +18 -0
- package/tiles/content/fuzzySearch.ts +121 -0
- package/tiles/content/mediaUpload.ts +55 -0
- package/tiles/content/moduleSiblings.ts +9 -0
- package/tiles/content/mountedTree.ts +136 -0
- package/tiles/content/nodeSettings.css +483 -0
- package/tiles/content/nodeSettings.tsx +963 -0
- package/tiles/content/pasteText.ts +131 -0
- package/tiles/content/prefabFields.tsx +158 -0
- package/tiles/content/prefabParts.ts +105 -0
- package/tiles/content/previewEnvironment.ts +46 -0
- package/tiles/content/projectConfig.ts +46 -0
- package/tiles/content/projectPrefabs.ts +57 -0
- package/tiles/content/publishModal.css +109 -0
- package/tiles/content/publishModal.tsx +92 -0
- package/tiles/content/refsEditing.ts +412 -0
- package/tiles/content/scaledStage.ts +63 -0
- package/tiles/content/searchField.css +32 -0
- package/tiles/content/searchField.tsx +32 -0
- package/tiles/content/settingsDraft.ts +178 -0
- package/tiles/content/settingsIssues.tsx +124 -0
- package/tiles/content/themeScope.ts +72 -0
- package/tiles/content/themeVariables.ts +47 -0
- package/tiles/content/tooltipPlacement.ts +17 -0
- package/tiles/content/tooltips.tsx +131 -0
- package/tiles/content/treeMounter.ts +33 -0
- package/tiles/content/updatesModal.css +124 -0
- package/tiles/content/updatesModal.tsx +110 -0
- package/tiles/content/useCtrlHeld.ts +24 -0
- package/tiles/content/useEditingSession.ts +115 -0
- package/tiles/content/useMountedTree.ts +47 -0
- package/tiles/content/workbenchScale.css +27 -0
- package/tiles/hostFallback/hostFallback.component.tsx +32 -0
- package/tiles/hostFallback/hostFallback.css +12 -0
- package/tiles/hostFallback/hostFallback.editor.tsx +10 -0
- package/tiles/page/page.component.tsx +7 -0
- package/tiles/page/page.script.tsx +5 -0
- package/tiles/pageModules/pageModules.component.tsx +740 -0
- package/tiles/pageModules/pageModules.css +383 -0
- package/tiles/pageModules/pageModules.icon.svg +1 -0
- package/tiles/pageModules/servedPage.ts +465 -0
- package/tiles/pagesPanel/pagesPanel.component.tsx +1645 -0
- package/tiles/pagesPanel/pagesPanel.css +713 -0
- package/tiles/pagesPanel/pagesPanel.editor.tsx +23 -0
- package/tiles/pagesPanel/pagesSearch.ts +194 -0
- package/tiles/pagesPanel/routeTree.ts +98 -0
- package/tiles/previewBridge/landing.ts +66 -0
- package/tiles/previewBridge/previewAgent.ts +457 -0
- package/tiles/previewBridge/previewBridge.component.tsx +137 -0
- package/tiles/previewBridge/previewBridge.editor.tsx +10 -0
- package/tiles/previewBridge/previewBridgeCommon.ts +172 -0
- package/tiles/previewBridge/standIn.ts +22 -0
- package/tiles/treeView/treeView.component.tsx +514 -0
- package/tiles/treeView/treeView.css +303 -0
- package/tiles/treeView/treeView.editor.tsx +12 -0
- package/tiles/workbench/assistant.css +401 -0
- package/tiles/workbench/assistant.tsx +516 -0
- package/tiles/workbench/assistantChat.ts +165 -0
- package/tiles/workbench/dictation.ts +354 -0
- package/tiles/workbench/projectInfo.ts +72 -0
- package/tiles/workbench/recent.ts +67 -0
- package/tiles/workbench/workbench.component.tsx +476 -0
- package/tiles/workbench/workbench.css +376 -0
- package/tiles/workbench/workbenchAddress.ts +202 -0
- package/tiles/workbench/workspace.ts +775 -0
- package/tiles/workbenchEditor/workbenchEditor.component.tsx +461 -0
- package/tiles/workbenchEditor/workbenchEditor.css +370 -0
- package/tiles/workbenchHome/workbenchHome.component.tsx +196 -0
- package/tiles/workbenchHome/workbenchHome.css +271 -0
- 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);
|