@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,194 @@
1
+ // The pages panel's client-side path search: pure functions, no react. The whole listing is on
2
+ // the client anyway (the panel fetches it in one action), so filtering is local and instant.
3
+ //
4
+ // Matching runs in TIERS — the first tier that matches wins, and results sort by tier first:
5
+ // 0 PREFIX — the term is the path's beginning: `/app` finds /apple/green/small
6
+ // 1 INITIALS — the term walks the path's word starts: `ags` finds /apple/green/small
7
+ // (word starts = segment starts + camelCase/dash/underscore/digit boundaries)
8
+ // 2 SUBSTRING — the term sits somewhere inside: `een/sm` finds /apple/green/small
9
+ // 3 SCATTER — the term is an in-order scatter of characters (fzf-style), tie-broken by how
10
+ // tightly the matched characters cluster: `apsm` finds /apple/green/small
11
+ // Every match reports the matched character indices so the UI can highlight them.
12
+
13
+ export interface IPathMatch {
14
+ /** the tier that matched (lower is better) */
15
+ readonly tier: number;
16
+ /** tie-breaker inside a tier (lower is better) */
17
+ readonly score: number;
18
+ /** indices into the PATH string to highlight */
19
+ readonly indices: readonly number[];
20
+ }
21
+
22
+ /** where words start in a path: after separators, at camelCase humps, at letter→digit turns */
23
+ export function wordStarts(path: string): number[] {
24
+ const starts: number[] = [];
25
+ for (let i = 0; i < path.length; i += 1) {
26
+ const ch = path[i];
27
+ if (!/[a-zA-Z0-9]/.test(ch)) continue;
28
+ const prev = i > 0 ? path[i - 1] : '';
29
+ const isBoundary = i === 0
30
+ || !/[a-zA-Z0-9]/.test(prev) // after a separator (/ - _ . {)
31
+ || (/[a-z]/.test(prev) && /[A-Z]/.test(ch)) // camelCase hump
32
+ || (/[a-zA-Z]/.test(prev) && /[0-9]/.test(ch)); // letter → digit
33
+ if (isBoundary) starts.push(i);
34
+ }
35
+ return starts;
36
+ }
37
+
38
+ function prefixMatch(path: string, term: string): IPathMatch | null {
39
+ // typing without the leading slash is natural — `app` prefixes /apple the same as `/app` does
40
+ const lower = path.toLowerCase();
41
+ const wanted = term.toLowerCase();
42
+ if (lower.startsWith(wanted)) {
43
+ return { tier: 0, score: path.length, indices: Array.from({ length: term.length }, (_, i) => i) };
44
+ }
45
+ if (!wanted.startsWith('/') && lower.startsWith(`/${wanted}`)) {
46
+ return { tier: 0, score: path.length, indices: Array.from({ length: term.length }, (_, i) => i + 1) };
47
+ }
48
+ return null;
49
+ }
50
+
51
+ function initialsMatch(path: string, term: string): IPathMatch | null {
52
+ // the term (separators dropped: typing /a/g/s or a-g-s should still work) walks the word
53
+ // starts IN ORDER — a scatter over initials, so `acd` finds /a/b/c/d too
54
+ const wanted = term.toLowerCase().replace(/[^a-z0-9]/g, '');
55
+ if (!wanted) return null;
56
+ const starts = wordStarts(path);
57
+ const indices: number[] = [];
58
+ let cursor = 0;
59
+ for (const start of starts) {
60
+ if (cursor >= wanted.length) break;
61
+ if (path[start].toLowerCase() === wanted[cursor]) {
62
+ indices.push(start);
63
+ cursor += 1;
64
+ }
65
+ }
66
+ if (cursor < wanted.length) return null;
67
+ // tighter runs first: fewer skipped word starts between the first and the last hit
68
+ const span = indices.length ? indices[indices.length - 1] - indices[0] : 0;
69
+ return { tier: 1, score: span * 100 + path.length, indices };
70
+ }
71
+
72
+ function substringMatch(path: string, term: string): IPathMatch | null {
73
+ const at = path.toLowerCase().indexOf(term.toLowerCase());
74
+ if (at < 0) return null;
75
+ return { tier: 2, score: at * 100 + path.length, indices: Array.from({ length: term.length }, (_, i) => at + i) };
76
+ }
77
+
78
+ function scatterMatch(path: string, term: string): IPathMatch | null {
79
+ const wanted = term.toLowerCase().replace(/\s+/g, '');
80
+ if (!wanted) return null;
81
+ const lower = path.toLowerCase();
82
+ const indices: number[] = [];
83
+ let from = 0;
84
+ for (const ch of wanted) {
85
+ const at = lower.indexOf(ch, from);
86
+ if (at < 0) return null;
87
+ indices.push(at);
88
+ from = at + 1;
89
+ }
90
+ const span = indices[indices.length - 1] - indices[0];
91
+ return { tier: 3, score: span * 100 + indices[0], indices };
92
+ }
93
+
94
+ /** null = no match; otherwise the best (lowest) tier that accepted the term */
95
+ export function matchPath(path: string, term: string): IPathMatch | null {
96
+ const trimmed = term.trim();
97
+ if (!trimmed) return { tier: 0, score: 0, indices: [] };
98
+ return prefixMatch(path, trimmed)
99
+ ?? initialsMatch(path, trimmed)
100
+ ?? substringMatch(path, trimmed)
101
+ ?? scatterMatch(path, trimmed);
102
+ }
103
+
104
+ /** matches sorted best-first; ties inside a tier break by score, then alphabetically */
105
+ export function rankPaths<T>(items: readonly T[], term: string, pathOf: (item: T) => string): Array<{ item: T; match: IPathMatch }> {
106
+ return items
107
+ .flatMap((item) => {
108
+ const match = matchPath(pathOf(item), term);
109
+ return match ? [{ item, match }] : [];
110
+ })
111
+ .sort((a, b) => a.match.tier - b.match.tier
112
+ || a.match.score - b.match.score
113
+ || pathOf(a.item).localeCompare(pathOf(b.item)));
114
+ }
115
+
116
+ /** the path's first segment — the grouping unit ('/' for the root itself) */
117
+ export function groupKeyOf(path: string): string {
118
+ const segment = path.split('/').filter(Boolean)[0];
119
+ return segment ? `/${segment}` : '/';
120
+ }
121
+
122
+ // ---- entity names ------------------------------------------------------------------------------
123
+
124
+ /** Cyrillic to Latin, letter by letter (a plain, readable transliteration) */
125
+ const CYRILLIC: Record<string, string> = {
126
+ а: 'a', б: 'b', в: 'v', г: 'g', д: 'd', е: 'e', ё: 'e', ж: 'zh', з: 'z', и: 'i', й: 'y', к: 'k', л: 'l', м: 'm', н: 'n',
127
+ о: 'o', п: 'p', р: 'r', с: 's', т: 't', у: 'u', ф: 'f', х: 'kh', ц: 'ts', ч: 'ch', ш: 'sh', щ: 'shch', ъ: '', ы: 'y',
128
+ ь: '', э: 'e', ю: 'yu', я: 'ya', і: 'i', ї: 'yi', є: 'ye', ґ: 'g',
129
+ };
130
+
131
+ /** the path's letters as Latin: diacritics dropped (é → e), Cyrillic transliterated; the rest stays */
132
+ export function latinize(text: string): string {
133
+ return text
134
+ .normalize('NFD')
135
+ .replace(/[\u0300-\u036f]/g, '')
136
+ .replace(/[а-яёіїєґ]/gi, (letter) => {
137
+ const lower = letter.toLowerCase();
138
+ const out = CYRILLIC[lower] ?? '';
139
+ return letter === lower ? out : out.charAt(0).toUpperCase() + out.slice(1);
140
+ });
141
+ }
142
+
143
+ /**
144
+ * The entity name is a STORAGE KEY, not a UX concept: the panel speaks in paths, and the key is
145
+ * derived once at creation (lowerCamel per ENTITY_NAME_RE — `/apple/green/small` → appleGreenSmall)
146
+ * and never shown. It stays stable afterwards even if drafts move the pattern: patterns are
147
+ * versioned with the docs, keys are not. A path in another alphabet is latinized first
148
+ * (`/о-нас` → oNas); one with no letters the name can carry (CJK, Arabic) is just a `page`.
149
+ */
150
+ export function nameFromPattern(pattern: string, taken: ReadonlySet<string>): string {
151
+ const path = pattern.replace(/\?.*$/, '');
152
+ const words = latinize(path).split(/[^a-zA-Z0-9]+/).filter(Boolean);
153
+ let base = words
154
+ .map((word, i) => {
155
+ const clean = word.toLowerCase();
156
+ return i === 0 ? clean : clean.charAt(0).toUpperCase() + clean.slice(1);
157
+ })
158
+ .join('');
159
+ // the root path '/' has no words — it is the home; a path with words in letters the name
160
+ // cannot carry is a page, not a second home
161
+ if (!base) base = path.split('/').some(Boolean) ? 'page' : 'home';
162
+ if (!/^[a-z]/.test(base)) base = `p${base}`; // names must start with a letter
163
+ if (!taken.has(base)) return base;
164
+ for (let n = 2; ; n += 1) {
165
+ const candidate = `${base}${n}`;
166
+ if (!taken.has(candidate)) return candidate;
167
+ }
168
+ }
169
+
170
+ /**
171
+ * How many draft tags a folded route row shows: the live tag always stands (its width is taken
172
+ * first), then drafts in order while they fit; when not all of them do, a "…" tag must fit too.
173
+ * Widths in pixels, `gap` between neighbouring tags.
174
+ */
175
+ export function fittingTags({ room, gap, live, drafts, more }: {
176
+ readonly room: number;
177
+ readonly gap: number;
178
+ /** the live tag's width, 0 when nothing is published */
179
+ readonly live: number;
180
+ readonly drafts: readonly number[];
181
+ readonly more: number;
182
+ }): number {
183
+ const left = room - (live ? live + gap : 0);
184
+ const all = drafts.reduce((sum, w) => sum + w, 0) + gap * Math.max(0, drafts.length - 1);
185
+ if (all <= left) return drafts.length;
186
+ let used = more;
187
+ let fit = 0;
188
+ for (const w of drafts) {
189
+ if (used + gap + w > left) break;
190
+ used += gap + w;
191
+ fit += 1;
192
+ }
193
+ return fit;
194
+ }
@@ -0,0 +1,98 @@
1
+ // The pages as a TREE of their routes: `/blog` and `/blog/{slug}` are a branch and its leaf, a
2
+ // folder stands in for a prefix no page owns (`/docs/api/{name}` without `/docs`). Pure functions,
3
+ // no react: the panel fetches the whole listing in one action anyway, so the shape is computed
4
+ // on the client and recomputed on every keystroke of the search.
5
+ //
6
+ // A search ranks the full patterns (pagesSearch) and the tree is rebuilt from the matches alone;
7
+ // the matched characters are carried along the hierarchy — every node lights the characters of
8
+ // its own segment that any matched route under it lit, so the eye follows the match down the
9
+ // branch instead of hunting for it in a flat path.
10
+ import type { IPathMatch } from './pagesSearch';
11
+
12
+ export interface IRouteTreeNode<T> {
13
+ /** this level's piece of the pattern: `blog`, `{slug}`, and `/` for the root page itself */
14
+ readonly segment: string;
15
+ /** the pattern up to and including this segment — the key of the node */
16
+ readonly path: string;
17
+ /** the page whose pattern ends here, or null for a folder no page owns */
18
+ readonly route: T | null;
19
+ readonly children: IRouteTreeNode<T>[];
20
+ /** matched character indices INSIDE the segment (a search), empty otherwise */
21
+ readonly lit: readonly number[];
22
+ }
23
+
24
+ interface IMutableNode<T> extends IRouteTreeNode<T> {
25
+ route: T | null;
26
+ children: IMutableNode<T>[];
27
+ lit: number[];
28
+ }
29
+
30
+ /** a pattern's segments, the query requirement (if any) kept on the last one */
31
+ export function patternSegments(pattern: string): string[] {
32
+ const at = pattern.indexOf('?');
33
+ const path = at < 0 ? pattern : pattern.slice(0, at);
34
+ const query = at < 0 ? '' : pattern.slice(at);
35
+ const segments = path.split('/').filter(Boolean);
36
+ if (segments.length === 0) return [`/${query}`];
37
+ segments[segments.length - 1] += query;
38
+ return segments;
39
+ }
40
+
41
+ const isParam = (segment: string): boolean => segment.startsWith('{');
42
+
43
+ /** literals before parameters, the root page first, alphabetical inside */
44
+ function compareNodes<T>(a: IRouteTreeNode<T>, b: IRouteTreeNode<T>): number {
45
+ if (a.segment === '/') return -1;
46
+ if (b.segment === '/') return 1;
47
+ const paramA = isParam(a.segment);
48
+ const paramB = isParam(b.segment);
49
+ if (paramA !== paramB) return paramA ? 1 : -1;
50
+ return a.segment.localeCompare(b.segment);
51
+ }
52
+
53
+ function sortDeep<T>(nodes: IMutableNode<T>[]): void {
54
+ nodes.sort(compareNodes);
55
+ for (const node of nodes) sortDeep(node.children);
56
+ }
57
+
58
+ /**
59
+ * The tree of the given routes. With matches, each route's lit indices (into its full pattern)
60
+ * are split among the nodes on its way down; a node reached by several routes keeps the union.
61
+ */
62
+ export function buildRouteTree<T>(
63
+ routes: readonly T[],
64
+ patternOf: (route: T) => string,
65
+ matchOf?: (route: T) => IPathMatch | null,
66
+ ): IRouteTreeNode<T>[] {
67
+ const top: IMutableNode<T>[] = [];
68
+ for (const route of routes) {
69
+ const pattern = patternOf(route);
70
+ const lit = matchOf?.(route)?.indices ?? [];
71
+ const segments = patternSegments(pattern);
72
+ let level = top;
73
+ let path = '';
74
+ segments.forEach((segment, i) => {
75
+ // the node's path is the pattern prefix the segment closes: the segment's characters start
76
+ // where the prefix before it ended, after the `/` that opens it (the root page excepted)
77
+ const start = segment === '/' ? 0 : path.length + 1;
78
+ path = segment === '/' ? '/' : `${path}/${segment}`;
79
+ let node = level.find((candidate) => candidate.segment === segment);
80
+ if (!node) {
81
+ node = { segment, path, route: null, children: [], lit: [] };
82
+ level.push(node);
83
+ }
84
+ if (i === segments.length - 1) node.route = route;
85
+ for (const index of lit) {
86
+ if (index >= start && index < start + segment.length && !node.lit.includes(index - start)) node.lit.push(index - start);
87
+ }
88
+ level = node.children;
89
+ });
90
+ }
91
+ sortDeep(top);
92
+ return top;
93
+ }
94
+
95
+ /** every node's path, depth-first — the keys a folded/unfolded set is kept by */
96
+ export function pathsOf<T>(nodes: readonly IRouteTreeNode<T>[]): string[] {
97
+ return nodes.flatMap((node) => [node.path, ...pathsOf(node.children)]);
98
+ }
@@ -0,0 +1,66 @@
1
+ // The LANDING SPACE in the preview's own copy of the page: where a block is about to go, the agent
2
+ // makes room for it (previewBridge.component.tsx renders the emptiness), so the overlay can frame
3
+ // the very spot. Pure tree surgery — the editor's document never holds the node.
4
+ import { DEFAULT_SLOT } from '@mosetta/react';
5
+ import type { IBlueprintNode, IBlueprint } from '@mosetta/core';
6
+ import { isOutletReference, unitOwnerOf } from '@mosetta/content/units';
7
+ import { LANDING_NODE_ID } from './previewBridgeCommon';
8
+ import type { ILandingPlace } from './previewBridgeCommon';
9
+
10
+ /** a tree without the landing space, wherever it stood */
11
+ export function withoutLanding<TNode extends { children?: readonly IBlueprintNode[] }>(node: TNode): TNode {
12
+ const children = node.children;
13
+ if (!children?.length) return node;
14
+ let changed = false;
15
+ const next: IBlueprintNode[] = [];
16
+ for (const child of children) {
17
+ if (child.id === LANDING_NODE_ID) {
18
+ changed = true;
19
+ continue;
20
+ }
21
+ const kept = withoutLanding(child);
22
+ if (kept !== child) changed = true;
23
+ next.push(kept);
24
+ }
25
+ return changed ? { ...node, children: next } : node;
26
+ }
27
+
28
+ /**
29
+ * The tree with the LANDING SPACE where a block is about to go (or without it, for null): a node
30
+ * of the agent's own module, which renders a little emptiness there (previewBridge.component.tsx),
31
+ * so the editor can frame the spot. Among `parentId`'s children in `slot`, it stands at `index` —
32
+ * counted the way the editor counts, in the document's own order (the agent's node, last under the
33
+ * root, is counted too: it stands after every child the document has). A PREFAB parent is not in
34
+ * the realm's tree as itself — it was expanded into its blocks, and its children (the page's
35
+ * blocks) stand in its OUTLET: there the space goes. A parent the tree does not hold (it changed
36
+ * meanwhile) gets no space; only the editor's own copy of the page ever had one.
37
+ */
38
+ export function withLanding(page: IBlueprint, at: ILandingPlace | null, agentReference: string): IBlueprint {
39
+ const clean = withoutLanding(page);
40
+ if (!at) return clean;
41
+ const space: IBlueprintNode = {
42
+ id: LANDING_NODE_ID,
43
+ reference: agentReference,
44
+ settings: { landing: true },
45
+ ...(at.slot === DEFAULT_SLOT ? {} : { slot: at.slot }),
46
+ children: [],
47
+ };
48
+ const isParent = (node: { id?: string; reference?: string }): boolean => node.id === at.parentId
49
+ || (node.id !== undefined && unitOwnerOf(node.id) === at.parentId && isOutletReference(node.reference ?? ''));
50
+ const place = <TNode extends { id?: string; reference?: string; children?: readonly IBlueprintNode[] }>(node: TNode): TNode | null => {
51
+ const children = node.children ?? [];
52
+ if (isParent(node)) {
53
+ const inSlot = children.filter((child) => (child.slot ?? DEFAULT_SLOT) === at.slot);
54
+ const before = inSlot[at.index];
55
+ const last = inSlot[inSlot.length - 1];
56
+ const position = before ? children.indexOf(before) : last ? children.indexOf(last) + 1 : children.length;
57
+ return { ...node, children: [...children.slice(0, position), space, ...children.slice(position)] };
58
+ }
59
+ for (const [i, child] of children.entries()) {
60
+ const placed = place(child);
61
+ if (placed) return { ...node, children: [...children.slice(0, i), placed, ...children.slice(i + 1)] };
62
+ }
63
+ return null;
64
+ };
65
+ return place(clean) ?? clean;
66
+ }