@wildo-ai/presets-components-3d 1.1.6

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 (102) hide show
  1. package/LICENSE +34 -0
  2. package/dist/esm/frame/frame-3d-style.d.ts +156 -0
  3. package/dist/esm/frame/frame-3d-style.d.ts.map +1 -0
  4. package/dist/esm/frame/frame-3d-style.js +229 -0
  5. package/dist/esm/frame/frame-3d-style.js.map +1 -0
  6. package/dist/esm/frame/frame-appearance.d.ts +99 -0
  7. package/dist/esm/frame/frame-appearance.d.ts.map +1 -0
  8. package/dist/esm/frame/frame-appearance.js +207 -0
  9. package/dist/esm/frame/frame-appearance.js.map +1 -0
  10. package/dist/esm/frame/frame-canvas.d.ts +63 -0
  11. package/dist/esm/frame/frame-canvas.d.ts.map +1 -0
  12. package/dist/esm/frame/frame-canvas.js +134 -0
  13. package/dist/esm/frame/frame-canvas.js.map +1 -0
  14. package/dist/esm/frame/frame-canvas.lazy.d.ts +21 -0
  15. package/dist/esm/frame/frame-canvas.lazy.d.ts.map +1 -0
  16. package/dist/esm/frame/frame-canvas.lazy.js +31 -0
  17. package/dist/esm/frame/frame-canvas.lazy.js.map +1 -0
  18. package/dist/esm/frame/frame-color.d.ts +24 -0
  19. package/dist/esm/frame/frame-color.d.ts.map +1 -0
  20. package/dist/esm/frame/frame-color.js +52 -0
  21. package/dist/esm/frame/frame-color.js.map +1 -0
  22. package/dist/esm/frame/frame-driver.d.ts +60 -0
  23. package/dist/esm/frame/frame-driver.d.ts.map +1 -0
  24. package/dist/esm/frame/frame-driver.js +207 -0
  25. package/dist/esm/frame/frame-driver.js.map +1 -0
  26. package/dist/esm/frame/frame-renderer.d.ts +141 -0
  27. package/dist/esm/frame/frame-renderer.d.ts.map +1 -0
  28. package/dist/esm/frame/frame-renderer.js +635 -0
  29. package/dist/esm/frame/frame-renderer.js.map +1 -0
  30. package/dist/esm/frame/frame-shaders.d.ts +30 -0
  31. package/dist/esm/frame/frame-shaders.d.ts.map +1 -0
  32. package/dist/esm/frame/frame-shaders.js +97 -0
  33. package/dist/esm/frame/frame-shaders.js.map +1 -0
  34. package/dist/esm/frame/use-frame-appearance.d.ts +17 -0
  35. package/dist/esm/frame/use-frame-appearance.d.ts.map +1 -0
  36. package/dist/esm/frame/use-frame-appearance.js +38 -0
  37. package/dist/esm/frame/use-frame-appearance.js.map +1 -0
  38. package/dist/esm/index.d.ts +14 -0
  39. package/dist/esm/index.d.ts.map +1 -0
  40. package/dist/esm/index.js +14 -0
  41. package/dist/esm/index.js.map +1 -0
  42. package/dist/esm/layout/header-first-shell-container.d.ts +22 -0
  43. package/dist/esm/layout/header-first-shell-container.d.ts.map +1 -0
  44. package/dist/esm/layout/header-first-shell-container.js +31 -0
  45. package/dist/esm/layout/header-first-shell-container.js.map +1 -0
  46. package/dist/esm/layout/layout-3d-definition.d.ts +54 -0
  47. package/dist/esm/layout/layout-3d-definition.d.ts.map +1 -0
  48. package/dist/esm/layout/layout-3d-definition.js +106 -0
  49. package/dist/esm/layout/layout-3d-definition.js.map +1 -0
  50. package/dist/esm/layout/layout-3d-transition-choreography.d.ts +30 -0
  51. package/dist/esm/layout/layout-3d-transition-choreography.d.ts.map +1 -0
  52. package/dist/esm/layout/layout-3d-transition-choreography.js +43 -0
  53. package/dist/esm/layout/layout-3d-transition-choreography.js.map +1 -0
  54. package/dist/esm/layout/layout-3d-vocabulary.d.ts +118 -0
  55. package/dist/esm/layout/layout-3d-vocabulary.d.ts.map +1 -0
  56. package/dist/esm/layout/layout-3d-vocabulary.js +104 -0
  57. package/dist/esm/layout/layout-3d-vocabulary.js.map +1 -0
  58. package/dist/esm/layout/shell-layout-placement.d.ts +26 -0
  59. package/dist/esm/layout/shell-layout-placement.d.ts.map +1 -0
  60. package/dist/esm/layout/shell-layout-placement.js +18 -0
  61. package/dist/esm/layout/shell-layout-placement.js.map +1 -0
  62. package/dist/esm/layout-3d-main-container.d.ts +73 -0
  63. package/dist/esm/layout-3d-main-container.d.ts.map +1 -0
  64. package/dist/esm/layout-3d-main-container.js +124 -0
  65. package/dist/esm/layout-3d-main-container.js.map +1 -0
  66. package/dist/esm/register-3d-layout.d.ts +40 -0
  67. package/dist/esm/register-3d-layout.d.ts.map +1 -0
  68. package/dist/esm/register-3d-layout.js +72 -0
  69. package/dist/esm/register-3d-layout.js.map +1 -0
  70. package/dist/esm/shell-region-measurement.d.ts +75 -0
  71. package/dist/esm/shell-region-measurement.d.ts.map +1 -0
  72. package/dist/esm/shell-region-measurement.js +75 -0
  73. package/dist/esm/shell-region-measurement.js.map +1 -0
  74. package/dist/esm/spatial/spatial-camera.d.ts +106 -0
  75. package/dist/esm/spatial/spatial-camera.d.ts.map +1 -0
  76. package/dist/esm/spatial/spatial-camera.js +151 -0
  77. package/dist/esm/spatial/spatial-camera.js.map +1 -0
  78. package/dist/esm/spatial/spatial-environment.d.ts +6 -0
  79. package/dist/esm/spatial/spatial-environment.d.ts.map +1 -0
  80. package/dist/esm/spatial/spatial-environment.js +166 -0
  81. package/dist/esm/spatial/spatial-environment.js.map +1 -0
  82. package/dist/esm/spatial/spatial-environment.lazy.d.ts +10 -0
  83. package/dist/esm/spatial/spatial-environment.lazy.d.ts.map +1 -0
  84. package/dist/esm/spatial/spatial-environment.lazy.js +13 -0
  85. package/dist/esm/spatial/spatial-environment.lazy.js.map +1 -0
  86. package/dist/esm/spatial/spatial-shell-container.d.ts +21 -0
  87. package/dist/esm/spatial/spatial-shell-container.d.ts.map +1 -0
  88. package/dist/esm/spatial/spatial-shell-container.js +231 -0
  89. package/dist/esm/spatial/spatial-shell-container.js.map +1 -0
  90. package/dist/esm/spatial/spatial-stage-store.d.ts +27 -0
  91. package/dist/esm/spatial/spatial-stage-store.d.ts.map +1 -0
  92. package/dist/esm/spatial/spatial-stage-store.js +33 -0
  93. package/dist/esm/spatial/spatial-stage-store.js.map +1 -0
  94. package/dist/esm/tokens/design-token-resolution.d.ts +110 -0
  95. package/dist/esm/tokens/design-token-resolution.d.ts.map +1 -0
  96. package/dist/esm/tokens/design-token-resolution.js +410 -0
  97. package/dist/esm/tokens/design-token-resolution.js.map +1 -0
  98. package/dist/esm/tokens/use-design-token-resolution.d.ts +22 -0
  99. package/dist/esm/tokens/use-design-token-resolution.d.ts.map +1 -0
  100. package/dist/esm/tokens/use-design-token-resolution.js +49 -0
  101. package/dist/esm/tokens/use-design-token-resolution.js.map +1 -0
  102. package/package.json +61 -0
@@ -0,0 +1,124 @@
1
+ import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { useEffect } from 'react';
3
+ import { useNavigationState, useUI } from '@wildo-ai/saas-frontend-lib';
4
+ import { FrameCanvasDoor } from './frame/frame-canvas.lazy.js';
5
+ import { assertSupportedLayout3D, INHERIT_LAYOUT_3D } from './layout/layout-3d-definition.js';
6
+ import { crossFadesAppearance } from './layout/layout-3d-transition-choreography.js';
7
+ import { Layout3DCamera } from './layout/layout-3d-vocabulary.js';
8
+ import { ShellLayoutPlacementKind } from './layout/shell-layout-placement.js';
9
+ import { useFrameAppearance } from './frame/use-frame-appearance.js';
10
+ /**
11
+ * The brand a 3D main container carries. `Symbol.for`, not `Symbol`: the component registry lives on
12
+ * `globalThis` and survives a hot reload, while a reloaded module would mint a new `Symbol` and fail to
13
+ * recognise the container it registered last time — and then capture it as "the 2D shell".
14
+ */
15
+ const LAYOUT_3D_MAIN_CONTAINER_BRAND = Symbol.for('@wildo-ai/presets-components-3d/main-container');
16
+ /** Whether a registered main container is a 3D one — and so carries the 2D container it wraps. */
17
+ export function isLayout3DMainContainer(component) {
18
+ return typeof component === 'function' && component[LAYOUT_3D_MAIN_CONTAINER_BRAND] === true;
19
+ }
20
+ let appearanceMismatchWarned = false;
21
+ /**
22
+ * How long a shell appearance other than the one the layout requires must persist before it is reported. The UI
23
+ * context starts from the DEFAULT theme and resolves the application's own theme a moment later, so the first value
24
+ * this container sees is routinely SEPARATED even in a CONTINUOUS application — measured on Wonder Todos, whose
25
+ * shell renders `bg-transparent` while the first-render value read SEPARATED. Only a value that survives
26
+ * bootstrap is the application's.
27
+ */
28
+ const APPEARANCE_MISMATCH_REPORT_DELAY_MS = 5000;
29
+ /**
30
+ * Build the 3D main container: a captured 2D container, and a layout that either renders it (INHERIT) or arranges
31
+ * the regions with a container of its own (COMPOSED).
32
+ *
33
+ * It renders exactly two children, at FIXED positions:
34
+ *
35
+ * - **index 0, the frame slot** ({@link FrameSlot}): the decorative canvas behind its lazy door (`FrameCanvasDoor`),
36
+ * whose `LazyDoorErrorBoundary` contains any failure of it. The theme and the navigation state are read in the
37
+ * slot, in the DOM tree, and passed to the canvas as props: a theme, mode, breakpoint or navigation change
38
+ * re-renders index 0 and never touches index 1.
39
+ * It is `position: fixed` with NO z-index, `aria-hidden` and `pointer-events: none`.
40
+ * - **index 1, the placement**: the captured 2D container, unchanged, for an INHERIT layout; the layout's own
41
+ * container, composed from the same shell building blocks, otherwise. Either way every region, provider,
42
+ * context, portal and latch is the application's own.
43
+ *
44
+ * Why no wrapper, and no `isolate` (the #1821 text proposed `<div class="isolate">` with the canvas at
45
+ * `-z-10`): `isolate` would make the whole shell ONE stacking context, so anything positioned OUTSIDE it
46
+ * with a lower z-index — the consent prompt (`z-50`) — would paint over the shell's own dialogs
47
+ * (`--z-overlay`, 300). #1830 measured that exact reordering and chose `relative` for the 2D shell root
48
+ * for that reason. Instead, a positioned element with no z-index paints at the z-index:0 level in TREE
49
+ * order, which puts the canvas after the theme's background layer (`UIContext`'s `isolate` wrapper,
50
+ * earlier in the tree) and before the shell root (`relative`, the next sibling) — frame above the theme
51
+ * background, shell above the frame — while every z-index inside the shell keeps its global order.
52
+ *
53
+ * The precondition this places on the placement: its ROOT must be positioned (the 2D default and the
54
+ * header-first container are `relative`, #1830), or its in-flow content paints at a lower level than the frame and
55
+ * under it. The frame never takes a click either way — it handles no pointer events at all.
56
+ *
57
+ * Why fixed positions: a canvas failure empties only index 0. The placement never moves and keeps one component
58
+ * type for the application's life, so it never remounts — a remount there would unmount every navigation zone and
59
+ * lose its state.
60
+ */
61
+ export function create3DMainContainer(captured, options = {}) {
62
+ const layout = options.layout ?? INHERIT_LAYOUT_3D;
63
+ const frameStyles = options.frameStyles;
64
+ // A camera, transition kind or rest pose the renderer does not implement is refused here, at registration.
65
+ assertSupportedLayout3D(layout);
66
+ const crossFadeAppearance = crossFadesAppearance(layout.transitions.appearance);
67
+ // Chosen ONCE, outside the component: the element at index 1 keeps one type for the application's life, so it
68
+ // never remounts.
69
+ const Placement = layout.placement.kind === ShellLayoutPlacementKind.INHERIT ? captured : layout.placement.container;
70
+ function Layout3DMainContainer(props) {
71
+ const { shellAppearance } = useUI();
72
+ const required = layout.constraints.requiredShellAppearance;
73
+ useEffect(() => {
74
+ // The frame is visible only where regions paint nothing. A layout states the shell appearance it is designed
75
+ // for; any other is the application's choice to correct, so it is said once and nothing switches.
76
+ if (required === null || shellAppearance === required || appearanceMismatchWarned)
77
+ return undefined;
78
+ const report = setTimeout(() => {
79
+ // Checked again here: several containers (or a remount) may have armed a timer before the first fired.
80
+ if (appearanceMismatchWarned)
81
+ return;
82
+ appearanceMismatchWarned = true;
83
+ console.warn(`[presets-components-3d] the 3D layout '${layout.id}' is designed for a '${String(required)}' shell, and this theme's `
84
+ + `shell appearance is '${String(shellAppearance)}': opaque regions will cover the frame drawn under them.`);
85
+ }, APPEARANCE_MISMATCH_REPORT_DELAY_MS);
86
+ return () => clearTimeout(report);
87
+ }, [shellAppearance, required]);
88
+ return (_jsxs(_Fragment, { children: [layout.scene.camera === Layout3DCamera.FLAT_ORTHOGRAPHIC && _jsx(FrameSlot, { layout: layout, frameStyles: frameStyles, crossFadeAppearance: crossFadeAppearance }), _jsx(Placement, { ...props })] }));
89
+ }
90
+ return Object.assign(Layout3DMainContainer, {
91
+ [LAYOUT_3D_MAIN_CONTAINER_BRAND]: true,
92
+ captured,
93
+ layout,
94
+ frameStyles,
95
+ });
96
+ }
97
+ /**
98
+ * Index 0 of a 3D main container: reads what the frame follows — the theme (`useFrameAppearance`) and the navigation
99
+ * state (#1826) — and hands it to the canvas. Its own component, so a navigation, which changes the navigation state
100
+ * many times a second during some flows, re-renders this slot and never the placement beside it.
101
+ *
102
+ * The navigation signature is every structural zone's top entry (`getTopEntry`, so a tabbed zone reports its visible
103
+ * tab), identified by its URL PATH:
104
+ * - not by its id, which is minted again on every push or replace, so a route sync that re-lands the SAME place would
105
+ * read as a navigation nobody made (the id stands in only for an entry without a URL);
106
+ * - not by its query string, which carries VIEW state on the same content (`?section=`, `?dataMode=`, which the
107
+ * engine updates in place as "same id, same content"): switching a board's section tab or a list's mode is not
108
+ * going somewhere, and does not play.
109
+ * The route sync hooks mirror router navigations into those stacks, so a sidebar link and a record opened beside a
110
+ * list both change it; the overlay zone is not a place and does not. While no zone holds anything yet, the signature
111
+ * is empty, which the frame never records (see `FrameDriver.setNavigation`).
112
+ */
113
+ function FrameSlot({ layout, frameStyles, crossFadeAppearance, }) {
114
+ const appearance = useFrameAppearance(layout.scene.frameStyles, frameStyles);
115
+ const navigation = useNavigationState();
116
+ const zones = Object.keys(navigation.state.zones);
117
+ const places = zones.map((zone) => {
118
+ const top = navigation.getTopEntry(zone);
119
+ return top ? (top.urlPath?.split(/[?#]/)[0] ?? top.id) : '';
120
+ });
121
+ const navigationSignature = places.some((place) => place !== '') ? zones.map((zone, index) => `${zone}:${places[index]}`).join('|') : '';
122
+ return (_jsx(FrameCanvasDoor, { appearance: appearance, camera: layout.scene.camera, crossFadeAppearance: crossFadeAppearance, navigationTransition: layout.transitions.navigation, sidebarResizeTransition: layout.transitions.sidebarResize, zoneFocusTransition: layout.transitions.zoneFocus, navigationSignature: navigationSignature, focusedZone: navigation.focusedZone }));
123
+ }
124
+ //# sourceMappingURL=layout-3d-main-container.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"layout-3d-main-container.js","sourceRoot":"","sources":["../../../src/layout-3d-main-container.tsx"],"names":[],"mappings":"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","sourcesContent":["import React, { useEffect } from 'react';\n\nimport { useNavigationState, useUI, type AppLayoutPreset_MainContainer_DefaultProps } from '@wildo-ai/saas-frontend-lib';\n\nimport { FrameCanvasDoor } from './frame/frame-canvas.lazy';\nimport type { Frame3DStyleOverrides } from './frame/frame-3d-style';\nimport { assertSupportedLayout3D, INHERIT_LAYOUT_3D, type Layout3DDefinition } from './layout/layout-3d-definition';\nimport { crossFadesAppearance } from './layout/layout-3d-transition-choreography';\nimport { Layout3DCamera } from './layout/layout-3d-vocabulary';\nimport { ShellLayoutPlacementKind } from './layout/shell-layout-placement';\nimport { useFrameAppearance } from './frame/use-frame-appearance';\n\n/**\n * The brand a 3D main container carries. `Symbol.for`, not `Symbol`: the component registry lives on\n * `globalThis` and survives a hot reload, while a reloaded module would mint a new `Symbol` and fail to\n * recognise the container it registered last time — and then capture it as \"the 2D shell\".\n */\nconst LAYOUT_3D_MAIN_CONTAINER_BRAND = Symbol.for('@wildo-ai/presets-components-3d/main-container');\n\ntype MainContainerComponent = React.ComponentType<AppLayoutPreset_MainContainer_DefaultProps>;\n\n/** @wildo_source:part:start presets.3d.register-layout facet:layer:frontend facet:family:layout-3d facet:audience:app-developer */\n/** What a 3D main container draws: the layout, and the application's frame-style overrides. */\nexport interface Layout3DOptions {\n /**\n * The layout: its disposal, its scene and its transitions. Defaults to {@link INHERIT_LAYOUT_3D}, the 2D disposal\n * with the frame under it. An application uses ONE layout for its whole life.\n */\n layout?: Layout3DDefinition;\n /** Frame-style overrides layered over the layout's built-in styles. */\n frameStyles?: Frame3DStyleOverrides;\n}\n/** @wildo_source:part:end presets.3d.register-layout */\n\n/** A 3D main container: the component, the 2D container it captured, and the layout it draws. */\nexport type Layout3DMainContainer = MainContainerComponent & {\n readonly [LAYOUT_3D_MAIN_CONTAINER_BRAND]: true;\n /**\n * The 2D main container captured at registration: rendered unchanged by an INHERIT placement, and kept by every\n * layout so a re-registration wraps IT rather than a 3D container.\n */\n readonly captured: MainContainerComponent;\n /** The layout this container draws. */\n readonly layout: Layout3DDefinition;\n /** The application's frame-style overrides, when it registered any. */\n readonly frameStyles: Frame3DStyleOverrides | undefined;\n};\n\n/** Whether a registered main container is a 3D one — and so carries the 2D container it wraps. */\nexport function isLayout3DMainContainer(component: unknown): component is Layout3DMainContainer {\n return typeof component === 'function' && (component as Partial<Layout3DMainContainer>)[LAYOUT_3D_MAIN_CONTAINER_BRAND] === true;\n}\n\nlet appearanceMismatchWarned = false;\n\n/**\n * How long a shell appearance other than the one the layout requires must persist before it is reported. The UI\n * context starts from the DEFAULT theme and resolves the application's own theme a moment later, so the first value\n * this container sees is routinely SEPARATED even in a CONTINUOUS application — measured on Wonder Todos, whose\n * shell renders `bg-transparent` while the first-render value read SEPARATED. Only a value that survives\n * bootstrap is the application's.\n */\nconst APPEARANCE_MISMATCH_REPORT_DELAY_MS = 5000;\n\n/**\n * Build the 3D main container: a captured 2D container, and a layout that either renders it (INHERIT) or arranges\n * the regions with a container of its own (COMPOSED).\n *\n * It renders exactly two children, at FIXED positions:\n *\n * - **index 0, the frame slot** ({@link FrameSlot}): the decorative canvas behind its lazy door (`FrameCanvasDoor`),\n * whose `LazyDoorErrorBoundary` contains any failure of it. The theme and the navigation state are read in the\n * slot, in the DOM tree, and passed to the canvas as props: a theme, mode, breakpoint or navigation change\n * re-renders index 0 and never touches index 1.\n * It is `position: fixed` with NO z-index, `aria-hidden` and `pointer-events: none`.\n * - **index 1, the placement**: the captured 2D container, unchanged, for an INHERIT layout; the layout's own\n * container, composed from the same shell building blocks, otherwise. Either way every region, provider,\n * context, portal and latch is the application's own.\n *\n * Why no wrapper, and no `isolate` (the #1821 text proposed `<div class=\"isolate\">` with the canvas at\n * `-z-10`): `isolate` would make the whole shell ONE stacking context, so anything positioned OUTSIDE it\n * with a lower z-index — the consent prompt (`z-50`) — would paint over the shell's own dialogs\n * (`--z-overlay`, 300). #1830 measured that exact reordering and chose `relative` for the 2D shell root\n * for that reason. Instead, a positioned element with no z-index paints at the z-index:0 level in TREE\n * order, which puts the canvas after the theme's background layer (`UIContext`'s `isolate` wrapper,\n * earlier in the tree) and before the shell root (`relative`, the next sibling) — frame above the theme\n * background, shell above the frame — while every z-index inside the shell keeps its global order.\n *\n * The precondition this places on the placement: its ROOT must be positioned (the 2D default and the\n * header-first container are `relative`, #1830), or its in-flow content paints at a lower level than the frame and\n * under it. The frame never takes a click either way — it handles no pointer events at all.\n *\n * Why fixed positions: a canvas failure empties only index 0. The placement never moves and keeps one component\n * type for the application's life, so it never remounts — a remount there would unmount every navigation zone and\n * lose its state.\n */\nexport function create3DMainContainer(captured: MainContainerComponent, options: Layout3DOptions = {}): Layout3DMainContainer {\n const layout = options.layout ?? INHERIT_LAYOUT_3D;\n const frameStyles = options.frameStyles;\n // A camera, transition kind or rest pose the renderer does not implement is refused here, at registration.\n assertSupportedLayout3D(layout);\n const crossFadeAppearance = crossFadesAppearance(layout.transitions.appearance);\n // Chosen ONCE, outside the component: the element at index 1 keeps one type for the application's life, so it\n // never remounts.\n const Placement: MainContainerComponent = layout.placement.kind === ShellLayoutPlacementKind.INHERIT ? captured : layout.placement.container;\n function Layout3DMainContainer(props: AppLayoutPreset_MainContainer_DefaultProps): React.JSX.Element {\n const { shellAppearance } = useUI();\n const required = layout.constraints.requiredShellAppearance;\n\n useEffect(() => {\n // The frame is visible only where regions paint nothing. A layout states the shell appearance it is designed\n // for; any other is the application's choice to correct, so it is said once and nothing switches.\n if (required === null || shellAppearance === required || appearanceMismatchWarned) return undefined;\n const report = setTimeout(() => {\n // Checked again here: several containers (or a remount) may have armed a timer before the first fired.\n if (appearanceMismatchWarned) return;\n appearanceMismatchWarned = true;\n console.warn(\n `[presets-components-3d] the 3D layout '${layout.id}' is designed for a '${String(required)}' shell, and this theme's `\n + `shell appearance is '${String(shellAppearance)}': opaque regions will cover the frame drawn under them.`,\n );\n }, APPEARANCE_MISMATCH_REPORT_DELAY_MS);\n return () => clearTimeout(report);\n }, [shellAppearance, required]);\n\n return (\n <>\n {/* A spatial layout's placement draws its own environment; the flat frame belongs to the flat camera only. */}\n {layout.scene.camera === Layout3DCamera.FLAT_ORTHOGRAPHIC && <FrameSlot layout={layout} frameStyles={frameStyles} crossFadeAppearance={crossFadeAppearance} />}\n <Placement {...props} />\n </>\n );\n }\n\n return Object.assign(Layout3DMainContainer, {\n [LAYOUT_3D_MAIN_CONTAINER_BRAND]: true as const,\n captured,\n layout,\n frameStyles,\n });\n}\n\n/**\n * Index 0 of a 3D main container: reads what the frame follows — the theme (`useFrameAppearance`) and the navigation\n * state (#1826) — and hands it to the canvas. Its own component, so a navigation, which changes the navigation state\n * many times a second during some flows, re-renders this slot and never the placement beside it.\n *\n * The navigation signature is every structural zone's top entry (`getTopEntry`, so a tabbed zone reports its visible\n * tab), identified by its URL PATH:\n * - not by its id, which is minted again on every push or replace, so a route sync that re-lands the SAME place would\n * read as a navigation nobody made (the id stands in only for an entry without a URL);\n * - not by its query string, which carries VIEW state on the same content (`?section=`, `?dataMode=`, which the\n * engine updates in place as \"same id, same content\"): switching a board's section tab or a list's mode is not\n * going somewhere, and does not play.\n * The route sync hooks mirror router navigations into those stacks, so a sidebar link and a record opened beside a\n * list both change it; the overlay zone is not a place and does not. While no zone holds anything yet, the signature\n * is empty, which the frame never records (see `FrameDriver.setNavigation`).\n */\nfunction FrameSlot({\n layout,\n frameStyles,\n crossFadeAppearance,\n}: {\n layout: Layout3DDefinition;\n frameStyles: Frame3DStyleOverrides | undefined;\n crossFadeAppearance: boolean;\n}): React.JSX.Element {\n const appearance = useFrameAppearance(layout.scene.frameStyles, frameStyles);\n const navigation = useNavigationState();\n const zones = Object.keys(navigation.state.zones) as Array<keyof typeof navigation.state.zones>;\n const places = zones.map((zone) => {\n const top = navigation.getTopEntry(zone);\n return top ? (top.urlPath?.split(/[?#]/)[0] ?? top.id) : '';\n });\n const navigationSignature = places.some((place) => place !== '') ? zones.map((zone, index) => `${zone}:${places[index]}`).join('|') : '';\n return (\n <FrameCanvasDoor\n appearance={appearance}\n camera={layout.scene.camera}\n crossFadeAppearance={crossFadeAppearance}\n navigationTransition={layout.transitions.navigation}\n sidebarResizeTransition={layout.transitions.sidebarResize}\n zoneFocusTransition={layout.transitions.zoneFocus}\n navigationSignature={navigationSignature}\n focusedZone={navigation.focusedZone}\n />\n );\n}\n"]}
@@ -0,0 +1,40 @@
1
+ import { type FrontendComponentRegistration } from '@wildo-ai/saas-frontend-lib';
2
+ import { type Layout3DOptions } from './layout-3d-main-container';
3
+ /** @wildo_source:part:start presets.3d.register-layout facet:layer:frontend facet:family:layout-3d facet:audience:app-developer */
4
+ /**
5
+ * Make an application's signed-in shell 3D: the shell's regions — unchanged HTML, with all their contexts —
6
+ * arranged by a 3D layout, with a decorative WebGL frame drawn under them.
7
+ *
8
+ * There is no 2D/3D switch and no option to enable it. An application is 3D because it CALLS this, from
9
+ * its engine module's `componentRegistrations` — the entries `registerFrontendComponentRegistrations`
10
+ * runs AFTER the framework presets are registered, so the Default main container exists by then:
11
+ *
12
+ * ```ts
13
+ * import { register3DLayout } from '@wildo-ai/presets-components-3d';
14
+ *
15
+ * export const engineComponentRegistrations: FrontendComponentRegistration[] = [register3DLayout()];
16
+ * ```
17
+ *
18
+ * It registers under `Default` for `APP_LAYOUTS_MAIN_CONTAINER`, the only preset key the shell resolves,
19
+ * after capturing the container registered there before it. An INHERIT layout renders that captured container,
20
+ * unchanged; a layout with a disposal of its own renders its composed container instead, built from the same
21
+ * shell building blocks — either way every region, context and behaviour is the application's own.
22
+ *
23
+ * - **Refuses** when nothing is registered yet: capturing `null` would render no shell at all, and the
24
+ * cause — the registration running before the framework presets — is worth an error, not a blank app.
25
+ * - **Idempotent**: a registration that finds a 3D container already there (a second call, or the
26
+ * registry surviving a hot reload on `globalThis`) wraps the 2D container IT captured, never itself.
27
+ * - **Keeps the registration's metadata**: the captured container's `isConfigurable` is carried over, so
28
+ * nothing that reads the registry's metadata sees a difference.
29
+ *
30
+ * `options.layout` selects the layout (`INHERIT_LAYOUT_3D` by default); `options.frameStyles` layers an
31
+ * application's frame styles over the layout's own (per theme id, or for all themes).
32
+ *
33
+ * - **One layout per application.** A registration that finds a 3D container of a DIFFERENT layout already
34
+ * registered refuses: an application is not 3D in two ways, and a silent replacement would hide which one it
35
+ * is. The same layout again is idempotent. (A layout changed in code during a hot reload needs a full page
36
+ * reload, because the registry outlives the hot reload.)
37
+ */
38
+ export declare function register3DLayout(options?: Layout3DOptions): FrontendComponentRegistration;
39
+ /** @wildo_source:part:end presets.3d.register-layout */
40
+ //# sourceMappingURL=register-3d-layout.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"register-3d-layout.d.ts","sourceRoot":"","sources":["../../../src/register-3d-layout.ts"],"names":[],"mappings":"AACA,OAAO,EAA4B,KAAK,6BAA6B,EAAE,MAAM,6BAA6B,CAAC;AAE3G,OAAO,EAAkD,KAAK,eAAe,EAAE,MAAM,4BAA4B,CAAC;AAIlH,mIAAmI;AACnI;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAiCG;AACH,wBAAgB,gBAAgB,CAAC,OAAO,GAAE,eAAoB,GAAG,6BAA6B,CAiC7F;AACD,wDAAwD"}
@@ -0,0 +1,72 @@
1
+ import { CorePresetNames, FrontendComponentType } from '@wildo-ai/presets-components-models';
2
+ import { ComponentRegistryService } from '@wildo-ai/saas-frontend-lib';
3
+ import { create3DMainContainer, isLayout3DMainContainer } from './layout-3d-main-container.js';
4
+ import { BUILT_IN_LAYOUTS_3D, INHERIT_LAYOUT_3D } from './layout/layout-3d-definition.js';
5
+ import { Layout3DId } from './layout/layout-3d-vocabulary.js';
6
+ /** @wildo_source:part:start presets.3d.register-layout facet:layer:frontend facet:family:layout-3d facet:audience:app-developer */
7
+ /**
8
+ * Make an application's signed-in shell 3D: the shell's regions — unchanged HTML, with all their contexts —
9
+ * arranged by a 3D layout, with a decorative WebGL frame drawn under them.
10
+ *
11
+ * There is no 2D/3D switch and no option to enable it. An application is 3D because it CALLS this, from
12
+ * its engine module's `componentRegistrations` — the entries `registerFrontendComponentRegistrations`
13
+ * runs AFTER the framework presets are registered, so the Default main container exists by then:
14
+ *
15
+ * ```ts
16
+ * import { register3DLayout } from '@wildo-ai/presets-components-3d';
17
+ *
18
+ * export const engineComponentRegistrations: FrontendComponentRegistration[] = [register3DLayout()];
19
+ * ```
20
+ *
21
+ * It registers under `Default` for `APP_LAYOUTS_MAIN_CONTAINER`, the only preset key the shell resolves,
22
+ * after capturing the container registered there before it. An INHERIT layout renders that captured container,
23
+ * unchanged; a layout with a disposal of its own renders its composed container instead, built from the same
24
+ * shell building blocks — either way every region, context and behaviour is the application's own.
25
+ *
26
+ * - **Refuses** when nothing is registered yet: capturing `null` would render no shell at all, and the
27
+ * cause — the registration running before the framework presets — is worth an error, not a blank app.
28
+ * - **Idempotent**: a registration that finds a 3D container already there (a second call, or the
29
+ * registry surviving a hot reload on `globalThis`) wraps the 2D container IT captured, never itself.
30
+ * - **Keeps the registration's metadata**: the captured container's `isConfigurable` is carried over, so
31
+ * nothing that reads the registry's metadata sees a difference.
32
+ *
33
+ * `options.layout` selects the layout (`INHERIT_LAYOUT_3D` by default); `options.frameStyles` layers an
34
+ * application's frame styles over the layout's own (per theme id, or for all themes).
35
+ *
36
+ * - **One layout per application.** A registration that finds a 3D container of a DIFFERENT layout already
37
+ * registered refuses: an application is not 3D in two ways, and a silent replacement would hide which one it
38
+ * is. The same layout again is idempotent. (A layout changed in code during a hot reload needs a full page
39
+ * reload, because the registry outlives the hot reload.)
40
+ */
41
+ export function register3DLayout(options = {}) {
42
+ let capturedIsConfigurable = false;
43
+ return {
44
+ componentRef: FrontendComponentType.APP_LAYOUTS_MAIN_CONTAINER,
45
+ preset: CorePresetNames.DEFAULT,
46
+ get isConfigurable() {
47
+ return capturedIsConfigurable;
48
+ },
49
+ loadComponent: async () => {
50
+ const current = ComponentRegistryService.resolve(FrontendComponentType.APP_LAYOUTS_MAIN_CONTAINER, CorePresetNames.DEFAULT);
51
+ if (!current) {
52
+ throw new Error('register3DLayout(): no Default APP_LAYOUTS_MAIN_CONTAINER is registered to capture. Call it from the engine module\'s '
53
+ + '`componentRegistrations`, which run after the framework presets — not from a module evaluated before them.');
54
+ }
55
+ const layout = options.layout ?? INHERIT_LAYOUT_3D;
56
+ // An application's own layout may not borrow a built-in id: the one-layout check compares ids, and a borrowed
57
+ // one would make two different layouts look like the same.
58
+ if (Object.values(Layout3DId).includes(layout.id) && BUILT_IN_LAYOUTS_3D[layout.id] !== layout) {
59
+ throw new Error(`register3DLayout(): the layout id '${layout.id}' belongs to a built-in layout; give the application's own layout an id of its own.`);
60
+ }
61
+ if (isLayout3DMainContainer(current) && current.layout.id !== layout.id) {
62
+ throw new Error(`register3DLayout(): this application already registered the 3D layout '${current.layout.id}' and now asks for '${layout.id}'. `
63
+ + 'An application uses one layout: remove the other registration — or, if you just changed the layout in code, '
64
+ + 'reload the page (the component registry outlives a hot reload).');
65
+ }
66
+ capturedIsConfigurable = ComponentRegistryService.getMetadata(FrontendComponentType.APP_LAYOUTS_MAIN_CONTAINER, CorePresetNames.DEFAULT)?.isConfigurable ?? false;
67
+ return create3DMainContainer(isLayout3DMainContainer(current) ? current.captured : current, options);
68
+ },
69
+ };
70
+ }
71
+ /** @wildo_source:part:end presets.3d.register-layout */
72
+ //# sourceMappingURL=register-3d-layout.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"register-3d-layout.js","sourceRoot":"","sources":["../../../src/register-3d-layout.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,eAAe,EAAE,qBAAqB,EAAE,MAAM,qCAAqC,CAAC;AAC7F,OAAO,EAAE,wBAAwB,EAAsC,MAAM,6BAA6B,CAAC;AAE3G,OAAO,EAAE,qBAAqB,EAAE,uBAAuB,EAAwB,MAAM,4BAA4B,CAAC;AAClH,OAAO,EAAE,mBAAmB,EAAE,iBAAiB,EAAE,MAAM,+BAA+B,CAAC;AACvF,OAAO,EAAE,UAAU,EAAE,MAAM,+BAA+B,CAAC;AAE3D,mIAAmI;AACnI;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAiCG;AACH,MAAM,UAAU,gBAAgB,CAAC,UAA2B,EAAE;IAC5D,IAAI,sBAAsB,GAAG,KAAK,CAAC;IACnC,OAAO;QACL,YAAY,EAAE,qBAAqB,CAAC,0BAA0B;QAC9D,MAAM,EAAE,eAAe,CAAC,OAAO;QAC/B,IAAI,cAAc;YAChB,OAAO,sBAAsB,CAAC;QAChC,CAAC;QACD,aAAa,EAAE,KAAK,IAAI,EAAE;YACxB,MAAM,OAAO,GAAG,wBAAwB,CAAC,OAAO,CAAC,qBAAqB,CAAC,0BAA0B,EAAE,eAAe,CAAC,OAAO,CAAC,CAAC;YAC5H,IAAI,CAAC,OAAO,EAAE,CAAC;gBACb,MAAM,IAAI,KAAK,CACb,wHAAwH;sBACtH,4GAA4G,CAC/G,CAAC;YACJ,CAAC;YACD,MAAM,MAAM,GAAG,OAAO,CAAC,MAAM,IAAI,iBAAiB,CAAC;YACnD,8GAA8G;YAC9G,2DAA2D;YAC3D,IAAK,MAAM,CAAC,MAAM,CAAC,UAAU,CAAc,CAAC,QAAQ,CAAC,MAAM,CAAC,EAAE,CAAC,IAAI,mBAAmB,CAAC,MAAM,CAAC,EAAgB,CAAC,KAAK,MAAM,EAAE,CAAC;gBAC3H,MAAM,IAAI,KAAK,CAAC,sCAAsC,MAAM,CAAC,EAAE,qFAAqF,CAAC,CAAC;YACxJ,CAAC;YACD,IAAI,uBAAuB,CAAC,OAAO,CAAC,IAAI,OAAO,CAAC,MAAM,CAAC,EAAE,KAAK,MAAM,CAAC,EAAE,EAAE,CAAC;gBACxE,MAAM,IAAI,KAAK,CACb,0EAA0E,OAAO,CAAC,MAAM,CAAC,EAAE,uBAAuB,MAAM,CAAC,EAAE,KAAK;sBAC9H,8GAA8G;sBAC9G,iEAAiE,CACpE,CAAC;YACJ,CAAC;YACD,sBAAsB,GAAG,wBAAwB,CAAC,WAAW,CAAC,qBAAqB,CAAC,0BAA0B,EAAE,eAAe,CAAC,OAAO,CAAC,EAAE,cAAc,IAAI,KAAK,CAAC;YAClK,OAAO,qBAAqB,CAAC,uBAAuB,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC,QAAQ,CAAC,CAAC,CAAC,OAAO,EAAE,OAAO,CAAC,CAAC;QACvG,CAAC;KACF,CAAC;AACJ,CAAC;AACD,wDAAwD","sourcesContent":["import { CorePresetNames, FrontendComponentType } from '@wildo-ai/presets-components-models';\nimport { ComponentRegistryService, type FrontendComponentRegistration } from '@wildo-ai/saas-frontend-lib';\n\nimport { create3DMainContainer, isLayout3DMainContainer, type Layout3DOptions } from './layout-3d-main-container';\nimport { BUILT_IN_LAYOUTS_3D, INHERIT_LAYOUT_3D } from './layout/layout-3d-definition';\nimport { Layout3DId } from './layout/layout-3d-vocabulary';\n\n/** @wildo_source:part:start presets.3d.register-layout facet:layer:frontend facet:family:layout-3d facet:audience:app-developer */\n/**\n * Make an application's signed-in shell 3D: the shell's regions — unchanged HTML, with all their contexts —\n * arranged by a 3D layout, with a decorative WebGL frame drawn under them.\n *\n * There is no 2D/3D switch and no option to enable it. An application is 3D because it CALLS this, from\n * its engine module's `componentRegistrations` — the entries `registerFrontendComponentRegistrations`\n * runs AFTER the framework presets are registered, so the Default main container exists by then:\n *\n * ```ts\n * import { register3DLayout } from '@wildo-ai/presets-components-3d';\n *\n * export const engineComponentRegistrations: FrontendComponentRegistration[] = [register3DLayout()];\n * ```\n *\n * It registers under `Default` for `APP_LAYOUTS_MAIN_CONTAINER`, the only preset key the shell resolves,\n * after capturing the container registered there before it. An INHERIT layout renders that captured container,\n * unchanged; a layout with a disposal of its own renders its composed container instead, built from the same\n * shell building blocks — either way every region, context and behaviour is the application's own.\n *\n * - **Refuses** when nothing is registered yet: capturing `null` would render no shell at all, and the\n * cause — the registration running before the framework presets — is worth an error, not a blank app.\n * - **Idempotent**: a registration that finds a 3D container already there (a second call, or the\n * registry surviving a hot reload on `globalThis`) wraps the 2D container IT captured, never itself.\n * - **Keeps the registration's metadata**: the captured container's `isConfigurable` is carried over, so\n * nothing that reads the registry's metadata sees a difference.\n *\n * `options.layout` selects the layout (`INHERIT_LAYOUT_3D` by default); `options.frameStyles` layers an\n * application's frame styles over the layout's own (per theme id, or for all themes).\n *\n * - **One layout per application.** A registration that finds a 3D container of a DIFFERENT layout already\n * registered refuses: an application is not 3D in two ways, and a silent replacement would hide which one it\n * is. The same layout again is idempotent. (A layout changed in code during a hot reload needs a full page\n * reload, because the registry outlives the hot reload.)\n */\nexport function register3DLayout(options: Layout3DOptions = {}): FrontendComponentRegistration {\n let capturedIsConfigurable = false;\n return {\n componentRef: FrontendComponentType.APP_LAYOUTS_MAIN_CONTAINER,\n preset: CorePresetNames.DEFAULT,\n get isConfigurable(): boolean {\n return capturedIsConfigurable;\n },\n loadComponent: async () => {\n const current = ComponentRegistryService.resolve(FrontendComponentType.APP_LAYOUTS_MAIN_CONTAINER, CorePresetNames.DEFAULT);\n if (!current) {\n throw new Error(\n 'register3DLayout(): no Default APP_LAYOUTS_MAIN_CONTAINER is registered to capture. Call it from the engine module\\'s '\n + '`componentRegistrations`, which run after the framework presets — not from a module evaluated before them.',\n );\n }\n const layout = options.layout ?? INHERIT_LAYOUT_3D;\n // An application's own layout may not borrow a built-in id: the one-layout check compares ids, and a borrowed\n // one would make two different layouts look like the same.\n if ((Object.values(Layout3DId) as string[]).includes(layout.id) && BUILT_IN_LAYOUTS_3D[layout.id as Layout3DId] !== layout) {\n throw new Error(`register3DLayout(): the layout id '${layout.id}' belongs to a built-in layout; give the application's own layout an id of its own.`);\n }\n if (isLayout3DMainContainer(current) && current.layout.id !== layout.id) {\n throw new Error(\n `register3DLayout(): this application already registered the 3D layout '${current.layout.id}' and now asks for '${layout.id}'. `\n + 'An application uses one layout: remove the other registration — or, if you just changed the layout in code, '\n + 'reload the page (the component registry outlives a hot reload).',\n );\n }\n capturedIsConfigurable = ComponentRegistryService.getMetadata(FrontendComponentType.APP_LAYOUTS_MAIN_CONTAINER, CorePresetNames.DEFAULT)?.isConfigurable ?? false;\n return create3DMainContainer(isLayout3DMainContainer(current) ? current.captured : current, options);\n },\n };\n}\n/** @wildo_source:part:end presets.3d.register-layout */\n"]}
@@ -0,0 +1,75 @@
1
+ import { ShellRegion } from '@wildo-ai/presets-components-models';
2
+ /**
3
+ * One painted region of the signed-in shell, measured in CSS pixels relative to the viewport.
4
+ *
5
+ * The 3D frame only FOLLOWS the HTML shell: every size, collapse and breakpoint decision stays the 2D
6
+ * container's, and the frame reads the result off the DOM. So this is the frame's only input about the
7
+ * layout, and it carries nothing a renderer could mistake for a decision.
8
+ */
9
+ export interface MeasuredShellRegion {
10
+ region: ShellRegion;
11
+ /** The region's `data-placement` (a `HorizontalPlacement` / `VerticalPlacement` value), when it sits at an edge. */
12
+ placement: string | null;
13
+ left: number;
14
+ top: number;
15
+ width: number;
16
+ height: number;
17
+ /**
18
+ * The region already renders a glass material: its element carries `data-material-kind="glass"` (a region
19
+ * preset renders through `Surface asChild`, which puts the material attributes on the region element itself).
20
+ * The frame draws no slab under it — glass over a frosted slab would be two materials doing one job.
21
+ */
22
+ glass: boolean;
23
+ /** The element measured, so a caller can observe it for size changes. */
24
+ element: HTMLElement;
25
+ }
26
+ /**
27
+ * Every element marked as a shell region, whether or not it currently has a box — a region collapsed to
28
+ * nothing is still one to observe, so it is redrawn the moment it reappears. Same filtering as
29
+ * {@link measureShellRegions} otherwise.
30
+ */
31
+ export declare function findShellRegionElements(root?: ParentNode): HTMLElement[];
32
+ /**
33
+ * Every shell region currently painted, found by the public `data-component-area` marker (#1818) and
34
+ * never by a preset's markup — so a custom 2D region preset that keeps the marker is framed like the
35
+ * built-in one.
36
+ *
37
+ * Skipped, deliberately:
38
+ * - a value outside `ShellRegion`: `data-component-area` is the SHELL's contract, and anything else
39
+ * carrying the attribute is not a region the frame should draw under;
40
+ * - the sidebar's mobile DRAWER (`data-mobile-drawer`): it is an overlay that slides over the content,
41
+ * not a region that tiles the shell, so a slab under it would sit under the page;
42
+ * - an element with no box (a region rendered but collapsed to nothing, or hidden).
43
+ */
44
+ export declare function measureShellRegions(root?: ParentNode): MeasuredShellRegion[];
45
+ /** One navigation zone the content area shows, measured like a region. */
46
+ export interface MeasuredNavigationZone {
47
+ /** The zone's `data-content-area` value: the navigation's zone name (`primary`, `secondary`, `companion`). */
48
+ zone: string;
49
+ left: number;
50
+ top: number;
51
+ width: number;
52
+ height: number;
53
+ element: HTMLElement;
54
+ }
55
+ /**
56
+ * Every navigation zone container inside the shell's CONTENT region, found by the navigation's own public marker
57
+ * (`data-content-area`, one per zone container whatever the branch — single zone, resizable split or static split).
58
+ * Only inside the content region: the marker is the navigation's, and the frame draws zone rims only within the
59
+ * content slab. Whatever its box, so one that reappears is observed (as {@link findShellRegionElements}).
60
+ */
61
+ export declare function findNavigationZoneElements(root?: ParentNode): HTMLElement[];
62
+ /** The navigation zones currently shown: a hidden zone (`display: none`, collapsed responsively) has no box and is skipped. */
63
+ export declare function measureNavigationZones(root?: ParentNode): MeasuredNavigationZone[];
64
+ /**
65
+ * The centre of a measured region in an orthographic camera matched 1:1 to CSS pixels, whose origin is
66
+ * the viewport's centre and whose y axis points UP (three.js), while the DOM's points down.
67
+ */
68
+ export declare function regionCentreInFrameUnits(region: Pick<MeasuredShellRegion, 'left' | 'top' | 'width' | 'height'>, viewport: {
69
+ width: number;
70
+ height: number;
71
+ }): {
72
+ x: number;
73
+ y: number;
74
+ };
75
+ //# sourceMappingURL=shell-region-measurement.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"shell-region-measurement.d.ts","sourceRoot":"","sources":["../../../src/shell-region-measurement.ts"],"names":[],"mappings":"AAAA,OAAO,EAAgB,WAAW,EAAE,MAAM,qCAAqC,CAAC;AAEhF;;;;;;GAMG;AACH,MAAM,WAAW,mBAAmB;IAClC,MAAM,EAAE,WAAW,CAAC;IACpB,oHAAoH;IACpH,SAAS,EAAE,MAAM,GAAG,IAAI,CAAC;IACzB,IAAI,EAAE,MAAM,CAAC;IACb,GAAG,EAAE,MAAM,CAAC;IACZ,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,EAAE,MAAM,CAAC;IACf;;;;OAIG;IACH,KAAK,EAAE,OAAO,CAAC;IACf,yEAAyE;IACzE,OAAO,EAAE,WAAW,CAAC;CACtB;AAID;;;;GAIG;AACH,wBAAgB,uBAAuB,CAAC,IAAI,GAAE,UAAqB,GAAG,WAAW,EAAE,CAKlF;AAED;;;;;;;;;;;GAWG;AACH,wBAAgB,mBAAmB,CAAC,IAAI,GAAE,UAAqB,GAAG,mBAAmB,EAAE,CAiBtF;AAED,0EAA0E;AAC1E,MAAM,WAAW,sBAAsB;IACrC,8GAA8G;IAC9G,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;IACb,GAAG,EAAE,MAAM,CAAC;IACZ,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,EAAE,MAAM,CAAC;IACf,OAAO,EAAE,WAAW,CAAC;CACtB;AAED;;;;;GAKG;AACH,wBAAgB,0BAA0B,CAAC,IAAI,GAAE,UAAqB,GAAG,WAAW,EAAE,CAErF;AAED,+HAA+H;AAC/H,wBAAgB,sBAAsB,CAAC,IAAI,GAAE,UAAqB,GAAG,sBAAsB,EAAE,CAQ5F;AAED;;;GAGG;AACH,wBAAgB,wBAAwB,CACtC,MAAM,EAAE,IAAI,CAAC,mBAAmB,EAAE,MAAM,GAAG,KAAK,GAAG,OAAO,GAAG,QAAQ,CAAC,EACtE,QAAQ,EAAE;IAAE,KAAK,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,MAAM,CAAA;CAAE,GAC1C;IAAE,CAAC,EAAE,MAAM,CAAC;IAAC,CAAC,EAAE,MAAM,CAAA;CAAE,CAK1B"}
@@ -0,0 +1,75 @@
1
+ import { MaterialKind, ShellRegion } from '@wildo-ai/presets-components-models';
2
+ const SHELL_REGION_VALUES = new Set(Object.values(ShellRegion));
3
+ /**
4
+ * Every element marked as a shell region, whether or not it currently has a box — a region collapsed to
5
+ * nothing is still one to observe, so it is redrawn the moment it reappears. Same filtering as
6
+ * {@link measureShellRegions} otherwise.
7
+ */
8
+ export function findShellRegionElements(root = document) {
9
+ return [...root.querySelectorAll('[data-component-area]')].filter((element) => {
10
+ const value = element.dataset.componentArea;
11
+ return value !== undefined && SHELL_REGION_VALUES.has(value) && !element.hasAttribute('data-mobile-drawer');
12
+ });
13
+ }
14
+ /**
15
+ * Every shell region currently painted, found by the public `data-component-area` marker (#1818) and
16
+ * never by a preset's markup — so a custom 2D region preset that keeps the marker is framed like the
17
+ * built-in one.
18
+ *
19
+ * Skipped, deliberately:
20
+ * - a value outside `ShellRegion`: `data-component-area` is the SHELL's contract, and anything else
21
+ * carrying the attribute is not a region the frame should draw under;
22
+ * - the sidebar's mobile DRAWER (`data-mobile-drawer`): it is an overlay that slides over the content,
23
+ * not a region that tiles the shell, so a slab under it would sit under the page;
24
+ * - an element with no box (a region rendered but collapsed to nothing, or hidden).
25
+ */
26
+ export function measureShellRegions(root = document) {
27
+ const measured = [];
28
+ for (const element of findShellRegionElements(root)) {
29
+ const rect = element.getBoundingClientRect();
30
+ if (rect.width <= 0 || rect.height <= 0)
31
+ continue;
32
+ measured.push({
33
+ region: element.dataset.componentArea,
34
+ placement: element.dataset.placement ?? null,
35
+ left: rect.left,
36
+ top: rect.top,
37
+ width: rect.width,
38
+ height: rect.height,
39
+ glass: element.dataset.materialKind === MaterialKind.GLASS,
40
+ element,
41
+ });
42
+ }
43
+ return measured;
44
+ }
45
+ /**
46
+ * Every navigation zone container inside the shell's CONTENT region, found by the navigation's own public marker
47
+ * (`data-content-area`, one per zone container whatever the branch — single zone, resizable split or static split).
48
+ * Only inside the content region: the marker is the navigation's, and the frame draws zone rims only within the
49
+ * content slab. Whatever its box, so one that reappears is observed (as {@link findShellRegionElements}).
50
+ */
51
+ export function findNavigationZoneElements(root = document) {
52
+ return [...root.querySelectorAll(`[data-component-area="${ShellRegion.CONTENT}"] [data-content-area]`)];
53
+ }
54
+ /** The navigation zones currently shown: a hidden zone (`display: none`, collapsed responsively) has no box and is skipped. */
55
+ export function measureNavigationZones(root = document) {
56
+ const measured = [];
57
+ for (const element of findNavigationZoneElements(root)) {
58
+ const rect = element.getBoundingClientRect();
59
+ if (rect.width <= 0 || rect.height <= 0)
60
+ continue;
61
+ measured.push({ zone: element.dataset.contentArea ?? '', left: rect.left, top: rect.top, width: rect.width, height: rect.height, element });
62
+ }
63
+ return measured;
64
+ }
65
+ /**
66
+ * The centre of a measured region in an orthographic camera matched 1:1 to CSS pixels, whose origin is
67
+ * the viewport's centre and whose y axis points UP (three.js), while the DOM's points down.
68
+ */
69
+ export function regionCentreInFrameUnits(region, viewport) {
70
+ return {
71
+ x: region.left + region.width / 2 - viewport.width / 2,
72
+ y: viewport.height / 2 - (region.top + region.height / 2),
73
+ };
74
+ }
75
+ //# sourceMappingURL=shell-region-measurement.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"shell-region-measurement.js","sourceRoot":"","sources":["../../../src/shell-region-measurement.ts"],"names":[],"mappings":"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","sourcesContent":["import { MaterialKind, ShellRegion } from '@wildo-ai/presets-components-models';\n\n/**\n * One painted region of the signed-in shell, measured in CSS pixels relative to the viewport.\n *\n * The 3D frame only FOLLOWS the HTML shell: every size, collapse and breakpoint decision stays the 2D\n * container's, and the frame reads the result off the DOM. So this is the frame's only input about the\n * layout, and it carries nothing a renderer could mistake for a decision.\n */\nexport interface MeasuredShellRegion {\n region: ShellRegion;\n /** The region's `data-placement` (a `HorizontalPlacement` / `VerticalPlacement` value), when it sits at an edge. */\n placement: string | null;\n left: number;\n top: number;\n width: number;\n height: number;\n /**\n * The region already renders a glass material: its element carries `data-material-kind=\"glass\"` (a region\n * preset renders through `Surface asChild`, which puts the material attributes on the region element itself).\n * The frame draws no slab under it — glass over a frosted slab would be two materials doing one job.\n */\n glass: boolean;\n /** The element measured, so a caller can observe it for size changes. */\n element: HTMLElement;\n}\n\nconst SHELL_REGION_VALUES: ReadonlySet<string> = new Set(Object.values(ShellRegion));\n\n/**\n * Every element marked as a shell region, whether or not it currently has a box — a region collapsed to\n * nothing is still one to observe, so it is redrawn the moment it reappears. Same filtering as\n * {@link measureShellRegions} otherwise.\n */\nexport function findShellRegionElements(root: ParentNode = document): HTMLElement[] {\n return [...root.querySelectorAll<HTMLElement>('[data-component-area]')].filter((element) => {\n const value = element.dataset.componentArea;\n return value !== undefined && SHELL_REGION_VALUES.has(value) && !element.hasAttribute('data-mobile-drawer');\n });\n}\n\n/**\n * Every shell region currently painted, found by the public `data-component-area` marker (#1818) and\n * never by a preset's markup — so a custom 2D region preset that keeps the marker is framed like the\n * built-in one.\n *\n * Skipped, deliberately:\n * - a value outside `ShellRegion`: `data-component-area` is the SHELL's contract, and anything else\n * carrying the attribute is not a region the frame should draw under;\n * - the sidebar's mobile DRAWER (`data-mobile-drawer`): it is an overlay that slides over the content,\n * not a region that tiles the shell, so a slab under it would sit under the page;\n * - an element with no box (a region rendered but collapsed to nothing, or hidden).\n */\nexport function measureShellRegions(root: ParentNode = document): MeasuredShellRegion[] {\n const measured: MeasuredShellRegion[] = [];\n for (const element of findShellRegionElements(root)) {\n const rect = element.getBoundingClientRect();\n if (rect.width <= 0 || rect.height <= 0) continue;\n measured.push({\n region: element.dataset.componentArea as ShellRegion,\n placement: element.dataset.placement ?? null,\n left: rect.left,\n top: rect.top,\n width: rect.width,\n height: rect.height,\n glass: element.dataset.materialKind === MaterialKind.GLASS,\n element,\n });\n }\n return measured;\n}\n\n/** One navigation zone the content area shows, measured like a region. */\nexport interface MeasuredNavigationZone {\n /** The zone's `data-content-area` value: the navigation's zone name (`primary`, `secondary`, `companion`). */\n zone: string;\n left: number;\n top: number;\n width: number;\n height: number;\n element: HTMLElement;\n}\n\n/**\n * Every navigation zone container inside the shell's CONTENT region, found by the navigation's own public marker\n * (`data-content-area`, one per zone container whatever the branch — single zone, resizable split or static split).\n * Only inside the content region: the marker is the navigation's, and the frame draws zone rims only within the\n * content slab. Whatever its box, so one that reappears is observed (as {@link findShellRegionElements}).\n */\nexport function findNavigationZoneElements(root: ParentNode = document): HTMLElement[] {\n return [...root.querySelectorAll<HTMLElement>(`[data-component-area=\"${ShellRegion.CONTENT}\"] [data-content-area]`)];\n}\n\n/** The navigation zones currently shown: a hidden zone (`display: none`, collapsed responsively) has no box and is skipped. */\nexport function measureNavigationZones(root: ParentNode = document): MeasuredNavigationZone[] {\n const measured: MeasuredNavigationZone[] = [];\n for (const element of findNavigationZoneElements(root)) {\n const rect = element.getBoundingClientRect();\n if (rect.width <= 0 || rect.height <= 0) continue;\n measured.push({ zone: element.dataset.contentArea ?? '', left: rect.left, top: rect.top, width: rect.width, height: rect.height, element });\n }\n return measured;\n}\n\n/**\n * The centre of a measured region in an orthographic camera matched 1:1 to CSS pixels, whose origin is\n * the viewport's centre and whose y axis points UP (three.js), while the DOM's points down.\n */\nexport function regionCentreInFrameUnits(\n region: Pick<MeasuredShellRegion, 'left' | 'top' | 'width' | 'height'>,\n viewport: { width: number; height: number },\n): { x: number; y: number } {\n return {\n x: region.left + region.width / 2 - viewport.width / 2,\n y: viewport.height / 2 - (region.top + region.height / 2),\n };\n}\n"]}
@@ -0,0 +1,106 @@
1
+ /**
2
+ * The spatial layout's camera and panel geometry (PROTOTYPE, 2026-09-30): plain math, no three.js, so the placement
3
+ * container can import it without pulling the WebGL chunk in (`check:lazy-boundaries`).
4
+ *
5
+ * ## One world, two renderers
6
+ *
7
+ * The world is in CSS pixels, with its origin at the middle of the viewport, x to the right, y DOWN, z toward the
8
+ * viewer — CSS's own axes, so the HTML stage needs no conversion. A CSS `perspective: d` root with the camera element
9
+ * at its centre shows an element at z = 0 at exactly its CSS size when the camera rests at (0, 0, d): that is the rest
10
+ * pose, and it is why the content panel is crisp at rest. The WebGL environment renders the same world through a
11
+ * three.js perspective camera built from the same pose (y flipped, see `spatial-environment.tsx`).
12
+ */
13
+ /** Where the camera is and where it looks. Angles in degrees, in CSS rotation terms (`rotateY`, `rotateX`). */
14
+ export interface SpatialCameraPose {
15
+ x: number;
16
+ y: number;
17
+ z: number;
18
+ yawDeg: number;
19
+ pitchDeg: number;
20
+ }
21
+ /** The CSS perspective distance: the viewer's distance from the z = 0 plane at rest. Short enough to feel spatial. */
22
+ export declare const SPATIAL_CAMERA_DISTANCE_PX = 1100;
23
+ export declare function restPose(distance?: number): SpatialCameraPose;
24
+ /**
25
+ * The transform of the stage's camera element for a pose: the inverse of the camera (a camera moving right is the
26
+ * world moving left), then pushed to the perspective plane. `translateZ(d) · Rᵀ · T(−P)`, where the camera's rotation is
27
+ * `rotateY(yaw) · rotateX(pitch)`; at rest it is the identity.
28
+ */
29
+ export declare function cssCameraTransform(pose: SpatialCameraPose, distance?: number): string;
30
+ export declare function lerpPose(from: SpatialCameraPose, to: SpatialCameraPose, t: number): SpatialCameraPose;
31
+ /** A camera move: poses at points of its duration (0 to 1), eased between. The last pose is where it comes to rest. */
32
+ export interface SpatialCameraMove {
33
+ durationMs: number;
34
+ keyframes: ReadonlyArray<{
35
+ at: number;
36
+ pose: SpatialCameraPose;
37
+ }>;
38
+ }
39
+ /** A smooth in-out easing: the camera starts and stops without a jolt. */
40
+ export declare function easeInOutCubic(t: number): number;
41
+ /** The camera's pose `elapsedMs` into a move. Past its end, the move's final pose. */
42
+ export declare function sampleMove(move: SpatialCameraMove, elapsedMs: number): SpatialCameraPose;
43
+ /** The moves the prototype plays. Each leaves the camera at rest, so the content is always crisp when nothing moves. */
44
+ export declare enum SpatialCameraCue {
45
+ /** The shell appears: the camera glides in from far back. */
46
+ ARRIVAL = "arrival",
47
+ /** The content changed: the camera steps back to take in the space, then returns. */
48
+ NAVIGATION = "navigation",
49
+ /** Focus moved to a pane beside another: the camera leans toward it, then returns. */
50
+ ZONE_FOCUS = "zone-focus",
51
+ /** The side column resized: the camera breathes out while the panels re-arrange. */
52
+ COLUMN_RESIZE = "column-resize"
53
+ }
54
+ /**
55
+ * The move for a cue. `towardX` is, for a zone focus, the focused pane's centre in world x (the camera leans toward
56
+ * it); ignored otherwise.
57
+ */
58
+ export declare function cameraMoveFor(cue: SpatialCameraCue, distance?: number, towardX?: number): SpatialCameraMove;
59
+ /** The panels the spatial placement arranges the shell regions into. */
60
+ export declare enum SpatialPanelId {
61
+ /** Activity bar, start sidebar, start utility panel: one column standing angled on the start side. */
62
+ START_COLUMN = "start-column",
63
+ /** Toolbar and content: facing the camera. */
64
+ CENTRE = "centre",
65
+ /** End sidebar and end utility panel, angled on the end side. */
66
+ END_COLUMN = "end-column",
67
+ /** Top status bar and menubar, floating above the centre. */
68
+ TOP = "top",
69
+ /** Bottom status bar, below the centre. */
70
+ BOTTOM = "bottom"
71
+ }
72
+ /** A panel's size (its CSS box, measured untransformed) and where its centre stands in the world. */
73
+ export interface SpatialPanelPlacement {
74
+ x: number;
75
+ y: number;
76
+ z: number;
77
+ width: number;
78
+ height: number;
79
+ yawDeg: number;
80
+ }
81
+ export interface SpatialLayoutInput {
82
+ viewport: {
83
+ width: number;
84
+ height: number;
85
+ };
86
+ /** Natural widths of the side columns (0 when empty). */
87
+ startWidth: number;
88
+ endWidth: number;
89
+ /** Natural heights of the top and bottom strips (0 when empty). */
90
+ topHeight: number;
91
+ bottomHeight: number;
92
+ }
93
+ /** Margin around the arrangement and gap between panels, in CSS pixels. */
94
+ export declare const SPATIAL_MARGIN_PX = 28;
95
+ export declare const SPATIAL_GAP_PX = 18;
96
+ /** How far the side columns turn toward the viewer, like screens beside a desk. */
97
+ export declare const SPATIAL_SIDE_ANGLE_DEG = 26;
98
+ /**
99
+ * Where each panel stands. The centre keeps the footprint it would have in 2D and faces the camera at z = 0, so at
100
+ * rest it is shown at exactly its CSS size. Each side column is hinged at its inner edge, a gap from the centre, and
101
+ * turned so its outer edge comes toward the viewer. The top strip floats slightly forward, above the centre.
102
+ */
103
+ export declare function layoutSpatialPanels(input: SpatialLayoutInput): Record<SpatialPanelId, SpatialPanelPlacement>;
104
+ /** The CSS transform standing a panel at its placement: centred on the stage origin, moved, then turned. */
105
+ export declare function panelTransform(panel: SpatialPanelPlacement): string;
106
+ //# sourceMappingURL=spatial-camera.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"spatial-camera.d.ts","sourceRoot":"","sources":["../../../../src/spatial/spatial-camera.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAEH,+GAA+G;AAC/G,MAAM,WAAW,iBAAiB;IAChC,CAAC,EAAE,MAAM,CAAC;IACV,CAAC,EAAE,MAAM,CAAC;IACV,CAAC,EAAE,MAAM,CAAC;IACV,MAAM,EAAE,MAAM,CAAC;IACf,QAAQ,EAAE,MAAM,CAAC;CAClB;AAED,sHAAsH;AACtH,eAAO,MAAM,0BAA0B,OAAO,CAAC;AAE/C,wBAAgB,QAAQ,CAAC,QAAQ,GAAE,MAAmC,GAAG,iBAAiB,CAEzF;AAED;;;;GAIG;AACH,wBAAgB,kBAAkB,CAAC,IAAI,EAAE,iBAAiB,EAAE,QAAQ,GAAE,MAAmC,GAAG,MAAM,CAEjH;AAED,wBAAgB,QAAQ,CAAC,IAAI,EAAE,iBAAiB,EAAE,EAAE,EAAE,iBAAiB,EAAE,CAAC,EAAE,MAAM,GAAG,iBAAiB,CAGrG;AAED,uHAAuH;AACvH,MAAM,WAAW,iBAAiB;IAChC,UAAU,EAAE,MAAM,CAAC;IACnB,SAAS,EAAE,aAAa,CAAC;QAAE,EAAE,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,iBAAiB,CAAA;KAAE,CAAC,CAAC;CACnE;AAED,0EAA0E;AAC1E,wBAAgB,cAAc,CAAC,CAAC,EAAE,MAAM,GAAG,MAAM,CAEhD;AAED,sFAAsF;AACtF,wBAAgB,UAAU,CAAC,IAAI,EAAE,iBAAiB,EAAE,SAAS,EAAE,MAAM,GAAG,iBAAiB,CAaxF;AAED,wHAAwH;AACxH,oBAAY,gBAAgB;IAC1B,6DAA6D;IAC7D,OAAO,YAAY;IACnB,qFAAqF;IACrF,UAAU,eAAe;IACzB,sFAAsF;IACtF,UAAU,eAAe;IACzB,oFAAoF;IACpF,aAAa,kBAAkB;CAChC;AAED;;;GAGG;AACH,wBAAgB,aAAa,CAAC,GAAG,EAAE,gBAAgB,EAAE,QAAQ,GAAE,MAAmC,EAAE,OAAO,SAAI,GAAG,iBAAiB,CAwBlI;AAED,wEAAwE;AACxE,oBAAY,cAAc;IACxB,sGAAsG;IACtG,YAAY,iBAAiB;IAC7B,8CAA8C;IAC9C,MAAM,WAAW;IACjB,iEAAiE;IACjE,UAAU,eAAe;IACzB,6DAA6D;IAC7D,GAAG,QAAQ;IACX,2CAA2C;IAC3C,MAAM,WAAW;CAClB;AAED,qGAAqG;AACrG,MAAM,WAAW,qBAAqB;IACpC,CAAC,EAAE,MAAM,CAAC;IACV,CAAC,EAAE,MAAM,CAAC;IACV,CAAC,EAAE,MAAM,CAAC;IACV,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,EAAE,MAAM,CAAC;IACf,MAAM,EAAE,MAAM,CAAC;CAChB;AAED,MAAM,WAAW,kBAAkB;IACjC,QAAQ,EAAE;QAAE,KAAK,EAAE,MAAM,CAAC;QAAC,MAAM,EAAE,MAAM,CAAA;KAAE,CAAC;IAC5C,yDAAyD;IACzD,UAAU,EAAE,MAAM,CAAC;IACnB,QAAQ,EAAE,MAAM,CAAC;IACjB,mEAAmE;IACnE,SAAS,EAAE,MAAM,CAAC;IAClB,YAAY,EAAE,MAAM,CAAC;CACtB;AAED,2EAA2E;AAC3E,eAAO,MAAM,iBAAiB,KAAK,CAAC;AACpC,eAAO,MAAM,cAAc,KAAK,CAAC;AACjC,mFAAmF;AACnF,eAAO,MAAM,sBAAsB,KAAK,CAAC;AAEzC;;;;GAIG;AACH,wBAAgB,mBAAmB,CAAC,KAAK,EAAE,kBAAkB,GAAG,MAAM,CAAC,cAAc,EAAE,qBAAqB,CAAC,CA6B5G;AAED,4GAA4G;AAC5G,wBAAgB,cAAc,CAAC,KAAK,EAAE,qBAAqB,GAAG,MAAM,CAEnE"}