@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,185 @@
1
+ // A block's VARIANTS as the block's page shows them (ADR-0060), and the PLAYGROUND's settings —
2
+ // plain logic, no react: what the page lists, and the store its form writes into.
3
+ //
4
+ // The list always starts with "Default" — the block as it is placed: its default settings, or, when
5
+ // its `preview` is a tree holding the block, the settings and contents shown there. The variants its
6
+ // editor facet declares follow, each over the defaults; one that names no contents holds Default's.
7
+ // Where the preview shows the block INSIDE something (an item in a list, a theme switch in a theme),
8
+ // every variant is shown in that very frame.
9
+ //
10
+ // The store is the contract a block's form already speaks (SETTINGS_DRAFT_TOKEN: a snapshot, a
11
+ // subscription, a shallow update) — over nothing but itself. No references, no uploads, no
12
+ // document: what is turned in the playground stays there.
13
+ import type { IBlueprintNode } from '@mosetta/core';
14
+ import type { IBlockVariant, ISettingsDraftContract, TPreviewSpec } from '@mosetta/react';
15
+ import { namespaceOfReference } from '@mosetta/content/units';
16
+ import { versionOfReference, withVersion } from '@mosetta/content';
17
+
18
+ export interface ISpecimen {
19
+ readonly name: string;
20
+ readonly description?: string;
21
+ /** the block's whole settings: its defaults with the variant's over them */
22
+ readonly settings: Record<string, unknown>;
23
+ /** what the block holds */
24
+ readonly children: readonly IBlueprintNode[];
25
+ }
26
+
27
+ /** what the page needs of a block's editor facet */
28
+ export interface IVariantSource {
29
+ readonly preview?: TPreviewSpec;
30
+ readonly variants?: readonly IBlockVariant[];
31
+ }
32
+
33
+ export const DEFAULT_VARIANT = 'Default';
34
+
35
+ /**
36
+ * A facet's own trees AT ITS OWN VERSION: a distribution's preview and variants name its blocks
37
+ * with the version they were written at (`…_n_text_v_0_0_1`), and the facet of a later version
38
+ * still says so. Every block of the facet's own distribution is read at the facet's version; a
39
+ * block of another distribution keeps the version written.
40
+ */
41
+ export function atOwnVersion<T extends IVariantSource>(facet: T, reference: string): T {
42
+ const namespace = namespaceOfReference(reference);
43
+ const version = versionOfReference(reference);
44
+ if (!namespace || !version) return facet;
45
+ const own = (nodes: readonly IBlueprintNode[]): IBlueprintNode[] => nodes.map((node) => ({
46
+ ...node,
47
+ reference: namespaceOfReference(node.reference) === namespace ? withVersion(node.reference, version) : node.reference,
48
+ children: own(node.children ?? []),
49
+ }));
50
+ return {
51
+ ...facet,
52
+ ...(typeof facet.preview === 'object' ? { preview: { ...facet.preview, children: own(facet.preview.children) } } : {}),
53
+ ...(facet.variants ? { variants: facet.variants.map((variant) => (variant.children ? { ...variant, children: own(variant.children) } : variant)) } : {}),
54
+ };
55
+ }
56
+
57
+ /** the block's node in a tree the page mounts, when no preview frame names one */
58
+ export const SPECIMEN_ID = '__specimen';
59
+
60
+ /** the block's node in a preview tree: the first one of its module, anywhere in it */
61
+ function blockIn(nodes: readonly IBlueprintNode[], reference: string): IBlueprintNode | undefined {
62
+ for (const node of nodes) {
63
+ if (node.reference === reference) return node;
64
+ const inner = blockIn(node.children ?? [], reference);
65
+ if (inner) return inner;
66
+ }
67
+ return undefined;
68
+ }
69
+
70
+ /** what the preview shows around the block — its tree, and which node of it is the block */
71
+ export interface IVariantFrame {
72
+ readonly children: readonly IBlueprintNode[];
73
+ readonly nodeId: string;
74
+ }
75
+
76
+ /** the frame the facet's preview puts the block in, or null — the block stands alone */
77
+ export function frameOf(reference: string, facet: IVariantSource | undefined): IVariantFrame | null {
78
+ if (typeof facet?.preview !== 'object') return null;
79
+ const node = blockIn(facet.preview.children, reference);
80
+ return node ? { children: facet.preview.children, nodeId: node.id } : null;
81
+ }
82
+
83
+ /** the tree to mount: the specimen in its frame — or alone */
84
+ export function framed(frame: IVariantFrame | null, reference: string, specimen: Pick<ISpecimen, 'settings' | 'children'>): IBlueprintNode[] {
85
+ const own = (id: string): IBlueprintNode => ({ id, reference, settings: specimen.settings, children: [...specimen.children] });
86
+ if (!frame) return [own(SPECIMEN_ID)];
87
+ const place = (nodes: readonly IBlueprintNode[]): IBlueprintNode[] => nodes.map((node) => (node.id === frame.nodeId
88
+ ? { ...own(node.id), ...(node.slot !== undefined ? { slot: node.slot } : {}) }
89
+ : { ...node, children: place(node.children ?? []) }));
90
+ return place(frame.children);
91
+ }
92
+
93
+ /** the variants of the block `reference`, Default first */
94
+ export function specimensOf(reference: string, facet: IVariantSource | undefined, defaults: Record<string, unknown>): ISpecimen[] {
95
+ const shown = typeof facet?.preview === 'object' ? blockIn(facet.preview.children, reference) : undefined;
96
+ const base: ISpecimen = {
97
+ name: DEFAULT_VARIANT,
98
+ settings: { ...defaults, ...(shown?.settings ?? {}) },
99
+ children: shown?.children ?? [],
100
+ };
101
+ const declared = (facet?.variants ?? [])
102
+ .filter((variant) => typeof variant?.name === 'string' && variant.name.trim() !== '' && variant.name !== DEFAULT_VARIANT)
103
+ .map((variant): ISpecimen => ({
104
+ name: variant.name,
105
+ ...(variant.description ? { description: variant.description } : {}),
106
+ settings: { ...defaults, ...(variant.settings ?? {}) },
107
+ children: variant.children ?? base.children,
108
+ }));
109
+ return [base, ...declared];
110
+ }
111
+
112
+ /**
113
+ * What the playground's settings differ in from the defaults — the `settings` of a variant that
114
+ * would show them. A setting the defaults do not have is kept; one equal to its default is not.
115
+ */
116
+ export function settingsOver(settings: Record<string, unknown>, defaults: Record<string, unknown>): Record<string, unknown> {
117
+ const over: Record<string, unknown> = {};
118
+ for (const [key, value] of Object.entries(settings)) {
119
+ if (JSON.stringify(value) !== JSON.stringify(defaults[key])) over[key] = value;
120
+ }
121
+ return over;
122
+ }
123
+
124
+ /**
125
+ * The playground's settings as an entry of `variants`, in JSON — which is also an object literal,
126
+ * so it pastes into an editor facet as it is
127
+ */
128
+ export function variantSource(name: string, settings: Record<string, unknown>, defaults: Record<string, unknown>): string {
129
+ return JSON.stringify({ name, settings: settingsOver(settings, defaults) }, null, 2);
130
+ }
131
+
132
+ /**
133
+ * Whether a text is a VARIANT (variantSource): an object with a name and settings — which a page
134
+ * would take for written words and paste as a paragraph of braces. The editor's paste refuses it
135
+ * and points to "Copy block".
136
+ */
137
+ export function isVariantSource(text: string): boolean {
138
+ const trimmed = text.trim().replace(/,$/, '');
139
+ if (!trimmed.startsWith('{')) return false;
140
+ try {
141
+ const value = JSON.parse(trimmed) as unknown;
142
+ if (typeof value !== 'object' || value === null || Array.isArray(value)) return false;
143
+ const { name, settings } = value as { name?: unknown; settings?: unknown };
144
+ return typeof name === 'string' && typeof settings === 'object' && settings !== null && !Array.isArray(settings);
145
+ } catch {
146
+ return false;
147
+ }
148
+ }
149
+
150
+ /**
151
+ * The playground's settings: a store over itself, in the shape a block's form reads
152
+ * (`useSettingsDraft`). `reset` puts a variant in — the form and the stage follow.
153
+ */
154
+ export class PlaygroundDraft implements ISettingsDraftContract {
155
+ private settings: Record<string, unknown>;
156
+
157
+ private readonly listeners = new Set<() => void>();
158
+
159
+ constructor(settings: Record<string, unknown>) {
160
+ this.settings = settings;
161
+ }
162
+
163
+ snapshot = (): Record<string, unknown> => this.settings;
164
+
165
+ subscribe = (listener: () => void): (() => void) => {
166
+ this.listeners.add(listener);
167
+ return () => {
168
+ this.listeners.delete(listener);
169
+ };
170
+ };
171
+
172
+ update = (patch: Record<string, unknown>): void => {
173
+ this.settings = { ...this.settings, ...patch };
174
+ this.notify();
175
+ };
176
+
177
+ reset(settings: Record<string, unknown>): void {
178
+ this.settings = settings;
179
+ this.notify();
180
+ }
181
+
182
+ private notify(): void {
183
+ for (const listener of [...this.listeners]) listener();
184
+ }
185
+ }
@@ -0,0 +1,77 @@
1
+ // WHICH blocks a text paste becomes: the splitter needs full references (namespace, name,
2
+ // version), and it must not guess them. Two sources, in order: the catalog the palette has in
3
+ // hand (registered here when it arrives), and the page itself — a document that already shows a
4
+ // mosetta-ui block names the version to use. Neither → the paste is declined with a hint.
5
+ import { DiContainer, SingleDiToken } from '@mosetta/di';
6
+ import { compareVersions } from '@mosetta/content';
7
+ import type { ICatalogEntry, IBlueprintNodeDoc } from '@mosetta/content';
8
+ import type { IBlockVocabulary } from './pasteText';
9
+
10
+ const UI_NAMESPACE = 'mosetta-ui';
11
+ const NAMES: (keyof IBlockVocabulary)[] = ['heading', 'text', 'list', 'item', 'quote'];
12
+
13
+ const reference = (version: string, name: string) => `_ns_${UI_NAMESPACE}_t_component_n_${name}_v_${version}`;
14
+
15
+ const vocabularyAt = (version: string): IBlockVocabulary => Object.fromEntries(NAMES.map((name) => [name, reference(version, name)])) as unknown as IBlockVocabulary;
16
+
17
+ /**
18
+ * The ui-kit version the catalog ships, when every text block is there at that version — the
19
+ * highest such version wins. Taken from the entries' own ids (the canonical encoding of the
20
+ * version lives there; the reference's `version` is the dotted form).
21
+ */
22
+ export function vocabularyFromCatalog(entries: readonly ICatalogEntry[]): IBlockVocabulary | null {
23
+ const byVersion = new Map<string, Partial<Record<keyof IBlockVocabulary, string>>>();
24
+ for (const e of entries) {
25
+ if (e.reference.namespace !== UI_NAMESPACE || e.reference.type !== 'component') continue;
26
+ if (!(NAMES as string[]).includes(e.reference.name)) continue;
27
+ const names = byVersion.get(e.reference.version) ?? {};
28
+ names[e.reference.name as keyof IBlockVocabulary] = e.id;
29
+ byVersion.set(e.reference.version, names);
30
+ }
31
+ // the latest first — as versions, not as strings (0.0.10 is after 0.0.9)
32
+ for (const version of [...byVersion.keys()].sort((a, b) => compareVersions(b, a))) {
33
+ const names = byVersion.get(version)!;
34
+ if (NAMES.every((name) => names[name])) return names as IBlockVocabulary;
35
+ }
36
+ return null;
37
+ }
38
+
39
+ const NODE_RE = new RegExp(`^_ns_${UI_NAMESPACE}_t_component_n_[A-Za-z0-9]+_v_(.+)$`);
40
+
41
+ /** the ui-kit version the PAGE already uses — the first mosetta-ui block met in document order */
42
+ export function vocabularyFromDoc(doc: IBlueprintNodeDoc): IBlockVocabulary | null {
43
+ const walk = (node: IBlueprintNodeDoc): string | null => {
44
+ const m = NODE_RE.exec(node.reference);
45
+ if (m) return m[1];
46
+ for (const child of node.children ?? []) {
47
+ const found = walk(child);
48
+ if (found) return found;
49
+ }
50
+ return null;
51
+ };
52
+ const version = walk(doc);
53
+ return version ? vocabularyAt(version) : null;
54
+ }
55
+
56
+ /**
57
+ * Held in ONE mutable holder per workbench: every palette that mounts publishes the catalog it
58
+ * got — closing the palette and opening it again (or a second palette) publishes again, and a
59
+ * container takes a token once. The latest catalog wins.
60
+ */
61
+ interface IVocabularyHolder {
62
+ current: IBlockVocabulary | null;
63
+ }
64
+
65
+ export const BLOCK_VOCABULARY_TOKEN = new SingleDiToken<IVocabularyHolder>('BLOCK_VOCABULARY_TOKEN');
66
+
67
+ /** a palette calls this whenever its catalog is in: from then on a text paste knows its blocks */
68
+ export function publishVocabulary(di: DiContainer, entries: readonly ICatalogEntry[]): void {
69
+ const vocab = vocabularyFromCatalog(entries);
70
+ if (!vocab) return;
71
+ if (!di.hasByToken(BLOCK_VOCABULARY_TOKEN)) di.registerValueOfToken(BLOCK_VOCABULARY_TOKEN, { current: null });
72
+ di.getByToken(BLOCK_VOCABULARY_TOKEN).current = vocab;
73
+ }
74
+
75
+ export function vocabularyOf(di: DiContainer): IBlockVocabulary | null {
76
+ return di.hasByToken(BLOCK_VOCABULARY_TOKEN) ? di.getByToken(BLOCK_VOCABULARY_TOKEN).current : null;
77
+ }
@@ -0,0 +1,106 @@
1
+ // THE VERSIONS the catalog holds (ADR-0061): every version of a distribution deployed stays in it,
2
+ // a page names its blocks exactly, and the editor offers the latest by default. Plain logic over
3
+ // the catalog's entries — the picker, the Distributions tab, the tree's marks and the updates
4
+ // window all read it.
5
+ import { compareVersions, versionOfReference, withVersion } from '@mosetta/content';
6
+ import type { ICatalogEntry, IDistributionEntry } from '@mosetta/content';
7
+ import { namespaceOfReference } from '@mosetta/content/units';
8
+
9
+ /** a block that has a newer version: where it is, and where it would go */
10
+ export interface IUpgrade {
11
+ /** the reference as the page names it */
12
+ readonly from: string;
13
+ /** the same block at the latest version that has it */
14
+ readonly to: string;
15
+ readonly namespace: string;
16
+ readonly name: string;
17
+ readonly fromVersion: string;
18
+ readonly toVersion: string;
19
+ }
20
+
21
+ /** where a placed block comes from: its distribution and the version the page names */
22
+ export interface IPlacedVersion {
23
+ readonly namespace: string;
24
+ readonly version: string;
25
+ /** the latest version that has the block, when it is newer than the placed one */
26
+ readonly newer: string | null;
27
+ }
28
+
29
+ /** what a version of a distribution says changed */
30
+ export interface IVersionChanges {
31
+ readonly version: string;
32
+ readonly changes: string;
33
+ }
34
+
35
+ export interface ICatalogVersions {
36
+ /** the versions of a namespace's distribution, the latest first */
37
+ versions(namespace: string): readonly string[];
38
+ /** the latest version of a namespace, or undefined for one the catalog does not know */
39
+ latest(namespace: string): string | undefined;
40
+ /** the distribution's own entry at a version (its title, icon, what changed) */
41
+ distribution(namespace: string, version: string): IDistributionEntry | undefined;
42
+ /** the newer version of the block a reference names, or null — the latest, or a prefab */
43
+ upgradeOf(reference: string): IUpgrade | null;
44
+ /** what each version after `from` up to `to` says it changed, oldest first */
45
+ changesBetween(namespace: string, from: string, to: string): readonly IVersionChanges[];
46
+ /**
47
+ * the distribution and version a placed block names, or null — a prefab is live (no version),
48
+ * and a tool's block (the editor's page root) always runs at its latest, whatever the page names
49
+ */
50
+ placedAt(reference: string): IPlacedVersion | null;
51
+ /**
52
+ * a TOOL's block (of an internal distribution — the editor's page root, its fallback) at the
53
+ * latest version the catalog has, the way the server renders it; any other reference as it is
54
+ */
55
+ leadTool(reference: string): string;
56
+ }
57
+
58
+ const NAME_RE = /_t_component_n_(.+)_v_[A-Za-z0-9_]+$/;
59
+
60
+ export function catalogVersions(components: readonly ICatalogEntry[], distributions: readonly IDistributionEntry[]): ICatalogVersions {
61
+ const byNamespace = new Map<string, Set<string>>();
62
+ // a block's versions, by namespace and name: a block may come into a distribution later, or leave it
63
+ const byBlock = new Map<string, Set<string>>();
64
+ for (const entry of components) {
65
+ const { namespace, name, version } = entry.reference;
66
+ byNamespace.set(namespace, (byNamespace.get(namespace) ?? new Set()).add(version));
67
+ byBlock.set(`${namespace}/${name}`, (byBlock.get(`${namespace}/${name}`) ?? new Set()).add(version));
68
+ }
69
+ for (const d of distributions) byNamespace.set(d.namespace, (byNamespace.get(d.namespace) ?? new Set()).add(d.version));
70
+ const sorted = (versions: Set<string> | undefined) => [...(versions ?? [])].sort((a, b) => compareVersions(b, a));
71
+ const same = (a: string, b: string) => compareVersions(a, b) === 0;
72
+ const upgradeOf = (reference: string): IUpgrade | null => {
73
+ const namespace = namespaceOfReference(reference);
74
+ const name = NAME_RE.exec(reference)?.[1];
75
+ const fromVersion = versionOfReference(reference);
76
+ if (!namespace || !name || !fromVersion) return null;
77
+ const toVersion = sorted(byBlock.get(`${namespace}/${name}`))[0];
78
+ if (!toVersion || compareVersions(toVersion, fromVersion) <= 0) return null;
79
+ return { from: reference, to: withVersion(reference, toVersion), namespace, name, fromVersion, toVersion };
80
+ };
81
+
82
+ return {
83
+ versions: (namespace) => sorted(byNamespace.get(namespace)),
84
+ latest: (namespace) => sorted(byNamespace.get(namespace))[0],
85
+ distribution: (namespace, version) => distributions.find((d) => d.namespace === namespace && same(d.version, version)),
86
+ upgradeOf,
87
+ changesBetween(namespace, from, to) {
88
+ return distributions
89
+ .filter((d) => d.namespace === namespace && compareVersions(d.version, from) > 0 && compareVersions(d.version, to) <= 0)
90
+ .sort((a, b) => compareVersions(a.version, b.version))
91
+ .flatMap((d) => (d.changes ? [{ version: d.version, changes: d.changes }] : []));
92
+ },
93
+ leadTool(reference) {
94
+ const namespace = namespaceOfReference(reference);
95
+ if (!namespace || !distributions.some((d) => d.namespace === namespace && d.internal === true)) return reference;
96
+ return upgradeOf(reference)?.to ?? reference;
97
+ },
98
+ placedAt(reference) {
99
+ const namespace = namespaceOfReference(reference);
100
+ const version = versionOfReference(reference);
101
+ if (!namespace || !version) return null;
102
+ if (distributions.some((d) => d.namespace === namespace && d.internal === true)) return null;
103
+ return { namespace, version, newer: upgradeOf(reference)?.toVersion ?? null };
104
+ },
105
+ };
106
+ }
@@ -0,0 +1,82 @@
1
+ /* The block preview bubble (componentPreview.tsx) — shown beside a block row of the palette and
2
+ of the canvas's insert popover. */
3
+
4
+ /* THE PREVIEW BUBBLE. In a portal on the document body — the panel scrolls and clips, and this
5
+ has to leave the dock entirely — so it is positioned in WINDOW coordinates and given a z-index
6
+ above the dock but below a full-screen preview (which owns the screen outright).
7
+ Pointer-transparent: it is to be looked at, never reached, and so can never steal the hover
8
+ that opened it. */
9
+ .palette-preview {
10
+ position: fixed;
11
+ z-index: 2147482000;
12
+
13
+ /* NO overflow here: clipping lives on the viewport inside — this box must not cut the half of
14
+ the beak that stands outside it. The width/height styles are the CONTENT's; the padding and
15
+ the border ride on top of them (content-box), so the frame never eats into what was measured. */
16
+ box-sizing: content-box;
17
+ border: 1px solid #e4e4e7;
18
+ border-radius: 8px;
19
+ background: #ffffff;
20
+ box-shadow: 0 8px 24px rgb(0 0 0 / 12%);
21
+ pointer-events: none;
22
+
23
+ /* the panel's own font and size are the EDITOR's; a block previewed inside them would inherit
24
+ a look it will never have on a page. This is as close to "no inherited styles" as the
25
+ bubble can honestly get. */
26
+ color: #18181b;
27
+ font: 16px/1.5 ui-sans-serif, system-ui, sans-serif;
28
+ }
29
+
30
+ /* the beak, on the row's own line: a square turned 45°, showing the two sides that face out */
31
+ .palette-preview__beak {
32
+ position: absolute;
33
+ left: -5px;
34
+ width: 8px;
35
+ height: 8px;
36
+ border-bottom: 1px solid #e4e4e7;
37
+ border-left: 1px solid #e4e4e7;
38
+ background: #ffffff;
39
+ transform: rotate(45deg);
40
+ }
41
+
42
+ .palette-preview--flipped .palette-preview__beak {
43
+ right: -5px;
44
+ left: auto;
45
+ border-top: 1px solid #e4e4e7;
46
+ border-right: 1px solid #e4e4e7;
47
+ border-bottom: 0;
48
+ border-left: 0;
49
+ }
50
+
51
+ /* clipping and centering both live here: past the ceiling the specimen is cut, under it the
52
+ specimen floats in the middle instead of hugging the corner of the box */
53
+ .palette-preview__viewport {
54
+ display: flex;
55
+ overflow: hidden;
56
+ width: 100%;
57
+ height: 100%;
58
+ }
59
+
60
+ /* measured at its NATURAL size — the box grows to what it shows, never the other way around;
61
+ `flex: none` keeps the measurement honest when the specimen is wider than the box.
62
+ `margin: auto` rather than the container centering: a specimen that FITS floats in the middle,
63
+ a specimen past the ceiling is cut from its TOP-LEFT — the corner where widgets keep their
64
+ headers — instead of showing a headless middle slice */
65
+ .palette-preview__content {
66
+ flex: none;
67
+ width: max-content;
68
+ margin: auto;
69
+ }
70
+
71
+ /* an overlay, not a row: the stage stays mounted underneath, whatever state it is in */
72
+ .palette-preview__failed {
73
+ position: absolute;
74
+ display: flex;
75
+ align-items: center;
76
+ justify-content: center;
77
+ inset: 0;
78
+ border-radius: 8px;
79
+ background: #ffffff;
80
+ color: #a1a1aa;
81
+ font-size: var(--mosetta-wb-font);
82
+ }