mythik-react 0.1.0

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 (244) hide show
  1. package/LICENSE +201 -0
  2. package/NOTICE +4 -0
  3. package/README.md +83 -0
  4. package/dist/MythikApp.d.ts +61 -0
  5. package/dist/MythikApp.d.ts.map +1 -0
  6. package/dist/MythikApp.js +381 -0
  7. package/dist/MythikApp.js.map +1 -0
  8. package/dist/MythikRenderer.d.ts +31 -0
  9. package/dist/MythikRenderer.d.ts.map +1 -0
  10. package/dist/MythikRenderer.js +900 -0
  11. package/dist/MythikRenderer.js.map +1 -0
  12. package/dist/animation/index.d.ts +7 -0
  13. package/dist/animation/index.d.ts.map +1 -0
  14. package/dist/animation/index.js +5 -0
  15. package/dist/animation/index.js.map +1 -0
  16. package/dist/animation/stylesheet-singleton.d.ts +12 -0
  17. package/dist/animation/stylesheet-singleton.d.ts.map +1 -0
  18. package/dist/animation/stylesheet-singleton.js +107 -0
  19. package/dist/animation/stylesheet-singleton.js.map +1 -0
  20. package/dist/animation/useElementAnimations.d.ts +30 -0
  21. package/dist/animation/useElementAnimations.d.ts.map +1 -0
  22. package/dist/animation/useElementAnimations.js +254 -0
  23. package/dist/animation/useElementAnimations.js.map +1 -0
  24. package/dist/animation/usePrefersReducedMotion.d.ts +2 -0
  25. package/dist/animation/usePrefersReducedMotion.d.ts.map +1 -0
  26. package/dist/animation/usePrefersReducedMotion.js +29 -0
  27. package/dist/animation/usePrefersReducedMotion.js.map +1 -0
  28. package/dist/animation/useShapeAnimations.d.ts +21 -0
  29. package/dist/animation/useShapeAnimations.d.ts.map +1 -0
  30. package/dist/animation/useShapeAnimations.js +119 -0
  31. package/dist/animation/useShapeAnimations.js.map +1 -0
  32. package/dist/app-context.d.ts +15 -0
  33. package/dist/app-context.d.ts.map +1 -0
  34. package/dist/app-context.js +9 -0
  35. package/dist/app-context.js.map +1 -0
  36. package/dist/background/BackgroundLayer.d.ts +7 -0
  37. package/dist/background/BackgroundLayer.d.ts.map +1 -0
  38. package/dist/background/BackgroundLayer.js +50 -0
  39. package/dist/background/BackgroundLayer.js.map +1 -0
  40. package/dist/background/BackgroundStack.d.ts +19 -0
  41. package/dist/background/BackgroundStack.d.ts.map +1 -0
  42. package/dist/background/BackgroundStack.js +59 -0
  43. package/dist/background/BackgroundStack.js.map +1 -0
  44. package/dist/background/BlobLayer.d.ts +12 -0
  45. package/dist/background/BlobLayer.d.ts.map +1 -0
  46. package/dist/background/BlobLayer.js +60 -0
  47. package/dist/background/BlobLayer.js.map +1 -0
  48. package/dist/background/index.d.ts +3 -0
  49. package/dist/background/index.d.ts.map +1 -0
  50. package/dist/background/index.js +3 -0
  51. package/dist/background/index.js.map +1 -0
  52. package/dist/css-hover.d.ts +15 -0
  53. package/dist/css-hover.d.ts.map +1 -0
  54. package/dist/css-hover.js +51 -0
  55. package/dist/css-hover.js.map +1 -0
  56. package/dist/index.d.ts +10 -0
  57. package/dist/index.d.ts.map +1 -0
  58. package/dist/index.js +11 -0
  59. package/dist/index.js.map +1 -0
  60. package/dist/primitives/accordion.d.ts +12 -0
  61. package/dist/primitives/accordion.d.ts.map +1 -0
  62. package/dist/primitives/accordion.js +25 -0
  63. package/dist/primitives/accordion.js.map +1 -0
  64. package/dist/primitives/area-chart.d.ts +14 -0
  65. package/dist/primitives/area-chart.d.ts.map +1 -0
  66. package/dist/primitives/area-chart.js +18 -0
  67. package/dist/primitives/area-chart.js.map +1 -0
  68. package/dist/primitives/audio-player.d.ts +9 -0
  69. package/dist/primitives/audio-player.d.ts.map +1 -0
  70. package/dist/primitives/audio-player.js +5 -0
  71. package/dist/primitives/audio-player.js.map +1 -0
  72. package/dist/primitives/bar-chart.d.ts +14 -0
  73. package/dist/primitives/bar-chart.d.ts.map +1 -0
  74. package/dist/primitives/bar-chart.js +22 -0
  75. package/dist/primitives/bar-chart.js.map +1 -0
  76. package/dist/primitives/box.d.ts +21 -0
  77. package/dist/primitives/box.d.ts.map +1 -0
  78. package/dist/primitives/box.js +54 -0
  79. package/dist/primitives/box.js.map +1 -0
  80. package/dist/primitives/button.d.ts +14 -0
  81. package/dist/primitives/button.d.ts.map +1 -0
  82. package/dist/primitives/button.js +28 -0
  83. package/dist/primitives/button.js.map +1 -0
  84. package/dist/primitives/camera.d.ts +15 -0
  85. package/dist/primitives/camera.d.ts.map +1 -0
  86. package/dist/primitives/camera.js +25 -0
  87. package/dist/primitives/camera.js.map +1 -0
  88. package/dist/primitives/checkbox.d.ts +12 -0
  89. package/dist/primitives/checkbox.d.ts.map +1 -0
  90. package/dist/primitives/checkbox.js +24 -0
  91. package/dist/primitives/checkbox.js.map +1 -0
  92. package/dist/primitives/divider.d.ts +9 -0
  93. package/dist/primitives/divider.d.ts.map +1 -0
  94. package/dist/primitives/divider.js +10 -0
  95. package/dist/primitives/divider.js.map +1 -0
  96. package/dist/primitives/drawer.d.ts +21 -0
  97. package/dist/primitives/drawer.d.ts.map +1 -0
  98. package/dist/primitives/drawer.js +38 -0
  99. package/dist/primitives/drawer.js.map +1 -0
  100. package/dist/primitives/file-upload.d.ts +27 -0
  101. package/dist/primitives/file-upload.d.ts.map +1 -0
  102. package/dist/primitives/file-upload.js +225 -0
  103. package/dist/primitives/file-upload.js.map +1 -0
  104. package/dist/primitives/grid.d.ts +13 -0
  105. package/dist/primitives/grid.d.ts.map +1 -0
  106. package/dist/primitives/grid.js +13 -0
  107. package/dist/primitives/grid.js.map +1 -0
  108. package/dist/primitives/icon.d.ts +22 -0
  109. package/dist/primitives/icon.d.ts.map +1 -0
  110. package/dist/primitives/icon.js +52 -0
  111. package/dist/primitives/icon.js.map +1 -0
  112. package/dist/primitives/image.d.ts +13 -0
  113. package/dist/primitives/image.d.ts.map +1 -0
  114. package/dist/primitives/image.js +38 -0
  115. package/dist/primitives/image.js.map +1 -0
  116. package/dist/primitives/index.d.ts +57 -0
  117. package/dist/primitives/index.d.ts.map +1 -0
  118. package/dist/primitives/index.js +106 -0
  119. package/dist/primitives/index.js.map +1 -0
  120. package/dist/primitives/input.d.ts +32 -0
  121. package/dist/primitives/input.d.ts.map +1 -0
  122. package/dist/primitives/input.js +192 -0
  123. package/dist/primitives/input.js.map +1 -0
  124. package/dist/primitives/kanban-board.d.ts +13 -0
  125. package/dist/primitives/kanban-board.d.ts.map +1 -0
  126. package/dist/primitives/kanban-board.js +5 -0
  127. package/dist/primitives/kanban-board.js.map +1 -0
  128. package/dist/primitives/line-chart.d.ts +14 -0
  129. package/dist/primitives/line-chart.d.ts.map +1 -0
  130. package/dist/primitives/line-chart.js +17 -0
  131. package/dist/primitives/line-chart.js.map +1 -0
  132. package/dist/primitives/list.d.ts +13 -0
  133. package/dist/primitives/list.d.ts.map +1 -0
  134. package/dist/primitives/list.js +10 -0
  135. package/dist/primitives/list.js.map +1 -0
  136. package/dist/primitives/modal.d.ts +20 -0
  137. package/dist/primitives/modal.d.ts.map +1 -0
  138. package/dist/primitives/modal.js +60 -0
  139. package/dist/primitives/modal.js.map +1 -0
  140. package/dist/primitives/pie-chart.d.ts +15 -0
  141. package/dist/primitives/pie-chart.d.ts.map +1 -0
  142. package/dist/primitives/pie-chart.js +36 -0
  143. package/dist/primitives/pie-chart.js.map +1 -0
  144. package/dist/primitives/screen-outlet.d.ts +9 -0
  145. package/dist/primitives/screen-outlet.d.ts.map +1 -0
  146. package/dist/primitives/screen-outlet.js +92 -0
  147. package/dist/primitives/screen-outlet.js.map +1 -0
  148. package/dist/primitives/screen.d.ts +9 -0
  149. package/dist/primitives/screen.d.ts.map +1 -0
  150. package/dist/primitives/screen.js +10 -0
  151. package/dist/primitives/screen.js.map +1 -0
  152. package/dist/primitives/scroll.d.ts +11 -0
  153. package/dist/primitives/scroll.d.ts.map +1 -0
  154. package/dist/primitives/scroll.js +10 -0
  155. package/dist/primitives/scroll.js.map +1 -0
  156. package/dist/primitives/select.d.ts +19 -0
  157. package/dist/primitives/select.d.ts.map +1 -0
  158. package/dist/primitives/select.js +109 -0
  159. package/dist/primitives/select.js.map +1 -0
  160. package/dist/primitives/signature.d.ts +13 -0
  161. package/dist/primitives/signature.d.ts.map +1 -0
  162. package/dist/primitives/signature.js +45 -0
  163. package/dist/primitives/signature.js.map +1 -0
  164. package/dist/primitives/skeleton.d.ts +14 -0
  165. package/dist/primitives/skeleton.d.ts.map +1 -0
  166. package/dist/primitives/skeleton.js +41 -0
  167. package/dist/primitives/skeleton.js.map +1 -0
  168. package/dist/primitives/slider.d.ts +15 -0
  169. package/dist/primitives/slider.d.ts.map +1 -0
  170. package/dist/primitives/slider.js +7 -0
  171. package/dist/primitives/slider.js.map +1 -0
  172. package/dist/primitives/spacer.d.ts +9 -0
  173. package/dist/primitives/spacer.d.ts.map +1 -0
  174. package/dist/primitives/spacer.js +9 -0
  175. package/dist/primitives/spacer.js.map +1 -0
  176. package/dist/primitives/spatial-map-editing.d.ts +472 -0
  177. package/dist/primitives/spatial-map-editing.d.ts.map +1 -0
  178. package/dist/primitives/spatial-map-editing.js +886 -0
  179. package/dist/primitives/spatial-map-editing.js.map +1 -0
  180. package/dist/primitives/spatial-map.d.ts +1073 -0
  181. package/dist/primitives/spatial-map.d.ts.map +1 -0
  182. package/dist/primitives/spatial-map.js +1705 -0
  183. package/dist/primitives/spatial-map.js.map +1 -0
  184. package/dist/primitives/stack.d.ts +13 -0
  185. package/dist/primitives/stack.d.ts.map +1 -0
  186. package/dist/primitives/stack.js +12 -0
  187. package/dist/primitives/stack.js.map +1 -0
  188. package/dist/primitives/table.d.ts +115 -0
  189. package/dist/primitives/table.d.ts.map +1 -0
  190. package/dist/primitives/table.js +498 -0
  191. package/dist/primitives/table.js.map +1 -0
  192. package/dist/primitives/tabs.d.ts +17 -0
  193. package/dist/primitives/tabs.d.ts.map +1 -0
  194. package/dist/primitives/tabs.js +13 -0
  195. package/dist/primitives/tabs.js.map +1 -0
  196. package/dist/primitives/text.d.ts +11 -0
  197. package/dist/primitives/text.d.ts.map +1 -0
  198. package/dist/primitives/text.js +69 -0
  199. package/dist/primitives/text.js.map +1 -0
  200. package/dist/primitives/textarea.d.ts +15 -0
  201. package/dist/primitives/textarea.d.ts.map +1 -0
  202. package/dist/primitives/textarea.js +23 -0
  203. package/dist/primitives/textarea.js.map +1 -0
  204. package/dist/primitives/toast-container.d.ts +15 -0
  205. package/dist/primitives/toast-container.d.ts.map +1 -0
  206. package/dist/primitives/toast-container.js +160 -0
  207. package/dist/primitives/toast-container.js.map +1 -0
  208. package/dist/primitives/toggle.d.ts +12 -0
  209. package/dist/primitives/toggle.d.ts.map +1 -0
  210. package/dist/primitives/toggle.js +18 -0
  211. package/dist/primitives/toggle.js.map +1 -0
  212. package/dist/primitives/touchable.d.ts +10 -0
  213. package/dist/primitives/touchable.d.ts.map +1 -0
  214. package/dist/primitives/touchable.js +6 -0
  215. package/dist/primitives/touchable.js.map +1 -0
  216. package/dist/primitives/use-design-tokens.d.ts +127 -0
  217. package/dist/primitives/use-design-tokens.d.ts.map +1 -0
  218. package/dist/primitives/use-design-tokens.js +251 -0
  219. package/dist/primitives/use-design-tokens.js.map +1 -0
  220. package/dist/primitives/use-theme.d.ts +11 -0
  221. package/dist/primitives/use-theme.d.ts.map +1 -0
  222. package/dist/primitives/use-theme.js +17 -0
  223. package/dist/primitives/use-theme.js.map +1 -0
  224. package/dist/primitives/wizard.d.ts +11 -0
  225. package/dist/primitives/wizard.d.ts.map +1 -0
  226. package/dist/primitives/wizard.js +15 -0
  227. package/dist/primitives/wizard.js.map +1 -0
  228. package/dist/runtime/context-dispatcher.d.ts +3 -0
  229. package/dist/runtime/context-dispatcher.d.ts.map +1 -0
  230. package/dist/runtime/context-dispatcher.js +11 -0
  231. package/dist/runtime/context-dispatcher.js.map +1 -0
  232. package/dist/runtime/row-dispatcher.d.ts +19 -0
  233. package/dist/runtime/row-dispatcher.d.ts.map +1 -0
  234. package/dist/runtime/row-dispatcher.js +25 -0
  235. package/dist/runtime/row-dispatcher.js.map +1 -0
  236. package/dist/types.d.ts +10 -0
  237. package/dist/types.d.ts.map +1 -0
  238. package/dist/types.js +2 -0
  239. package/dist/types.js.map +1 -0
  240. package/dist/use-device-context.d.ts +8 -0
  241. package/dist/use-device-context.d.ts.map +1 -0
  242. package/dist/use-device-context.js +54 -0
  243. package/dist/use-device-context.js.map +1 -0
  244. package/package.json +59 -0
@@ -0,0 +1,900 @@
1
+ import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import React from 'react';
3
+ import { motion, AnimatePresence, LayoutGroup } from 'motion/react';
4
+ import { createMythik, createActionDispatcher, createTransactionEngine, createFormEngine, mountSpecRuntime, RESERVED_PATHS } from 'mythik';
5
+ import { registerReactPrimitives, PRIMITIVES, CSS_HOVER_SUPPORTED } from './primitives/index.js';
6
+ import { createContextDispatcher } from './runtime/context-dispatcher.js';
7
+ import { createRowDispatcher } from './runtime/row-dispatcher.js';
8
+ import { ToastContainer } from './primitives/toast-container.js';
9
+ import { BackgroundStack } from './background/BackgroundStack.js';
10
+ import { useDeviceContext } from './use-device-context.js';
11
+ import { needsMotionWrapper, generateHoverCSS, hashId } from './css-hover.js';
12
+ /**
13
+ * Gate for LayerBackground root mount (plan 3 Task 20). The legacy
14
+ * BackgroundConfig type was deleted in Task 21 but the defensive "has `style`"
15
+ * rejection stays to catch malformed specs — a caller accidentally passing
16
+ * legacy-shaped data (`{ style: 'solid' }`) gets a clean no-mount instead
17
+ * of a crash when BackgroundStack hits the v2 resolver. Empty objects `{}`
18
+ * and array inputs are also rejected so the wrapper never mounts for
19
+ * semantically-empty backgrounds.
20
+ */
21
+ function isLayerBackground(bg) {
22
+ if (typeof bg !== 'object' || bg === null || Array.isArray(bg))
23
+ return false;
24
+ if ('style' in bg)
25
+ return false;
26
+ return 'color' in bg || 'layers' in bg;
27
+ }
28
+ /**
29
+ * z-index of the content wrapper in the v2 root-mount. The BackgroundStack
30
+ * sibling sits inside its own stacking context (via `isolation: isolate`) so
31
+ * per-layer zIndex values don't leak out — this constant only needs to stay
32
+ * above any ancestor stacking contexts the consumer might establish.
33
+ */
34
+ const CONTENT_Z_INDEX = 1;
35
+ /** Convert "Infinity" strings to JS Infinity in motion config (JSON can't represent Infinity) */
36
+ function resolveInfinity(obj) {
37
+ if (obj === 'Infinity')
38
+ return Infinity;
39
+ if (typeof obj !== 'object' || obj === null)
40
+ return obj;
41
+ if (Array.isArray(obj))
42
+ return obj.map(resolveInfinity);
43
+ const result = {};
44
+ for (const [k, v] of Object.entries(obj)) {
45
+ result[k] = resolveInfinity(v);
46
+ }
47
+ return result;
48
+ }
49
+ function isTransactionBinding(binding) {
50
+ return (typeof binding === 'object' &&
51
+ binding !== null &&
52
+ 'transaction' in binding &&
53
+ typeof binding.transaction === 'object');
54
+ }
55
+ function ErrorPlaceholder({ elementId, error, exposeErrors }) {
56
+ if (!exposeErrors) {
57
+ return React.createElement('div', {
58
+ style: { padding: 4, margin: 2, backgroundColor: '#f3f4f6', borderRadius: 4, minHeight: 20 },
59
+ 'aria-hidden': true,
60
+ });
61
+ }
62
+ return React.createElement('div', {
63
+ style: {
64
+ padding: 8, margin: 4, border: '2px dashed #ef4444', borderRadius: 4,
65
+ backgroundColor: '#fef2f2', fontFamily: 'monospace', fontSize: 12, color: '#991b1b',
66
+ },
67
+ }, React.createElement('div', { style: { fontWeight: 'bold', marginBottom: 4 } }, elementId), React.createElement('div', null, error));
68
+ }
69
+ class RenderErrorBoundary extends React.Component {
70
+ state = { error: null, componentStack: '', resetKey: this.props.resetKey };
71
+ static getDerivedStateFromProps(props, state) {
72
+ if (props.resetKey !== state.resetKey) {
73
+ return { error: null, componentStack: '', resetKey: props.resetKey };
74
+ }
75
+ return null;
76
+ }
77
+ static getDerivedStateFromError(error) {
78
+ return { error };
79
+ }
80
+ componentDidCatch(_error, info) {
81
+ this.setState({ componentStack: info.componentStack ?? '' });
82
+ }
83
+ render() {
84
+ if (!this.state.error)
85
+ return this.props.children;
86
+ if (process.env.NODE_ENV === 'production' || !this.props.exposeErrors) {
87
+ return React.createElement('div', {
88
+ style: { padding: 12, backgroundColor: '#f3f4f6', borderRadius: 4, minHeight: 40 },
89
+ 'aria-hidden': true,
90
+ });
91
+ }
92
+ return React.createElement('div', {
93
+ role: 'alert',
94
+ 'data-mythik-error-overlay': 'true',
95
+ style: {
96
+ padding: 16,
97
+ margin: 8,
98
+ border: '2px solid #ef4444',
99
+ borderRadius: 6,
100
+ backgroundColor: '#fef2f2',
101
+ color: '#7f1d1d',
102
+ fontFamily: 'ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace',
103
+ whiteSpace: 'pre-wrap',
104
+ },
105
+ }, React.createElement('div', { style: { fontWeight: 700, marginBottom: 8 } }, 'Mythik render error'), React.createElement('div', null, this.state.error.message), this.state.componentStack
106
+ ? React.createElement('pre', { style: { marginTop: 12, fontSize: 12, overflow: 'auto' } }, this.state.componentStack)
107
+ : null);
108
+ }
109
+ }
110
+ /** Create an icon renderer from the plugin system, or undefined if no icon plugin registered */
111
+ function createIconRenderer(svc) {
112
+ const iconRenderer = svc.plugins.getPrimitives().get('icon');
113
+ if (!iconRenderer)
114
+ return undefined;
115
+ return (name, size, color) => {
116
+ const iconNode = iconRenderer({ name, size, color }, []);
117
+ const IconComp = iconNode.props._component ?? PRIMITIVES['icon'];
118
+ if (!IconComp)
119
+ return null;
120
+ return React.createElement(IconComp, { name, size, color });
121
+ };
122
+ }
123
+ // --- Auto-Skeleton ---
124
+ const SKELETON_SHAPES = {
125
+ text: { variant: 'text', height: 16, width: '90%' },
126
+ image: { variant: 'rect', height: 120 },
127
+ button: { variant: 'rect', height: 36, width: '120px' },
128
+ input: { variant: 'rect', height: 40 },
129
+ textarea: { variant: 'rect', height: 80 },
130
+ select: { variant: 'rect', height: 40 },
131
+ icon: { variant: 'circle', height: 24 },
132
+ 'bar-chart': { variant: 'rect', height: 200 },
133
+ 'line-chart': { variant: 'rect', height: 200 },
134
+ 'pie-chart': { variant: 'rect', height: 200 },
135
+ 'area-chart': { variant: 'rect', height: 200 },
136
+ table: { variant: 'rect', height: 200 },
137
+ slider: { variant: 'rect', height: 20 },
138
+ checkbox: { variant: 'rect', height: 20, width: '20px' },
139
+ toggle: { variant: 'rect', height: 24, width: '44px' },
140
+ };
141
+ const SKELETON_PASSTHROUGH = new Set(['stack', 'grid', 'box', 'scroll', 'list']);
142
+ const SKELETON_SKIP = new Set([
143
+ 'modal', 'drawer', 'tabs', 'accordion', 'wizard', 'screen',
144
+ 'file-upload', 'camera', 'signature',
145
+ 'audio-player', 'toast-container', 'screen-outlet', 'kanban-board', 'skeleton',
146
+ ]);
147
+ // Elements rendered as-is during skeleton mode (structural, not data-dependent)
148
+ const SKELETON_RENDER_ASIS = new Set(['divider', 'spacer']);
149
+ const SHIMMER_CSS = `
150
+ @keyframes sv-shimmer {
151
+ 0% { background-position: -200% 0; }
152
+ 100% { background-position: 200% 0; }
153
+ }
154
+ .sv-skeleton {
155
+ background: linear-gradient(90deg, #E5E7EB 25%, #F3F4F6 50%, #E5E7EB 75%);
156
+ background-size: 200% 100%;
157
+ animation: sv-shimmer 1.5s ease-in-out infinite;
158
+ }
159
+ .sv-skeleton-dark {
160
+ background: linear-gradient(90deg, #374151 25%, #4B5563 50%, #374151 75%);
161
+ background-size: 200% 100%;
162
+ animation: sv-shimmer 1.5s ease-in-out infinite;
163
+ }`;
164
+ /**
165
+ * When `emitElementIds` is on, wrap each rendered primitive in a
166
+ * layout-transparent `<div data-mythik-id>` sentinel. `display: contents`
167
+ * makes the wrapper invisible to flex/grid/block layout — its child
168
+ * behaves as if it were the direct child of the wrapper's parent. This
169
+ * lets us mark elements without touching individual primitive components
170
+ * or breaking layouts.
171
+ */
172
+ function withInspectId(element, elementId, emitElementIds, key) {
173
+ if (!emitElementIds || !elementId || !element)
174
+ return element;
175
+ return React.createElement('div', {
176
+ key: key ?? elementId,
177
+ 'data-mythik-id': elementId,
178
+ style: { display: 'contents' },
179
+ }, element);
180
+ }
181
+ /** Recursively render a RenderNode tree to React elements */
182
+ function renderNode(node, svc, dispatchAction, index = 0, cssCollector, fileRegistryRef, skeletonMode, spec, emitElementIds) {
183
+ // Handle error nodes from render engine
184
+ if (node.type === '_error') {
185
+ const exposeErrors = svc.security?.exposeErrors !== false;
186
+ return React.createElement(ErrorPlaceholder, {
187
+ key: node.key ?? `error-${index}`,
188
+ elementId: node.props.elementId,
189
+ error: node.props.error,
190
+ exposeErrors,
191
+ });
192
+ }
193
+ const { _component, _bindings, _eventBindings, _hover, _active, _focus, _transition, _motion, _elementId, ...restProps } = node.props;
194
+ // NOTE: the legacy `motionEntrance` identity token → Framer-Motion-props
195
+ // injection used to live here. It was removed in plan 2 (Task 23) when the
196
+ // animation engine was introduced. Consumers now declare entrance animations
197
+ // via `Element.animations.mount` on individual elements; the `<Box>`
198
+ // primitive (and future primitives) consume the field via `useElementAnimations`.
199
+ // Plan 3 adds identity-level cascade for `animations` so the ergonomics match
200
+ // the old token-driven approach — until then, specs wanting global mount
201
+ // animation must set `animations.mount` per element or via template/variant.
202
+ // Auto-skeleton: replace data-dependent elements with skeleton shapes during loading
203
+ if (skeletonMode && spec) {
204
+ const elementId = _elementId;
205
+ const element = elementId ? spec.elements[elementId] : undefined;
206
+ // Respect skeleton: false opt-out
207
+ if (element?.skeleton !== false) {
208
+ // Skip types that shouldn't be skeletonized
209
+ if (SKELETON_SKIP.has(node.type)) {
210
+ return null;
211
+ }
212
+ // Render structural elements as-is (divider, spacer)
213
+ if (SKELETON_RENDER_ASIS.has(node.type)) {
214
+ const Comp = _component ?? PRIMITIVES[node.type];
215
+ if (!Comp)
216
+ return null;
217
+ return React.createElement(Comp, { ...restProps, key: node.key ?? index });
218
+ }
219
+ // Passthrough: render children but keep layout
220
+ if (SKELETON_PASSTHROUGH.has(node.type)) {
221
+ const Component = _component ?? PRIMITIVES[node.type];
222
+ if (!Component)
223
+ return null;
224
+ const children = node.children.length > 0
225
+ ? node.children.map((child, i) => withInspectId(renderNode(child, svc, dispatchAction, i, cssCollector, fileRegistryRef, skeletonMode, spec, emitElementIds), child.props._elementId, emitElementIds, child.key ?? `${child.props._elementId}-${i}`))
226
+ : undefined;
227
+ return React.createElement(Component, { ...restProps, key: node.key ?? index }, children);
228
+ }
229
+ // Check if a manual skeleton sibling exists — if so, skip auto-skeleton for this element
230
+ // (manual skeletons handle their own visibility via visible conditions)
231
+ if (node.type === 'skeleton') {
232
+ const SkeletonComp = PRIMITIVES['skeleton'];
233
+ if (SkeletonComp) {
234
+ return React.createElement(SkeletonComp, { ...restProps, key: node.key ?? index });
235
+ }
236
+ }
237
+ // Shape mapping: replace element with skeleton
238
+ const shape = SKELETON_SHAPES[node.type];
239
+ if (shape) {
240
+ const SkeletonComp = PRIMITIVES['skeleton'];
241
+ if (SkeletonComp) {
242
+ return React.createElement(SkeletonComp, {
243
+ variant: shape.variant,
244
+ height: shape.height,
245
+ width: shape.width,
246
+ _tokens: restProps._tokens,
247
+ key: node.key ?? index,
248
+ });
249
+ }
250
+ }
251
+ }
252
+ }
253
+ const Component = _component ?? PRIMITIVES[node.type];
254
+ if (!Component) {
255
+ console.warn(`MythikRenderer: No React component found for type "${node.type}"`);
256
+ return null;
257
+ }
258
+ // Wire $bindState bindings to onChange handlers
259
+ const handlers = {};
260
+ if (_bindings && typeof _bindings === 'object') {
261
+ const bindings = _bindings;
262
+ for (const [propName, statePath] of Object.entries(bindings)) {
263
+ if (propName === 'value' || propName === 'checked') {
264
+ handlers.onChange = (val) => {
265
+ svc.store.set(statePath, val);
266
+ };
267
+ }
268
+ }
269
+ }
270
+ // Wire on.press, on.change, etc. to action dispatcher
271
+ if (_eventBindings && typeof _eventBindings === 'object') {
272
+ const eventBindings = _eventBindings;
273
+ if (eventBindings.press) {
274
+ handlers.onClick = () => dispatchAction(eventBindings.press);
275
+ }
276
+ if (eventBindings.change) {
277
+ const existingOnChange = handlers.onChange;
278
+ handlers.onChange = (val) => {
279
+ existingOnChange?.(val);
280
+ dispatchAction(eventBindings.change);
281
+ };
282
+ }
283
+ if (eventBindings.submit) {
284
+ handlers.onSubmit = () => dispatchAction(eventBindings.submit);
285
+ }
286
+ }
287
+ // Wire onStateChange and renderIcon for table primitives
288
+ if (node.type === 'table') {
289
+ handlers.onStateChange = (path, value) => {
290
+ svc.store.set(path, value);
291
+ };
292
+ handlers.renderIcon = createIconRenderer(svc) ?? (() => null);
293
+ // Shared row-context dispatcher — writes row to /ui/selectedRow before
294
+ // invoking the action chain. Used by column action buttons and onRowClick —
295
+ // they share the same row-context contract per ai-context-runtime-semantics § 2.1.
296
+ const rowDispatch = createRowDispatcher(svc.store, dispatchAction);
297
+ handlers.dispatchAction = rowDispatch;
298
+ // Wire onRowClick: if spec provides ActionBinding(s), wrap into a function
299
+ // the primitive can invoke. If consumer passed a function directly
300
+ // (programmatic mode), leave it untouched (the primitive receives it as-is via restProps).
301
+ const onRowClickRaw = restProps.onRowClick;
302
+ if (onRowClickRaw && typeof onRowClickRaw !== 'function') {
303
+ handlers.onRowClick = (row) => {
304
+ rowDispatch(onRowClickRaw, row);
305
+ };
306
+ }
307
+ }
308
+ // Wire spatial-map contextual selection + lazy actions.
309
+ if (node.type === 'spatial-map') {
310
+ const selectedItemPath = typeof restProps.selectedItemPath === 'string'
311
+ ? restProps.selectedItemPath
312
+ : RESERVED_PATHS.SELECTED_SPATIAL_ITEM;
313
+ const selectedContext = svc.store.get(selectedItemPath);
314
+ restProps._selectedItemContext = selectedContext;
315
+ const selectedZonePath = typeof restProps.selectedZonePath === 'string'
316
+ ? restProps.selectedZonePath
317
+ : RESERVED_PATHS.SELECTED_SPATIAL_ZONE;
318
+ const selectedZoneContext = svc.store.get(selectedZonePath);
319
+ restProps._selectedZoneContext = selectedZoneContext;
320
+ const spatialDispatch = createContextDispatcher(svc.store, dispatchAction, selectedItemPath);
321
+ const onItemPressRaw = restProps.onItemPress;
322
+ if (typeof onItemPressRaw !== 'function') {
323
+ handlers._onItemSelect = (context) => {
324
+ spatialDispatch(undefined, context);
325
+ };
326
+ if (onItemPressRaw) {
327
+ handlers.onItemPress = (context) => {
328
+ spatialDispatch(onItemPressRaw, context);
329
+ };
330
+ }
331
+ }
332
+ const spatialZoneDispatch = createContextDispatcher(svc.store, dispatchAction, selectedZonePath);
333
+ const onZonePressRaw = restProps.onZonePress;
334
+ if (typeof onZonePressRaw !== 'function') {
335
+ handlers._onZoneSelect = (context) => {
336
+ spatialZoneDispatch(undefined, context);
337
+ };
338
+ if (onZonePressRaw) {
339
+ handlers.onZonePress = (context) => {
340
+ spatialZoneDispatch(onZonePressRaw, context);
341
+ };
342
+ }
343
+ }
344
+ const itemChangePath = typeof restProps.itemChangePath === 'string'
345
+ ? restProps.itemChangePath
346
+ : RESERVED_PATHS.SPATIAL_ITEM_CHANGE;
347
+ const spatialChangeDispatch = createContextDispatcher(svc.store, dispatchAction, itemChangePath);
348
+ const onItemChangeRaw = restProps.onItemChange;
349
+ if (typeof onItemChangeRaw !== 'function') {
350
+ handlers.onItemChange = (context) => {
351
+ spatialChangeDispatch(onItemChangeRaw, context);
352
+ };
353
+ }
354
+ const zoneChangePath = typeof restProps.zoneChangePath === 'string'
355
+ ? restProps.zoneChangePath
356
+ : RESERVED_PATHS.SPATIAL_ZONE_CHANGE;
357
+ const spatialZoneChangeDispatch = createContextDispatcher(svc.store, dispatchAction, zoneChangePath);
358
+ const onZoneChangeRaw = restProps.onZoneChange;
359
+ if (typeof onZoneChangeRaw !== 'function') {
360
+ handlers.onZoneChange = (context) => {
361
+ spatialZoneChangeDispatch(onZoneChangeRaw, context);
362
+ };
363
+ }
364
+ const onZoneShapeEditExitRaw = restProps.onZoneShapeEditExit;
365
+ if (typeof onZoneShapeEditExitRaw !== 'function' && onZoneShapeEditExitRaw) {
366
+ handlers.onZoneShapeEditExit = () => {
367
+ dispatchAction(onZoneShapeEditExitRaw);
368
+ };
369
+ }
370
+ const onCanvasPressRaw = restProps.onCanvasPress;
371
+ if (typeof onCanvasPressRaw !== 'function') {
372
+ const policy = restProps.interactionPolicy;
373
+ const shouldClearSelection = policy?.clearSelectionOnCanvasPress !== false;
374
+ if (shouldClearSelection || onCanvasPressRaw) {
375
+ const canvasPressPath = typeof restProps.canvasPressPath === 'string'
376
+ ? restProps.canvasPressPath
377
+ : RESERVED_PATHS.SPATIAL_CANVAS_PRESS;
378
+ const canvasDispatch = createContextDispatcher(svc.store, dispatchAction, canvasPressPath);
379
+ handlers.onCanvasPress = (context) => {
380
+ if (shouldClearSelection) {
381
+ svc.store.set(selectedItemPath, undefined);
382
+ svc.store.set(selectedZonePath, undefined);
383
+ }
384
+ canvasDispatch(onCanvasPressRaw, context);
385
+ };
386
+ }
387
+ }
388
+ }
389
+ // Wire file-upload primitive — inject upload state + callbacks from registry
390
+ if (node.type === 'file-upload' && _elementId && fileRegistryRef) {
391
+ const elementId = _elementId;
392
+ // Read upload state from store
393
+ const uploadFiles = svc.store.get(`/ui/uploads/${elementId}/files`);
394
+ if (uploadFiles)
395
+ restProps.uploadState = uploadFiles;
396
+ // Generate preview URLs for images
397
+ restProps.onFiles = (files) => {
398
+ fileRegistryRef.current.set(elementId, files);
399
+ // Generate preview URLs for image files
400
+ const states = files.map((f) => ({
401
+ name: f.name,
402
+ size: f.size,
403
+ type: f.type,
404
+ progress: 0,
405
+ status: 'pending',
406
+ previewUrl: f.type.startsWith('image/') ? URL.createObjectURL(f) : null,
407
+ error: null,
408
+ }));
409
+ svc.store.set(`/ui/uploads/${elementId}/files`, states);
410
+ // Auto-upload if configured (default: true)
411
+ const autoUpload = restProps.autoUpload !== false;
412
+ if (autoUpload && _eventBindings) {
413
+ const eventBindings = _eventBindings;
414
+ const uploadBinding = eventBindings.upload;
415
+ if (uploadBinding) {
416
+ // dispatchAction with internal params (File[] is not an Expression — cast needed for internal plumbing)
417
+ dispatchAction({ action: 'uploadFile', params: {
418
+ ...uploadBinding.params,
419
+ files, elementId,
420
+ accept: restProps.accept ?? '*',
421
+ maxSize: restProps.maxSize ?? 10_485_760,
422
+ } });
423
+ }
424
+ }
425
+ };
426
+ restProps.onRemove = (idx) => {
427
+ const currentFiles = fileRegistryRef.current.get(elementId) ?? [];
428
+ const currentStates = svc.store.get(`/ui/uploads/${elementId}/files`) ?? [];
429
+ // Revoke preview URL
430
+ if (currentStates[idx]?.previewUrl) {
431
+ URL.revokeObjectURL(currentStates[idx].previewUrl);
432
+ }
433
+ currentFiles.splice(idx, 1);
434
+ const newStates = currentStates.filter((_, i) => i !== idx);
435
+ fileRegistryRef.current.set(elementId, currentFiles);
436
+ svc.store.set(`/ui/uploads/${elementId}/files`, newStates);
437
+ };
438
+ restProps.onRetry = (idx) => {
439
+ const currentFiles = fileRegistryRef.current.get(elementId) ?? [];
440
+ const file = currentFiles[idx];
441
+ if (!file || !_eventBindings)
442
+ return;
443
+ const eventBindings = _eventBindings;
444
+ const uploadBinding = eventBindings.upload;
445
+ if (uploadBinding) {
446
+ // Reset this file's state
447
+ const currentStates = (svc.store.get(`/ui/uploads/${elementId}/files`) ?? []).slice();
448
+ if (currentStates[idx]) {
449
+ currentStates[idx] = { ...currentStates[idx], status: 'uploading', progress: 0, error: null };
450
+ svc.store.set(`/ui/uploads/${elementId}/files`, currentStates);
451
+ }
452
+ dispatchAction({ action: 'uploadFile', params: {
453
+ ...uploadBinding.params,
454
+ files: [file], elementId,
455
+ accept: restProps.accept ?? '*',
456
+ maxSize: restProps.maxSize ?? 10_485_760,
457
+ } });
458
+ }
459
+ };
460
+ }
461
+ // Wire store and onDismiss for toast-container primitives (spec mode)
462
+ if (node.type === 'toast-container') {
463
+ handlers.store = svc.store;
464
+ handlers.onDismiss = (id) => {
465
+ dispatchAction({ action: 'dismissNotification', params: { id } });
466
+ };
467
+ const iconRender = createIconRenderer(svc);
468
+ if (iconRender)
469
+ handlers.renderIcon = iconRender;
470
+ }
471
+ const children = node.children.length > 0
472
+ ? node.children.map((child, i) => withInspectId(renderNode(child, svc, dispatchAction, i, cssCollector, fileRegistryRef, skeletonMode, spec, emitElementIds), child.props._elementId, emitElementIds, child.key ?? `${child.props._elementId}-${i}`))
473
+ : undefined;
474
+ // Check if this element has any interaction or animation props
475
+ const hasInteractions = _hover || _active || _focus || _motion;
476
+ // Overlays (modal, drawer) handle _motion internally — they animate
477
+ // backdrop and content panel separately. Pass _motion as a prop instead
478
+ // of wrapping in motion.div (which breaks fixed positioning).
479
+ const isOverlay = node.type === 'modal' || node.type === 'drawer';
480
+ if (isOverlay && _motion) {
481
+ const overlayMotion = _motion;
482
+ const isExiting = restProps._exiting === true;
483
+ const hasExit = overlayMotion?.exit;
484
+ if (hasExit) {
485
+ // Wrap in AnimatePresence for exit animations (modal close, drawer close)
486
+ const overlayElement = React.createElement(motion.div, {
487
+ key: `overlay-${node.key ?? index}`,
488
+ initial: overlayMotion.initial,
489
+ animate: overlayMotion.animate,
490
+ exit: overlayMotion.exit,
491
+ transition: overlayMotion.transition,
492
+ layoutId: overlayMotion.layoutId,
493
+ style: { position: 'fixed', inset: 0, zIndex: 1000 },
494
+ }, React.createElement(Component, { ...restProps, ...handlers, key: node.key ?? index }, children));
495
+ return React.createElement(AnimatePresence, { mode: 'wait', key: `ap-overlay-${node.key ?? index}` }, isExiting ? null : overlayElement);
496
+ }
497
+ return React.createElement(Component, { ...restProps, ...handlers, _motion, key: node.key ?? index }, children);
498
+ }
499
+ if (hasInteractions) {
500
+ const hoverObj = _hover;
501
+ const activeObj = _active;
502
+ const focusObj = _focus;
503
+ const hasCssInteractions = hoverObj || activeObj || focusObj;
504
+ const motionNeeded = needsMotionWrapper(hoverObj, activeObj, focusObj);
505
+ const motionConfig = _motion;
506
+ const hasMotionAnimation = motionConfig?.initial || motionConfig?.animate || motionConfig?.exit;
507
+ // CSS-only path: no transform props in hover/active/focus
508
+ // May still have motion animation (initial/animate/exit) — mixed case
509
+ if (hasCssInteractions && !motionNeeded) {
510
+ // Dev warning: primitive must accept className for CSS hover to work
511
+ if (typeof process !== 'undefined' && process.env?.NODE_ENV !== 'production' && !CSS_HOVER_SUPPORTED.has(node.type)) {
512
+ console.warn(`[Mythik] Element "${_elementId ?? node.key ?? index}" (type: "${node.type}") has CSS hover/active/focus but "${node.type}" does not support className. The interaction will be silently ignored. Add className support to the primitive or use transform-based hover (scale, y, etc.) which uses a Motion wrapper instead.`);
513
+ }
514
+ const cssId = _elementId ?? node.key ?? `el-${index}`;
515
+ const className = hashId(String(cssId));
516
+ const transitionObj = _transition;
517
+ const cssRule = generateHoverCSS(className, {
518
+ hover: hoverObj,
519
+ active: activeObj,
520
+ focus: focusObj,
521
+ transition: transitionObj,
522
+ });
523
+ cssCollector?.set(className, cssRule);
524
+ if (hasMotionAnimation) {
525
+ // Mixed case: CSS handles hover/active/focus, motion.div handles animation
526
+ const motionProps = {};
527
+ if (motionConfig?.initial)
528
+ motionProps.initial = motionConfig.initial;
529
+ if (motionConfig?.animate)
530
+ motionProps.animate = resolveInfinity(motionConfig.animate);
531
+ if (motionConfig?.exit)
532
+ motionProps.exit = motionConfig.exit;
533
+ if (motionConfig?.transition)
534
+ motionProps.transition = resolveInfinity(motionConfig.transition);
535
+ if (motionConfig?.layoutId)
536
+ motionProps.layoutId = motionConfig.layoutId;
537
+ return React.createElement(motion.div, { key: node.key ?? index, ...motionProps }, React.createElement(Component, { ...restProps, ...handlers, className }, children));
538
+ }
539
+ // Pure CSS path: no wrapper at all
540
+ return React.createElement(Component, { ...restProps, ...handlers, className, key: node.key ?? index }, children);
541
+ }
542
+ // Motion wrapper path: transform-based interactions (scale, rotate, x, y)
543
+ const motionProps = {};
544
+ if (_hover)
545
+ motionProps.whileHover = _hover;
546
+ if (_active)
547
+ motionProps.whileTap = _active;
548
+ if (_focus)
549
+ motionProps.whileFocus = _focus;
550
+ if (_transition)
551
+ motionProps.transition = _transition;
552
+ if (motionConfig?.initial)
553
+ motionProps.initial = motionConfig.initial;
554
+ if (motionConfig?.animate)
555
+ motionProps.animate = resolveInfinity(motionConfig.animate);
556
+ if (motionConfig?.exit)
557
+ motionProps.exit = motionConfig.exit;
558
+ if (motionConfig?.layoutId)
559
+ motionProps.layoutId = motionConfig.layoutId;
560
+ if (motionConfig?.whileTap && !_active)
561
+ motionProps.whileTap = motionConfig.whileTap;
562
+ if (motionConfig?.whileHover && !_hover)
563
+ motionProps.whileHover = motionConfig.whileHover;
564
+ if (motionConfig?.transition && !_transition) {
565
+ motionProps.transition = resolveInfinity(motionConfig.transition);
566
+ }
567
+ // Wrap: motion.div is a transparent animation wrapper.
568
+ // The primitive keeps ALL its styles (layout, colors, borders, etc.)
569
+ // motion.div only handles transforms/opacity/animations.
570
+ //
571
+ // The wrapper must inherit visual shape props from the element so that:
572
+ // - borderRadius: hover hit area matches the visible rounded shape (no square flash)
573
+ // - overflow: clipped content stays clipped during transforms
574
+ // - position/top/right/etc: absolutely positioned elements don't collapse
575
+ const elementStyle = restProps.style;
576
+ const pos = elementStyle?.position;
577
+ const needsPositioning = pos === 'absolute' || pos === 'fixed';
578
+ const wrapperStyle = {};
579
+ const innerStyleOverrides = {};
580
+ // Always inherit borderRadius and overflow so the wrapper matches the element's shape
581
+ if (elementStyle?.borderRadius != null) {
582
+ wrapperStyle.borderRadius = elementStyle.borderRadius;
583
+ }
584
+ if (elementStyle?.overflow != null) {
585
+ wrapperStyle.overflow = elementStyle.overflow;
586
+ }
587
+ // For absolutely/fixed positioned elements: move positioning to wrapper
588
+ if (needsPositioning) {
589
+ wrapperStyle.position = elementStyle.position;
590
+ wrapperStyle.top = elementStyle.top;
591
+ wrapperStyle.right = elementStyle.right;
592
+ wrapperStyle.bottom = elementStyle.bottom;
593
+ wrapperStyle.left = elementStyle.left;
594
+ wrapperStyle.zIndex = elementStyle.zIndex;
595
+ innerStyleOverrides.position = undefined;
596
+ innerStyleOverrides.top = undefined;
597
+ innerStyleOverrides.right = undefined;
598
+ innerStyleOverrides.bottom = undefined;
599
+ innerStyleOverrides.left = undefined;
600
+ innerStyleOverrides.zIndex = undefined;
601
+ }
602
+ const hasWrapperStyle = Object.keys(wrapperStyle).length > 0;
603
+ const hasInnerOverrides = Object.keys(innerStyleOverrides).length > 0;
604
+ const innerStyle = hasInnerOverrides
605
+ ? { ...elementStyle, ...innerStyleOverrides }
606
+ : elementStyle;
607
+ const needsPresence = motionProps.exit || motionProps.layoutId;
608
+ const isExiting = restProps._exiting === true;
609
+ const motionElement = React.createElement(motion.div, {
610
+ key: `m-${node.key ?? index}`,
611
+ style: hasWrapperStyle ? wrapperStyle : undefined,
612
+ ...motionProps,
613
+ }, React.createElement(Component, { ...restProps, ...handlers, style: innerStyle }, children));
614
+ // Wrap in AnimatePresence for exit animations and layoutId transitions
615
+ if (needsPresence) {
616
+ return React.createElement(AnimatePresence, { mode: 'popLayout', key: `ap-${node.key ?? index}` }, isExiting ? null : motionElement);
617
+ }
618
+ return motionElement;
619
+ }
620
+ // No interactions — render directly (zero overhead)
621
+ return React.createElement(Component, { ...restProps, ...handlers, key: node.key ?? index }, children);
622
+ }
623
+ export function MythikRenderer({ spec, config = {}, instance, autoDeviceContext = true, fetcher, storage, storageConfig, exportAdapters, autoSkeleton = true, emitElementIds = false, onSpecRuntimeMount }) {
624
+ const svc = React.useMemo(() => {
625
+ if (instance)
626
+ return instance;
627
+ const s = createMythik(config);
628
+ registerReactPrimitives(s.plugins);
629
+ s.applyPlugins();
630
+ return s;
631
+ }, [instance, config]);
632
+ // Auto-track device context (viewport, platform, orientation, colorScheme)
633
+ useDeviceContext(svc.store, autoDeviceContext);
634
+ // Create form engine if spec has forms config
635
+ const formEngine = React.useMemo(() => {
636
+ if (!spec.forms || Object.keys(spec.forms).length === 0)
637
+ return undefined;
638
+ return createFormEngine({
639
+ store: svc.store,
640
+ resolve: (expr) => svc.resolver.resolve(expr),
641
+ forms: spec.forms,
642
+ });
643
+ }, [spec, svc]);
644
+ // Cleanup form engine on unmount or spec change
645
+ React.useEffect(() => {
646
+ return () => { formEngine?.destroy(); };
647
+ }, [formEngine]);
648
+ // File registry — holds File objects in refs, never in state (binary data stays out of store)
649
+ const fileRegistryRef = React.useRef(new Map());
650
+ // Create action dispatcher with security guards + plugin actions + form engine + framework fetch + storage
651
+ const dispatcher = React.useMemo(() => {
652
+ const d = createActionDispatcher({
653
+ store: svc.store,
654
+ customActions: svc.plugins.getActions(),
655
+ urlGuard: svc.security?.urlGuard,
656
+ stateGuard: svc.security?.stateGuard,
657
+ rateLimiter: svc.security?.rateLimiter,
658
+ formEngine,
659
+ fetcher,
660
+ storage,
661
+ storageConfig,
662
+ exportAdapters,
663
+ });
664
+ return d;
665
+ }, [svc, formEngine, fetcher, storage, storageConfig, exportAdapters]);
666
+ // Create transaction engine for optimistic updates
667
+ const txEngine = React.useMemo(() => {
668
+ return createTransactionEngine({
669
+ store: svc.store,
670
+ dispatcher,
671
+ resolve: (expr) => svc.resolver.resolve(expr),
672
+ });
673
+ }, [svc, dispatcher]);
674
+ // Dispatch function — handles standard actions, arrays, and transactions
675
+ const dispatchAction = React.useCallback((binding) => {
676
+ // Transaction binding
677
+ if (isTransactionBinding(binding)) {
678
+ txEngine.execute(binding.transaction).catch((err) => {
679
+ console.error('Transaction failed:', err);
680
+ });
681
+ return;
682
+ }
683
+ // Standard action binding(s)
684
+ const bindings = Array.isArray(binding) ? binding : [binding];
685
+ (async () => {
686
+ for (const b of bindings) {
687
+ try {
688
+ const promise = dispatcher.dispatch(b, (expr) => svc.resolver.resolve(expr));
689
+ if (!b.fireAndForget) {
690
+ await promise;
691
+ }
692
+ }
693
+ catch (err) {
694
+ // Auth actions log only in development — prevents leaking stack traces in production
695
+ const authActions = ['login', 'logout', 'refreshSession'];
696
+ if (!authActions.includes(b.action) || process.env.NODE_ENV !== 'production') {
697
+ console.error(`Action "${b.action}" failed:`, err);
698
+ }
699
+ }
700
+ }
701
+ })();
702
+ }, [dispatcher, svc, txEngine]);
703
+ // Re-render when state changes (batched + incremental)
704
+ // Collect changed paths between frames, pass to engine for selective re-render
705
+ // NOTE: declared BEFORE mountSpecRuntime so the subscription is attached
706
+ // before deriveEngine.mount() writes initial derive values — otherwise the
707
+ // first-paint synchronous writes would land before any listener exists.
708
+ const changedPathsRef = React.useRef(new Set());
709
+ const [, setTick] = React.useState(0);
710
+ React.useEffect(() => {
711
+ let frameId = null;
712
+ const unsubscribe = svc.store.subscribe((_state, changedPath) => {
713
+ changedPathsRef.current.add(changedPath);
714
+ if (frameId === null) {
715
+ frameId = requestAnimationFrame(() => {
716
+ frameId = null;
717
+ setTick((t) => t + 1);
718
+ });
719
+ }
720
+ });
721
+ return () => {
722
+ unsubscribe();
723
+ if (frameId !== null)
724
+ cancelAnimationFrame(frameId);
725
+ };
726
+ }, [svc]);
727
+ // ── v49 Item E: per-spec runtime (derive + dataSources engines) ──
728
+ React.useEffect(() => {
729
+ const runtime = mountSpecRuntime(spec, {
730
+ store: svc.store,
731
+ resolver: svc.resolver,
732
+ dispatcher,
733
+ protectionRegistry: svc.protectionRegistry,
734
+ fetcher,
735
+ urlGuard: svc.security?.urlGuard,
736
+ });
737
+ onSpecRuntimeMount?.(runtime);
738
+ return () => {
739
+ onSpecRuntimeMount?.(null);
740
+ runtime.unmount();
741
+ };
742
+ }, [spec, dispatcher, svc, fetcher, onSpecRuntimeMount]);
743
+ // Execute initial actions on mount (e.g., fetch data from Supabase)
744
+ React.useEffect(() => {
745
+ if (spec.initialActions && spec.initialActions.length > 0) {
746
+ (async () => {
747
+ for (const binding of spec.initialActions) {
748
+ try {
749
+ if (isTransactionBinding(binding)) {
750
+ await txEngine.execute(binding.transaction);
751
+ }
752
+ else {
753
+ await dispatcher.dispatch(binding, (expr) => svc.resolver.resolve(expr));
754
+ }
755
+ }
756
+ catch (err) {
757
+ const label = 'action' in binding ? binding.action : 'transaction';
758
+ console.error(`Initial action "${label}" failed:`, err);
759
+ }
760
+ }
761
+ })();
762
+ }
763
+ }, [spec, dispatcher, svc]);
764
+ // Clipboard bridge — write to browser clipboard when /ui/clipboard changes
765
+ React.useEffect(() => {
766
+ const unsub = svc.store.subscribe((_s, path) => {
767
+ if (path === RESERVED_PATHS.CLIPBOARD) {
768
+ const data = svc.store.get(RESERVED_PATHS.CLIPBOARD);
769
+ if (data?.value && typeof navigator !== 'undefined' && navigator.clipboard) {
770
+ navigator.clipboard.writeText(String(data.value)).catch(() => { });
771
+ }
772
+ }
773
+ });
774
+ return unsub;
775
+ }, [svc]);
776
+ // Pass collected changedPaths for incremental render, then clear
777
+ const changedPaths = changedPathsRef.current.size > 0 ? changedPathsRef.current : undefined;
778
+ const tree = svc.engine.render(spec, changedPaths);
779
+ changedPathsRef.current = new Set();
780
+ // Auto-skeleton detection
781
+ const isLoading = svc.store.get(RESERVED_PATHS.LOADING) === true;
782
+ const skeletonMode = React.useMemo(() => {
783
+ if (!autoSkeleton || !isLoading)
784
+ return false;
785
+ const hasFetch = spec.initialActions?.some((a) => !isTransactionBinding(a) && a.action === 'fetch');
786
+ if (!hasFetch)
787
+ return false;
788
+ const fetchTargets = (spec.initialActions ?? [])
789
+ .filter((a) => !isTransactionBinding(a))
790
+ .filter((a) => a.action === 'fetch')
791
+ .map((a) => a.params?.target)
792
+ .filter(Boolean);
793
+ return fetchTargets.some((t) => {
794
+ const data = svc.store.get(t);
795
+ return data === undefined || data === null || (Array.isArray(data) && data.length === 0);
796
+ });
797
+ }, [autoSkeleton, isLoading, spec, svc]);
798
+ // Collect render errors and write to /ui/renderErrors in dev mode (deduped to prevent loops)
799
+ const exposeErrors = svc.security?.exposeErrors !== false;
800
+ const prevErrorKeyRef = React.useRef('');
801
+ React.useEffect(() => {
802
+ if (!exposeErrors)
803
+ return;
804
+ const errors = [];
805
+ function collectErrors(node) {
806
+ if (node.type === '_error') {
807
+ errors.push({
808
+ elementId: node.props.elementId,
809
+ message: node.props.error,
810
+ type: node.props.originalType,
811
+ });
812
+ }
813
+ for (const child of node.children) {
814
+ collectErrors(child);
815
+ }
816
+ }
817
+ collectErrors(tree);
818
+ const errorKey = errors.map(e => `${e.elementId}:${e.message}`).join('|');
819
+ if (errorKey === prevErrorKeyRef.current)
820
+ return; // Same errors — skip write to prevent loop
821
+ prevErrorKeyRef.current = errorKey;
822
+ if (errors.length > 0) {
823
+ svc.store.set(RESERVED_PATHS.RENDER_ERRORS, errors);
824
+ }
825
+ else {
826
+ const existing = svc.store.get(RESERVED_PATHS.RENDER_ERRORS);
827
+ if (existing)
828
+ svc.store.set(RESERVED_PATHS.RENDER_ERRORS, undefined);
829
+ }
830
+ });
831
+ // Collect CSS rules for CSS-based hover/active/focus
832
+ const cssCollector = React.useMemo(() => new Map(), []);
833
+ cssCollector.clear(); // Clear before each render pass
834
+ const rendered = withInspectId(renderNode(tree, svc, dispatchAction, 0, cssCollector, fileRegistryRef, skeletonMode, spec, emitElementIds), tree.props._elementId, emitElementIds);
835
+ const cssText = cssCollector.size > 0
836
+ ? Array.from(cssCollector.values()).join('\n')
837
+ : null;
838
+ // Auto-inject ToastContainer if spec doesn't include one
839
+ const specHasToastContainer = React.useMemo(() => {
840
+ return Object.values(spec.elements).some((el) => el.type === 'toast-container');
841
+ }, [spec]);
842
+ const toastElement = !specHasToastContainer
843
+ ? React.createElement(ToastContainer, {
844
+ store: svc.store,
845
+ onDismiss: (id) => {
846
+ dispatchAction({ action: 'dismissNotification', params: { id } });
847
+ },
848
+ renderIcon: createIconRenderer(svc),
849
+ })
850
+ : null;
851
+ // Plan 3 Task 20 — root mount for LayerBackground v2 consumer path.
852
+ // When `tokens.identity.background` is a LayerBackground (has color or
853
+ // layers), wrap the rendered tree in a positioned container and mount
854
+ // <BackgroundStack>. Palette threads from tokens.colors so blob layers
855
+ // render the real BlobLayer (Task 18) instead of the stub. Legacy
856
+ // BackgroundConfig was deleted in Task 21 — the isLayerBackground
857
+ // rejection of `{ style: ... }` shapes now serves as malformed-spec
858
+ // defense rather than coexistence handling.
859
+ // Tokens source (Task 23 follow-up): read from `/tokens/resolved` in the
860
+ // store — factory persists the fully-resolved token tree there at
861
+ // creation and after every updateTokens call. This is the single reliable
862
+ // source for DNA-derived colors + current identity.background shape.
863
+ // Falls back to `spec.tokens` for test harnesses that attach tokens
864
+ // inline on the spec without going through createMythik config.
865
+ const resolvedTokens = svc.store.get(RESERVED_PATHS.RESOLVED_TOKENS);
866
+ const resolvedIdentity = resolvedTokens?.identity;
867
+ const specInline = spec.tokens;
868
+ const specInlineIdentity = specInline?.identity;
869
+ // identity.background priority: spec-inline (explicit consumer intent) →
870
+ // resolved store value.
871
+ const backgroundRaw = specInlineIdentity?.background ?? resolvedIdentity?.background;
872
+ const specColors = specInline?.colors
873
+ ?? resolvedTokens?.colors;
874
+ const layerBackground = isLayerBackground(backgroundRaw) ? backgroundRaw : undefined;
875
+ let palette;
876
+ if (layerBackground) {
877
+ if (specColors && typeof specColors.primary === 'string' && typeof specColors.accent === 'string') {
878
+ palette = { primary: specColors.primary, accent: specColors.accent };
879
+ }
880
+ else if (process.env.NODE_ENV !== 'production') {
881
+ // Task 20 review M3 — surface malformed tokens.colors at the renderer
882
+ // seam, more diagnostic than BackgroundStack's generic "no palette"
883
+ // warn because we know the user intended a LayerBackground here.
884
+ // eslint-disable-next-line no-console
885
+ console.warn('MythikRenderer: identity.background is a LayerBackground but tokens.colors.{primary,accent} are missing or non-string — blob layers will fall back to the stub. Check tokens shape.');
886
+ }
887
+ }
888
+ const body = (_jsx(RenderErrorBoundary, { exposeErrors: exposeErrors, resetKey: spec, children: _jsxs(_Fragment, { children: [cssText && _jsx("style", { "data-mythik-hover": true, children: cssText }), skeletonMode && _jsx("style", { "data-mythik-skeleton": true, children: SHIMMER_CSS }), rendered, toastElement] }) }));
889
+ if (layerBackground) {
890
+ // Task 20 review C1 — `isolation: isolate` creates a new stacking context
891
+ // so per-layer zIndex values (which resolveCommon defaults to array-index,
892
+ // so a 3-layer stack reaches zIndex 3) compete only among siblings, not
893
+ // against the content wrapper's `CONTENT_Z_INDEX`. Review I1 — `minHeight`
894
+ // removed: the renderer should not invent layout height; consumers
895
+ // determine the mount-point dimensions.
896
+ return (_jsx(LayoutGroup, { children: _jsxs("div", { "data-sv-renderer-root": "v2", style: { position: 'relative' }, children: [_jsx("div", { style: { position: 'absolute', inset: 0, isolation: 'isolate', zIndex: 0, pointerEvents: 'none' }, children: _jsx(BackgroundStack, { background: layerBackground, palette: palette }) }), _jsx("div", { style: { position: 'relative', zIndex: CONTENT_Z_INDEX }, children: body })] }) }));
897
+ }
898
+ return (_jsx(LayoutGroup, { children: body }));
899
+ }
900
+ //# sourceMappingURL=MythikRenderer.js.map