@ankhorage/studio 0.0.3 → 0.0.5

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.
@@ -0,0 +1,858 @@
1
+ export const DEFAULT_STUDIO_SCREEN_TEMPLATE = {
2
+ id: 'tpl-screen-empty',
3
+ type: 'Screen',
4
+ props: {
5
+ width: 'wide',
6
+ },
7
+ children: [
8
+ {
9
+ id: 'tpl-screen-empty-header',
10
+ type: 'SectionHeader',
11
+ props: {
12
+ title: 'New Screen',
13
+ description: 'Start authoring with ZORA layouts and patterns.',
14
+ },
15
+ },
16
+ {
17
+ id: 'tpl-screen-empty-section',
18
+ type: 'ScreenSection',
19
+ props: {
20
+ title: 'Build the first section',
21
+ description: 'Insert panels, forms, or content patterns to start authoring.',
22
+ },
23
+ children: [
24
+ {
25
+ id: 'tpl-screen-empty-state',
26
+ type: 'EmptyState',
27
+ props: {
28
+ title: 'Canvas is ready',
29
+ description: 'Use Insert to add components and layouts.',
30
+ },
31
+ },
32
+ ],
33
+ },
34
+ {
35
+ id: 'tpl-screen-empty-action',
36
+ type: 'Button',
37
+ props: {
38
+ children: 'Add first section',
39
+ tone: 'primary',
40
+ emphasis: 'solid',
41
+ },
42
+ },
43
+ ],
44
+ };
45
+ export const generateManifestStateId = (prefix) => {
46
+ const timestamp = Date.now().toString(36);
47
+ const random = Math.random().toString(36).substring(2, 11);
48
+ const id = `${timestamp}-${random}`;
49
+ return prefix ? `${prefix.toLowerCase()}-${id}` : id;
50
+ };
51
+ export function createStudioManifestFingerprint(manifest) {
52
+ if (!manifest)
53
+ return '';
54
+ return JSON.stringify({
55
+ navigator: manifest.navigator,
56
+ screens: Object.keys(manifest.screens),
57
+ data: manifest.data ?? {},
58
+ dataBindings: Object.keys(manifest.dataBindings ?? {}),
59
+ dataSources: Object.keys(manifest.dataSources ?? {}),
60
+ themes: manifest.themes.map((theme) => theme.id),
61
+ activeThemeId: manifest.activeThemeId,
62
+ activeThemeMode: manifest.activeThemeMode,
63
+ settings: manifest.settings,
64
+ infra: manifest.infra,
65
+ });
66
+ }
67
+ export function pathToKey(path) {
68
+ return path.length === 0 ? '__root__' : path.join('/');
69
+ }
70
+ export function isRouteGroupSegment(segment) {
71
+ return /^\(.*\)$/.test(segment);
72
+ }
73
+ export function collectScreenRouteEntries(routes, parentPath = [], routePathPrefix = []) {
74
+ const entries = [];
75
+ for (const route of routes) {
76
+ const routePath = [...routePathPrefix, route.name];
77
+ if (route.screenId) {
78
+ entries.push({
79
+ route,
80
+ screenId: route.screenId,
81
+ parentPath,
82
+ routePath,
83
+ });
84
+ }
85
+ if (route.navigator?.routes.length) {
86
+ entries.push(...collectScreenRouteEntries(route.navigator.routes, routePath, routePath));
87
+ }
88
+ }
89
+ return entries;
90
+ }
91
+ export function groupScreenRouteEntries(entries) {
92
+ const groups = new Map();
93
+ for (const entry of entries) {
94
+ const key = pathToKey(entry.parentPath);
95
+ if (!groups.has(key)) {
96
+ groups.set(key, {
97
+ id: key,
98
+ parentPath: entry.parentPath,
99
+ entries: [],
100
+ });
101
+ }
102
+ groups.get(key)?.entries.push(entry);
103
+ }
104
+ return Array.from(groups.values());
105
+ }
106
+ export function listScreenIdsInRouteOrder(routes) {
107
+ return collectScreenRouteEntries(routes).map((entry) => entry.screenId);
108
+ }
109
+ export function resolveInitialActiveScreenId(manifest) {
110
+ if (!manifest)
111
+ return null;
112
+ const firstRoutedScreenId = listScreenIdsInRouteOrder(manifest.navigator.routes).find((screenId) => !!manifest.screens[screenId]);
113
+ const [firstScreenId] = Object.keys(manifest.screens);
114
+ return firstRoutedScreenId ?? firstScreenId ?? null;
115
+ }
116
+ export function resolveActiveRootNode(manifest, activeScreenId) {
117
+ if (!manifest || !activeScreenId)
118
+ return null;
119
+ return manifest.screens[activeScreenId]?.root ?? null;
120
+ }
121
+ export function findNodeInManifest(root, id) {
122
+ if (root.id === id)
123
+ return root;
124
+ for (const child of root.children ?? []) {
125
+ const nested = findNodeInManifest(child, id);
126
+ if (nested)
127
+ return nested;
128
+ }
129
+ return null;
130
+ }
131
+ export function resolveSafeSelectedNodeId(rootNode, selectedNodeId) {
132
+ if (!selectedNodeId || !rootNode)
133
+ return null;
134
+ return findNodeInManifest(rootNode, selectedNodeId) ? selectedNodeId : null;
135
+ }
136
+ export function findScreenIdForNode(manifest, nodeId) {
137
+ for (const [screenId, screen] of Object.entries(manifest.screens)) {
138
+ if (findNodeInManifest(screen.root, nodeId)) {
139
+ return screenId;
140
+ }
141
+ }
142
+ return null;
143
+ }
144
+ export function updateStudioManifestNode(manifest, activeScreenId, nodeId, newProps) {
145
+ if (!activeScreenId)
146
+ return manifest;
147
+ const screen = manifest.screens[activeScreenId];
148
+ if (!screen)
149
+ return manifest;
150
+ const newRoot = updateNodeInManifestTree(screen.root, nodeId, newProps);
151
+ if (newRoot === screen.root)
152
+ return manifest;
153
+ return {
154
+ ...manifest,
155
+ screens: {
156
+ ...manifest.screens,
157
+ [activeScreenId]: {
158
+ ...screen,
159
+ root: newRoot,
160
+ },
161
+ },
162
+ };
163
+ }
164
+ export function deleteStudioManifestNode(manifest, activeScreenId, nodeId) {
165
+ if (!activeScreenId)
166
+ return manifest;
167
+ const screen = manifest.screens[activeScreenId];
168
+ if (!screen || screen.root.id === nodeId)
169
+ return manifest;
170
+ const newRoot = removeNodeFromManifestTree(screen.root, nodeId);
171
+ if (!newRoot || newRoot === screen.root)
172
+ return manifest;
173
+ const nextDataBindings = Object.fromEntries(Object.entries(manifest.dataBindings ?? {}).filter(([componentId]) => componentId !== nodeId));
174
+ return {
175
+ ...manifest,
176
+ dataBindings: nextDataBindings,
177
+ screens: {
178
+ ...manifest.screens,
179
+ [activeScreenId]: {
180
+ ...screen,
181
+ root: newRoot,
182
+ },
183
+ },
184
+ };
185
+ }
186
+ export function moveStudioManifestNode(manifest, activeScreenId, nodeId, direction) {
187
+ if (!activeScreenId)
188
+ return manifest;
189
+ const screen = manifest.screens[activeScreenId];
190
+ if (!screen)
191
+ return manifest;
192
+ const newRoot = moveNodeInManifestTree(screen.root, nodeId, direction);
193
+ if (newRoot === screen.root)
194
+ return manifest;
195
+ return {
196
+ ...manifest,
197
+ screens: {
198
+ ...manifest.screens,
199
+ [activeScreenId]: {
200
+ ...screen,
201
+ root: newRoot,
202
+ },
203
+ },
204
+ };
205
+ }
206
+ export function insertStudioManifestNodeAtPlacement(args) {
207
+ const { manifest, activeScreenId, placement, newNode, componentMeta } = args;
208
+ if (!activeScreenId)
209
+ return null;
210
+ const screen = manifest.screens[activeScreenId];
211
+ if (!screen)
212
+ return null;
213
+ if (!validateManifestNodePlacement(screen.root, placement, newNode.type, componentMeta))
214
+ return null;
215
+ const insertion = insertChildAtIndex({
216
+ node: screen.root,
217
+ parentId: placement.parentId,
218
+ index: placement.index,
219
+ newNode,
220
+ });
221
+ if (!insertion.inserted)
222
+ return null;
223
+ return {
224
+ manifest: {
225
+ ...manifest,
226
+ screens: {
227
+ ...manifest.screens,
228
+ [activeScreenId]: {
229
+ ...screen,
230
+ root: insertion.node,
231
+ },
232
+ },
233
+ },
234
+ insertedNodeId: newNode.id,
235
+ };
236
+ }
237
+ export function moveStudioManifestNodeToPlacement(args) {
238
+ const { manifest, activeScreenId, nodeId, placement, componentMeta } = args;
239
+ if (!activeScreenId)
240
+ return null;
241
+ const screen = manifest.screens[activeScreenId];
242
+ if (!screen)
243
+ return null;
244
+ const source = findNodeWithParent(screen.root, nodeId);
245
+ if (!source?.parent)
246
+ return null;
247
+ if (placement.parentId === nodeId)
248
+ return null;
249
+ if (isDescendantNode(source.node, placement.parentId))
250
+ return null;
251
+ const adjustedPlacement = adjustMovePlacement({ source, placement });
252
+ if (!adjustedPlacement)
253
+ return null;
254
+ const removed = removeNodeForMove({ node: screen.root, nodeId });
255
+ if (!removed.removedNode)
256
+ return null;
257
+ if (!validateManifestNodePlacement(removed.node, adjustedPlacement, removed.removedNode.type, componentMeta)) {
258
+ return null;
259
+ }
260
+ const inserted = insertChildAtIndex({
261
+ node: removed.node,
262
+ parentId: adjustedPlacement.parentId,
263
+ index: adjustedPlacement.index,
264
+ newNode: removed.removedNode,
265
+ });
266
+ if (!inserted.inserted)
267
+ return null;
268
+ return {
269
+ manifest: {
270
+ ...manifest,
271
+ screens: {
272
+ ...manifest.screens,
273
+ [activeScreenId]: {
274
+ ...screen,
275
+ root: inserted.node,
276
+ },
277
+ },
278
+ },
279
+ movedNodeId: removed.removedNode.id,
280
+ };
281
+ }
282
+ export function updateStudioManifestAppData(manifest, data) {
283
+ return { ...manifest, data };
284
+ }
285
+ export function updateStudioManifestDataBindings(manifest, dataBindings) {
286
+ return { ...manifest, dataBindings };
287
+ }
288
+ export function updateStudioManifestDataSources(manifest, dataSources) {
289
+ return { ...manifest, dataSources };
290
+ }
291
+ export function createDefaultThemeConfig(themeIndex, id = generateManifestStateId('theme')) {
292
+ return {
293
+ id,
294
+ name: `New Theme ${themeIndex + 1}`,
295
+ light: {
296
+ primaryColor: '#3B82F6',
297
+ harmony: 'monochromatic',
298
+ },
299
+ dark: {
300
+ primaryColor: '#3B82F6',
301
+ harmony: 'monochromatic',
302
+ },
303
+ };
304
+ }
305
+ export function addStudioManifestTheme(manifest, theme = createDefaultThemeConfig(manifest.themes.length)) {
306
+ return { ...manifest, themes: [...manifest.themes, theme] };
307
+ }
308
+ export function updateStudioManifestTheme(manifest, themeId, updates) {
309
+ return {
310
+ ...manifest,
311
+ themes: manifest.themes.map((theme) => {
312
+ if (theme.id !== themeId)
313
+ return theme;
314
+ return {
315
+ ...theme,
316
+ ...(updates.name ? { name: updates.name } : {}),
317
+ ...(updates.light ? { light: { ...theme.light, ...updates.light } } : {}),
318
+ ...(updates.dark ? { dark: { ...theme.dark, ...updates.dark } } : {}),
319
+ };
320
+ }),
321
+ };
322
+ }
323
+ export function deleteStudioManifestTheme(manifest, themeId) {
324
+ if (manifest.themes.length <= 1)
325
+ return manifest;
326
+ const themes = manifest.themes.filter((theme) => theme.id !== themeId);
327
+ const activeThemeId = manifest.activeThemeId === themeId
328
+ ? (themes[0]?.id ?? manifest.activeThemeId)
329
+ : manifest.activeThemeId;
330
+ return { ...manifest, themes, activeThemeId };
331
+ }
332
+ export function setStudioManifestActiveThemeId(manifest, activeThemeId) {
333
+ return { ...manifest, activeThemeId };
334
+ }
335
+ export function setStudioManifestActiveThemeMode(manifest, activeThemeMode) {
336
+ return { ...manifest, activeThemeMode };
337
+ }
338
+ export function updateStudioManifestModuleConfig(manifest, moduleId, config) {
339
+ const previousModuleConfig = manifest.infra.modulesConfig?.[moduleId];
340
+ const updatedModuleConfig = {
341
+ ...manifest.infra.modulesConfig,
342
+ [moduleId]: {
343
+ ...(typeof previousModuleConfig === 'object' && previousModuleConfig !== null
344
+ ? previousModuleConfig
345
+ : {}),
346
+ ...config,
347
+ },
348
+ };
349
+ let nextManifest = {
350
+ ...manifest,
351
+ infra: {
352
+ ...manifest.infra,
353
+ modulesConfig: updatedModuleConfig,
354
+ },
355
+ };
356
+ if (moduleId === 'expo-localization') {
357
+ const previousLocalization = manifest.settings.localization;
358
+ const nextLocalization = { ...previousLocalization };
359
+ let hasLocalizationUpdate = false;
360
+ if (typeof config.defaultLocale === 'string') {
361
+ nextLocalization.defaultLocale = config.defaultLocale;
362
+ hasLocalizationUpdate = true;
363
+ }
364
+ if (Array.isArray(config.locales) &&
365
+ config.locales.every((locale) => typeof locale === 'string')) {
366
+ nextLocalization.locales = config.locales;
367
+ hasLocalizationUpdate = true;
368
+ }
369
+ if (hasLocalizationUpdate) {
370
+ nextManifest = {
371
+ ...nextManifest,
372
+ settings: {
373
+ ...manifest.settings,
374
+ localization: nextLocalization,
375
+ },
376
+ };
377
+ }
378
+ }
379
+ return nextManifest;
380
+ }
381
+ export function updateStudioManifestOAuthProviders(manifest, providers) {
382
+ const previousAuth = manifest.infra.auth ?? {
383
+ provider: 'supabase',
384
+ scope: 'global',
385
+ authorization: { kind: 'ABAC', engine: 'cerbos' },
386
+ };
387
+ const previousOauth = previousAuth.oauth ?? {
388
+ enabled: true,
389
+ callbackRoute: '/auth/callback',
390
+ providers: [],
391
+ };
392
+ return {
393
+ ...manifest,
394
+ infra: {
395
+ ...manifest.infra,
396
+ auth: {
397
+ ...previousAuth,
398
+ oauth: {
399
+ ...previousOauth,
400
+ enabled: providers.length > 0,
401
+ providers,
402
+ },
403
+ },
404
+ },
405
+ };
406
+ }
407
+ export function getPrimaryNavigatorPath(routes) {
408
+ const appGroupRoute = routes.find((route) => route.name === '(app)' && route.navigator?.routes);
409
+ if (appGroupRoute)
410
+ return ['(app)'];
411
+ const appRoute = routes.find((route) => route.name === 'app' && route.navigator?.routes);
412
+ if (appRoute)
413
+ return ['app'];
414
+ return [];
415
+ }
416
+ export function findParentPathForScreenId(routes, screenId, parentPath = [], routePathPrefix = []) {
417
+ for (const route of routes) {
418
+ const routePath = [...routePathPrefix, route.name];
419
+ if (route.screenId === screenId)
420
+ return parentPath;
421
+ if (route.navigator?.routes.length) {
422
+ const nested = findParentPathForScreenId(route.navigator.routes, screenId, routePath, routePath);
423
+ if (nested)
424
+ return nested;
425
+ }
426
+ }
427
+ return null;
428
+ }
429
+ export function findRoutesAtParentPath(routes, parentPath) {
430
+ if (parentPath.length === 0)
431
+ return routes;
432
+ const [segment, ...rest] = parentPath;
433
+ if (!segment)
434
+ return null;
435
+ const route = routes.find((item) => item.name === segment);
436
+ if (!route?.navigator?.routes)
437
+ return null;
438
+ return findRoutesAtParentPath(route.navigator.routes, rest);
439
+ }
440
+ export function insertRouteAtParentPath(routes, parentPath, newRoute) {
441
+ if (parentPath.length === 0)
442
+ return [...routes, newRoute];
443
+ const [segment, ...rest] = parentPath;
444
+ return routes.map((route) => {
445
+ if (route.name !== segment || !route.navigator?.routes)
446
+ return route;
447
+ return {
448
+ ...route,
449
+ navigator: {
450
+ ...route.navigator,
451
+ routes: insertRouteAtParentPath(route.navigator.routes, rest, newRoute),
452
+ },
453
+ };
454
+ });
455
+ }
456
+ export function findNavigatorAtPath(navigator, parentPath) {
457
+ if (parentPath.length === 0)
458
+ return navigator;
459
+ const [segment, ...rest] = parentPath;
460
+ if (!segment)
461
+ return null;
462
+ const route = navigator.routes.find((item) => item.name === segment);
463
+ if (!route?.navigator)
464
+ return null;
465
+ return findNavigatorAtPath(route.navigator, rest);
466
+ }
467
+ export function updateNavigatorAtPath(navigator, parentPath, updater) {
468
+ if (parentPath.length === 0)
469
+ return updater(navigator);
470
+ const [segment, ...rest] = parentPath;
471
+ return {
472
+ ...navigator,
473
+ routes: navigator.routes.map((route) => {
474
+ if (route.name !== segment || !route.navigator)
475
+ return route;
476
+ return {
477
+ ...route,
478
+ navigator: updateNavigatorAtPath(route.navigator, rest, updater),
479
+ };
480
+ }),
481
+ };
482
+ }
483
+ export function setStudioManifestNavigatorType(manifest, type) {
484
+ const primaryNavigatorPath = getPrimaryNavigatorPath(manifest.navigator.routes);
485
+ const currentNavigator = findNavigatorAtPath(manifest.navigator, primaryNavigatorPath);
486
+ if (!currentNavigator || currentNavigator.type === type)
487
+ return manifest;
488
+ return {
489
+ ...manifest,
490
+ navigator: updateNavigatorAtPath(manifest.navigator, primaryNavigatorPath, (current) => ({
491
+ ...current,
492
+ type,
493
+ })),
494
+ };
495
+ }
496
+ export function setStudioManifestNavigatorInitialRoute(manifest, routeName) {
497
+ const normalizedRoute = routeName.trim();
498
+ if (!normalizedRoute)
499
+ return manifest;
500
+ const primaryNavigatorPath = getPrimaryNavigatorPath(manifest.navigator.routes);
501
+ const currentNavigator = findNavigatorAtPath(manifest.navigator, primaryNavigatorPath);
502
+ if (!currentNavigator)
503
+ return manifest;
504
+ if (!currentNavigator.routes.some((route) => route.name === normalizedRoute))
505
+ return manifest;
506
+ if (currentNavigator.initialRouteName === normalizedRoute)
507
+ return manifest;
508
+ return {
509
+ ...manifest,
510
+ navigator: updateNavigatorAtPath(manifest.navigator, primaryNavigatorPath, (current) => ({
511
+ ...current,
512
+ initialRouteName: normalizedRoute,
513
+ })),
514
+ };
515
+ }
516
+ export function addStudioManifestScreen(args) {
517
+ const { manifest, activeScreenId, createId = generateManifestStateId } = args;
518
+ const trimmedName = args.name.trim();
519
+ if (!trimmedName)
520
+ return { manifest, activeScreenId };
521
+ const baseRouteName = normalizeRouteName(trimmedName);
522
+ const activeParentPath = activeScreenId
523
+ ? findParentPathForScreenId(manifest.navigator.routes, activeScreenId)
524
+ : null;
525
+ const fallbackParentPath = getPrimaryNavigatorPath(manifest.navigator.routes);
526
+ const activeParentRoutes = activeParentPath !== null
527
+ ? findRoutesAtParentPath(manifest.navigator.routes, activeParentPath)
528
+ : null;
529
+ const parentPath = activeParentPath && activeParentRoutes ? activeParentPath : fallbackParentPath;
530
+ const siblingRoutes = findRoutesAtParentPath(manifest.navigator.routes, parentPath) ?? [];
531
+ let screenId = createId('Screen');
532
+ while (manifest.screens[screenId]) {
533
+ screenId = createId('Screen');
534
+ }
535
+ const existingPatterns = new Set(collectScreenRouteEntries(manifest.navigator.routes).map((entry) => toCanonicalRoutePattern(entry.routePath)));
536
+ const routeName = makeUniqueRouteNameForParent(baseRouteName, siblingRoutes, parentPath, existingPatterns);
537
+ const newScreen = {
538
+ id: screenId,
539
+ name: trimmedName,
540
+ title: trimmedName,
541
+ root: cloneNodeWithNewIds(args.screenTemplate ?? DEFAULT_STUDIO_SCREEN_TEMPLATE, createId),
542
+ };
543
+ return {
544
+ activeScreenId: screenId,
545
+ manifest: {
546
+ ...manifest,
547
+ screens: {
548
+ ...manifest.screens,
549
+ [screenId]: newScreen,
550
+ },
551
+ navigator: {
552
+ ...manifest.navigator,
553
+ routes: insertRouteAtParentPath(manifest.navigator.routes, parentPath, {
554
+ name: routeName,
555
+ label: trimmedName,
556
+ screenId,
557
+ }),
558
+ },
559
+ },
560
+ };
561
+ }
562
+ export function deleteStudioManifestScreen(manifest, screenId, activeScreenId) {
563
+ if (Object.keys(manifest.screens).length <= 1)
564
+ return { manifest, activeScreenId };
565
+ const { [screenId]: _deletedScreen, ...remainingScreens } = manifest.screens;
566
+ const remainingScreenIds = Object.keys(remainingScreens);
567
+ let safeRoutes = removeScreenIdFromRoutes(manifest.navigator.routes, screenId);
568
+ if (safeRoutes.length === 0 && remainingScreenIds.length > 0) {
569
+ const [fallbackScreenId] = remainingScreenIds;
570
+ if (!fallbackScreenId)
571
+ return { manifest, activeScreenId };
572
+ const fallbackScreen = remainingScreens[fallbackScreenId];
573
+ safeRoutes = [
574
+ {
575
+ name: makeUniqueSiblingRouteName('screen', []),
576
+ label: fallbackScreen?.name ?? 'Screen',
577
+ screenId: fallbackScreenId,
578
+ },
579
+ ];
580
+ }
581
+ const orderedScreenIds = listScreenIdsInRouteOrder(safeRoutes).filter((id) => !!remainingScreens[id]);
582
+ const nextActiveScreenId = !activeScreenId || activeScreenId === screenId || !remainingScreens[activeScreenId]
583
+ ? (orderedScreenIds[0] ?? remainingScreenIds[0] ?? null)
584
+ : activeScreenId;
585
+ return {
586
+ activeScreenId: nextActiveScreenId,
587
+ manifest: {
588
+ ...manifest,
589
+ screens: remainingScreens,
590
+ navigator: {
591
+ ...manifest.navigator,
592
+ routes: safeRoutes,
593
+ },
594
+ },
595
+ };
596
+ }
597
+ export function reorderStudioManifestScreens(manifest, newRoutes) {
598
+ return {
599
+ ...manifest,
600
+ navigator: {
601
+ ...manifest.navigator,
602
+ routes: newRoutes,
603
+ },
604
+ };
605
+ }
606
+ export function removeScreenIdFromRoutes(routes, screenId) {
607
+ const nextRoutes = [];
608
+ for (const route of routes) {
609
+ const nextRoute = { ...route };
610
+ if (nextRoute.screenId === screenId) {
611
+ delete nextRoute.screenId;
612
+ }
613
+ if (nextRoute.navigator?.routes) {
614
+ const nextNested = removeScreenIdFromRoutes(nextRoute.navigator.routes, screenId);
615
+ if (nextNested.length === 0) {
616
+ delete nextRoute.navigator;
617
+ }
618
+ else {
619
+ nextRoute.navigator = normalizeNavigatorAfterRouteUpdate({
620
+ ...nextRoute.navigator,
621
+ routes: nextNested,
622
+ });
623
+ }
624
+ }
625
+ if (!nextRoute.screenId && !nextRoute.navigator)
626
+ continue;
627
+ nextRoutes.push(nextRoute);
628
+ }
629
+ return nextRoutes;
630
+ }
631
+ export function makeUniqueSiblingRouteName(base, siblingRoutes) {
632
+ const normalized = normalizeRouteName(base);
633
+ const existingNames = new Set(siblingRoutes.map((route) => route.name));
634
+ if (!existingNames.has(normalized))
635
+ return normalized;
636
+ let suffix = 2;
637
+ while (existingNames.has(`${normalized}-${suffix}`)) {
638
+ suffix += 1;
639
+ }
640
+ return `${normalized}-${suffix}`;
641
+ }
642
+ export function toCanonicalRoutePattern(routePath) {
643
+ const normalized = routePath.filter((segment) => !isRouteGroupSegment(segment));
644
+ while (normalized[0] === 'index')
645
+ normalized.shift();
646
+ while (normalized.at(-1) === 'index')
647
+ normalized.pop();
648
+ return normalized.length ? `/${normalized.join('/')}` : '/';
649
+ }
650
+ export function makeUniqueRouteNameForParent(baseRouteName, siblingRoutes, parentPath, existingPatterns) {
651
+ const siblingNames = new Set(siblingRoutes.map((route) => route.name));
652
+ let candidate = baseRouteName;
653
+ let suffix = 2;
654
+ while (siblingNames.has(candidate) ||
655
+ existingPatterns.has(toCanonicalRoutePattern([...parentPath, candidate]))) {
656
+ candidate = `${baseRouteName}-${suffix}`;
657
+ suffix += 1;
658
+ }
659
+ return candidate;
660
+ }
661
+ function normalizeRouteName(value) {
662
+ return (value
663
+ .trim()
664
+ .toLowerCase()
665
+ .replace(/[^a-z0-9]+/g, '-')
666
+ .replace(/^-+|-+$/g, '') || 'screen');
667
+ }
668
+ function cloneNodeWithNewIds(node, createId) {
669
+ const clonedNode = {
670
+ ...node,
671
+ id: createId(node.type),
672
+ props: node.props ? { ...node.props } : node.props,
673
+ };
674
+ if (node.children) {
675
+ clonedNode.children = node.children.map((child) => cloneNodeWithNewIds(child, createId));
676
+ }
677
+ return clonedNode;
678
+ }
679
+ function updateNodeInManifestTree(root, id, newProps) {
680
+ if (root.id === id) {
681
+ const { alias, style, ...rest } = newProps;
682
+ const aliasUpdate = typeof alias === 'string' ? { alias } : {};
683
+ const styleUpdate = isStyleRecord(style) ? { style } : {};
684
+ return {
685
+ ...root,
686
+ ...aliasUpdate,
687
+ ...styleUpdate,
688
+ props: { ...(root.props ?? {}), ...rest },
689
+ };
690
+ }
691
+ if (!root.children)
692
+ return root;
693
+ const nextChildren = root.children.map((child) => updateNodeInManifestTree(child, id, newProps));
694
+ const hasChanged = nextChildren.some((child, index) => child !== root.children?.[index]);
695
+ return hasChanged ? { ...root, children: nextChildren } : root;
696
+ }
697
+ function removeNodeFromManifestTree(root, nodeId) {
698
+ if (root.id === nodeId)
699
+ return null;
700
+ if (!root.children)
701
+ return root;
702
+ const filteredChildren = root.children.filter((child) => child.id !== nodeId);
703
+ if (filteredChildren.length !== root.children.length) {
704
+ return { ...root, children: filteredChildren };
705
+ }
706
+ const nextChildren = root.children.map((child) => removeNodeFromManifestTree(child, nodeId) ?? child);
707
+ const hasChanged = nextChildren.some((child, index) => child !== root.children?.[index]);
708
+ return hasChanged ? { ...root, children: nextChildren } : root;
709
+ }
710
+ function moveNodeInManifestTree(root, nodeId, direction) {
711
+ if (root.id === nodeId || !root.children)
712
+ return root;
713
+ const index = root.children.findIndex((child) => child.id === nodeId);
714
+ if (index !== -1) {
715
+ const targetIndex = direction === 'up' ? index - 1 : index + 1;
716
+ if (targetIndex < 0 || targetIndex >= root.children.length)
717
+ return root;
718
+ const currentNode = root.children[index];
719
+ const targetNode = root.children[targetIndex];
720
+ if (!currentNode || !targetNode)
721
+ return root;
722
+ const nextChildren = [...root.children];
723
+ nextChildren[index] = targetNode;
724
+ nextChildren[targetIndex] = currentNode;
725
+ return { ...root, children: nextChildren };
726
+ }
727
+ const nextChildren = root.children.map((child) => moveNodeInManifestTree(child, nodeId, direction));
728
+ const hasChanged = nextChildren.some((child, childIndex) => child !== root.children?.[childIndex]);
729
+ return hasChanged ? { ...root, children: nextChildren } : root;
730
+ }
731
+ function findNodeWithParent(root, nodeId) {
732
+ if (root.id === nodeId)
733
+ return { node: root, parent: null, index: -1 };
734
+ const visit = (node) => {
735
+ const children = node.children ?? [];
736
+ for (const [index, child] of children.entries()) {
737
+ if (child.id === nodeId)
738
+ return { node: child, parent: node, index };
739
+ const nested = visit(child);
740
+ if (nested)
741
+ return nested;
742
+ }
743
+ return null;
744
+ };
745
+ return visit(root);
746
+ }
747
+ function validateManifestNodePlacement(root, placement, childType, componentMeta) {
748
+ const parent = findNodeInManifest(root, placement.parentId);
749
+ if (!parent)
750
+ return false;
751
+ const meta = componentMeta[parent.type];
752
+ if (!meta?.allowedChildren.includes(childType))
753
+ return false;
754
+ const children = parent.children ?? [];
755
+ if (placement.referenceId && !children.some((child) => child.id === placement.referenceId)) {
756
+ return false;
757
+ }
758
+ return placement.index >= 0 && placement.index <= children.length;
759
+ }
760
+ function insertChildAtIndex(args) {
761
+ const { node, parentId, index, newNode } = args;
762
+ if (node.id === parentId) {
763
+ const children = node.children ?? [];
764
+ if (index < 0 || index > children.length)
765
+ return { node, inserted: false };
766
+ return {
767
+ node: {
768
+ ...node,
769
+ children: [...children.slice(0, index), newNode, ...children.slice(index)],
770
+ },
771
+ inserted: true,
772
+ };
773
+ }
774
+ if (!node.children?.length)
775
+ return { node, inserted: false };
776
+ const results = node.children.map((child) => insertChildAtIndex({ node: child, parentId, index, newNode }));
777
+ const inserted = results.some((result) => result.inserted);
778
+ return inserted
779
+ ? { node: { ...node, children: results.map((result) => result.node) }, inserted }
780
+ : { node, inserted };
781
+ }
782
+ function isDescendantNode(node, descendantId) {
783
+ for (const child of node.children ?? []) {
784
+ if (child.id === descendantId || isDescendantNode(child, descendantId))
785
+ return true;
786
+ }
787
+ return false;
788
+ }
789
+ function removeNodeForMove(args) {
790
+ const { node, nodeId } = args;
791
+ const children = node.children ?? [];
792
+ const directIndex = children.findIndex((child) => child.id === nodeId);
793
+ if (directIndex !== -1) {
794
+ const removedNode = children[directIndex];
795
+ if (!removedNode)
796
+ return { node, removedNode: null };
797
+ return {
798
+ node: {
799
+ ...node,
800
+ children: children.filter((child) => child.id !== nodeId),
801
+ },
802
+ removedNode,
803
+ };
804
+ }
805
+ const nextChildren = [];
806
+ let removedNode = null;
807
+ for (const child of children) {
808
+ if (removedNode) {
809
+ nextChildren.push(child);
810
+ continue;
811
+ }
812
+ const { node: nextChild, removedNode: nextRemovedNode } = removeNodeForMove({
813
+ node: child,
814
+ nodeId,
815
+ });
816
+ if (nextRemovedNode)
817
+ removedNode = nextRemovedNode;
818
+ nextChildren.push(nextChild);
819
+ }
820
+ if (!removedNode)
821
+ return { node, removedNode: null };
822
+ return { node: { ...node, children: nextChildren }, removedNode };
823
+ }
824
+ function adjustMovePlacement(args) {
825
+ const { source, placement } = args;
826
+ if (!source.parent)
827
+ return null;
828
+ if (placement.referenceId === source.node.id)
829
+ return null;
830
+ if (placement.parentId !== source.parent.id)
831
+ return placement;
832
+ const adjustedIndex = source.index < placement.index ? placement.index - 1 : placement.index;
833
+ if (adjustedIndex === source.index)
834
+ return null;
835
+ return { ...placement, index: adjustedIndex };
836
+ }
837
+ function normalizeNavigatorAfterRouteUpdate(navigator) {
838
+ const nextRoutes = navigator.routes;
839
+ let nextInitialRouteName = navigator.initialRouteName;
840
+ if (nextRoutes.length === 0) {
841
+ nextInitialRouteName = undefined;
842
+ }
843
+ else if (nextInitialRouteName &&
844
+ !nextRoutes.some((route) => route.name === nextInitialRouteName)) {
845
+ nextInitialRouteName = nextRoutes[0]?.name;
846
+ }
847
+ if (nextInitialRouteName === undefined) {
848
+ const { initialRouteName: _omit, ...restNavigator } = navigator;
849
+ return { ...restNavigator, routes: nextRoutes };
850
+ }
851
+ return { ...navigator, routes: nextRoutes, initialRouteName: nextInitialRouteName };
852
+ }
853
+ function isStyleRecord(value) {
854
+ if (typeof value !== 'object' || value === null || Array.isArray(value))
855
+ return false;
856
+ return Object.values(value).every((entry) => typeof entry === 'string' || typeof entry === 'number');
857
+ }
858
+ //# sourceMappingURL=manifestState.js.map