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.
- package/LICENSE +201 -0
- package/NOTICE +4 -0
- package/README.md +83 -0
- package/dist/MythikApp.d.ts +61 -0
- package/dist/MythikApp.d.ts.map +1 -0
- package/dist/MythikApp.js +381 -0
- package/dist/MythikApp.js.map +1 -0
- package/dist/MythikRenderer.d.ts +31 -0
- package/dist/MythikRenderer.d.ts.map +1 -0
- package/dist/MythikRenderer.js +900 -0
- package/dist/MythikRenderer.js.map +1 -0
- package/dist/animation/index.d.ts +7 -0
- package/dist/animation/index.d.ts.map +1 -0
- package/dist/animation/index.js +5 -0
- package/dist/animation/index.js.map +1 -0
- package/dist/animation/stylesheet-singleton.d.ts +12 -0
- package/dist/animation/stylesheet-singleton.d.ts.map +1 -0
- package/dist/animation/stylesheet-singleton.js +107 -0
- package/dist/animation/stylesheet-singleton.js.map +1 -0
- package/dist/animation/useElementAnimations.d.ts +30 -0
- package/dist/animation/useElementAnimations.d.ts.map +1 -0
- package/dist/animation/useElementAnimations.js +254 -0
- package/dist/animation/useElementAnimations.js.map +1 -0
- package/dist/animation/usePrefersReducedMotion.d.ts +2 -0
- package/dist/animation/usePrefersReducedMotion.d.ts.map +1 -0
- package/dist/animation/usePrefersReducedMotion.js +29 -0
- package/dist/animation/usePrefersReducedMotion.js.map +1 -0
- package/dist/animation/useShapeAnimations.d.ts +21 -0
- package/dist/animation/useShapeAnimations.d.ts.map +1 -0
- package/dist/animation/useShapeAnimations.js +119 -0
- package/dist/animation/useShapeAnimations.js.map +1 -0
- package/dist/app-context.d.ts +15 -0
- package/dist/app-context.d.ts.map +1 -0
- package/dist/app-context.js +9 -0
- package/dist/app-context.js.map +1 -0
- package/dist/background/BackgroundLayer.d.ts +7 -0
- package/dist/background/BackgroundLayer.d.ts.map +1 -0
- package/dist/background/BackgroundLayer.js +50 -0
- package/dist/background/BackgroundLayer.js.map +1 -0
- package/dist/background/BackgroundStack.d.ts +19 -0
- package/dist/background/BackgroundStack.d.ts.map +1 -0
- package/dist/background/BackgroundStack.js +59 -0
- package/dist/background/BackgroundStack.js.map +1 -0
- package/dist/background/BlobLayer.d.ts +12 -0
- package/dist/background/BlobLayer.d.ts.map +1 -0
- package/dist/background/BlobLayer.js +60 -0
- package/dist/background/BlobLayer.js.map +1 -0
- package/dist/background/index.d.ts +3 -0
- package/dist/background/index.d.ts.map +1 -0
- package/dist/background/index.js +3 -0
- package/dist/background/index.js.map +1 -0
- package/dist/css-hover.d.ts +15 -0
- package/dist/css-hover.d.ts.map +1 -0
- package/dist/css-hover.js +51 -0
- package/dist/css-hover.js.map +1 -0
- package/dist/index.d.ts +10 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +11 -0
- package/dist/index.js.map +1 -0
- package/dist/primitives/accordion.d.ts +12 -0
- package/dist/primitives/accordion.d.ts.map +1 -0
- package/dist/primitives/accordion.js +25 -0
- package/dist/primitives/accordion.js.map +1 -0
- package/dist/primitives/area-chart.d.ts +14 -0
- package/dist/primitives/area-chart.d.ts.map +1 -0
- package/dist/primitives/area-chart.js +18 -0
- package/dist/primitives/area-chart.js.map +1 -0
- package/dist/primitives/audio-player.d.ts +9 -0
- package/dist/primitives/audio-player.d.ts.map +1 -0
- package/dist/primitives/audio-player.js +5 -0
- package/dist/primitives/audio-player.js.map +1 -0
- package/dist/primitives/bar-chart.d.ts +14 -0
- package/dist/primitives/bar-chart.d.ts.map +1 -0
- package/dist/primitives/bar-chart.js +22 -0
- package/dist/primitives/bar-chart.js.map +1 -0
- package/dist/primitives/box.d.ts +21 -0
- package/dist/primitives/box.d.ts.map +1 -0
- package/dist/primitives/box.js +54 -0
- package/dist/primitives/box.js.map +1 -0
- package/dist/primitives/button.d.ts +14 -0
- package/dist/primitives/button.d.ts.map +1 -0
- package/dist/primitives/button.js +28 -0
- package/dist/primitives/button.js.map +1 -0
- package/dist/primitives/camera.d.ts +15 -0
- package/dist/primitives/camera.d.ts.map +1 -0
- package/dist/primitives/camera.js +25 -0
- package/dist/primitives/camera.js.map +1 -0
- package/dist/primitives/checkbox.d.ts +12 -0
- package/dist/primitives/checkbox.d.ts.map +1 -0
- package/dist/primitives/checkbox.js +24 -0
- package/dist/primitives/checkbox.js.map +1 -0
- package/dist/primitives/divider.d.ts +9 -0
- package/dist/primitives/divider.d.ts.map +1 -0
- package/dist/primitives/divider.js +10 -0
- package/dist/primitives/divider.js.map +1 -0
- package/dist/primitives/drawer.d.ts +21 -0
- package/dist/primitives/drawer.d.ts.map +1 -0
- package/dist/primitives/drawer.js +38 -0
- package/dist/primitives/drawer.js.map +1 -0
- package/dist/primitives/file-upload.d.ts +27 -0
- package/dist/primitives/file-upload.d.ts.map +1 -0
- package/dist/primitives/file-upload.js +225 -0
- package/dist/primitives/file-upload.js.map +1 -0
- package/dist/primitives/grid.d.ts +13 -0
- package/dist/primitives/grid.d.ts.map +1 -0
- package/dist/primitives/grid.js +13 -0
- package/dist/primitives/grid.js.map +1 -0
- package/dist/primitives/icon.d.ts +22 -0
- package/dist/primitives/icon.d.ts.map +1 -0
- package/dist/primitives/icon.js +52 -0
- package/dist/primitives/icon.js.map +1 -0
- package/dist/primitives/image.d.ts +13 -0
- package/dist/primitives/image.d.ts.map +1 -0
- package/dist/primitives/image.js +38 -0
- package/dist/primitives/image.js.map +1 -0
- package/dist/primitives/index.d.ts +57 -0
- package/dist/primitives/index.d.ts.map +1 -0
- package/dist/primitives/index.js +106 -0
- package/dist/primitives/index.js.map +1 -0
- package/dist/primitives/input.d.ts +32 -0
- package/dist/primitives/input.d.ts.map +1 -0
- package/dist/primitives/input.js +192 -0
- package/dist/primitives/input.js.map +1 -0
- package/dist/primitives/kanban-board.d.ts +13 -0
- package/dist/primitives/kanban-board.d.ts.map +1 -0
- package/dist/primitives/kanban-board.js +5 -0
- package/dist/primitives/kanban-board.js.map +1 -0
- package/dist/primitives/line-chart.d.ts +14 -0
- package/dist/primitives/line-chart.d.ts.map +1 -0
- package/dist/primitives/line-chart.js +17 -0
- package/dist/primitives/line-chart.js.map +1 -0
- package/dist/primitives/list.d.ts +13 -0
- package/dist/primitives/list.d.ts.map +1 -0
- package/dist/primitives/list.js +10 -0
- package/dist/primitives/list.js.map +1 -0
- package/dist/primitives/modal.d.ts +20 -0
- package/dist/primitives/modal.d.ts.map +1 -0
- package/dist/primitives/modal.js +60 -0
- package/dist/primitives/modal.js.map +1 -0
- package/dist/primitives/pie-chart.d.ts +15 -0
- package/dist/primitives/pie-chart.d.ts.map +1 -0
- package/dist/primitives/pie-chart.js +36 -0
- package/dist/primitives/pie-chart.js.map +1 -0
- package/dist/primitives/screen-outlet.d.ts +9 -0
- package/dist/primitives/screen-outlet.d.ts.map +1 -0
- package/dist/primitives/screen-outlet.js +92 -0
- package/dist/primitives/screen-outlet.js.map +1 -0
- package/dist/primitives/screen.d.ts +9 -0
- package/dist/primitives/screen.d.ts.map +1 -0
- package/dist/primitives/screen.js +10 -0
- package/dist/primitives/screen.js.map +1 -0
- package/dist/primitives/scroll.d.ts +11 -0
- package/dist/primitives/scroll.d.ts.map +1 -0
- package/dist/primitives/scroll.js +10 -0
- package/dist/primitives/scroll.js.map +1 -0
- package/dist/primitives/select.d.ts +19 -0
- package/dist/primitives/select.d.ts.map +1 -0
- package/dist/primitives/select.js +109 -0
- package/dist/primitives/select.js.map +1 -0
- package/dist/primitives/signature.d.ts +13 -0
- package/dist/primitives/signature.d.ts.map +1 -0
- package/dist/primitives/signature.js +45 -0
- package/dist/primitives/signature.js.map +1 -0
- package/dist/primitives/skeleton.d.ts +14 -0
- package/dist/primitives/skeleton.d.ts.map +1 -0
- package/dist/primitives/skeleton.js +41 -0
- package/dist/primitives/skeleton.js.map +1 -0
- package/dist/primitives/slider.d.ts +15 -0
- package/dist/primitives/slider.d.ts.map +1 -0
- package/dist/primitives/slider.js +7 -0
- package/dist/primitives/slider.js.map +1 -0
- package/dist/primitives/spacer.d.ts +9 -0
- package/dist/primitives/spacer.d.ts.map +1 -0
- package/dist/primitives/spacer.js +9 -0
- package/dist/primitives/spacer.js.map +1 -0
- package/dist/primitives/spatial-map-editing.d.ts +472 -0
- package/dist/primitives/spatial-map-editing.d.ts.map +1 -0
- package/dist/primitives/spatial-map-editing.js +886 -0
- package/dist/primitives/spatial-map-editing.js.map +1 -0
- package/dist/primitives/spatial-map.d.ts +1073 -0
- package/dist/primitives/spatial-map.d.ts.map +1 -0
- package/dist/primitives/spatial-map.js +1705 -0
- package/dist/primitives/spatial-map.js.map +1 -0
- package/dist/primitives/stack.d.ts +13 -0
- package/dist/primitives/stack.d.ts.map +1 -0
- package/dist/primitives/stack.js +12 -0
- package/dist/primitives/stack.js.map +1 -0
- package/dist/primitives/table.d.ts +115 -0
- package/dist/primitives/table.d.ts.map +1 -0
- package/dist/primitives/table.js +498 -0
- package/dist/primitives/table.js.map +1 -0
- package/dist/primitives/tabs.d.ts +17 -0
- package/dist/primitives/tabs.d.ts.map +1 -0
- package/dist/primitives/tabs.js +13 -0
- package/dist/primitives/tabs.js.map +1 -0
- package/dist/primitives/text.d.ts +11 -0
- package/dist/primitives/text.d.ts.map +1 -0
- package/dist/primitives/text.js +69 -0
- package/dist/primitives/text.js.map +1 -0
- package/dist/primitives/textarea.d.ts +15 -0
- package/dist/primitives/textarea.d.ts.map +1 -0
- package/dist/primitives/textarea.js +23 -0
- package/dist/primitives/textarea.js.map +1 -0
- package/dist/primitives/toast-container.d.ts +15 -0
- package/dist/primitives/toast-container.d.ts.map +1 -0
- package/dist/primitives/toast-container.js +160 -0
- package/dist/primitives/toast-container.js.map +1 -0
- package/dist/primitives/toggle.d.ts +12 -0
- package/dist/primitives/toggle.d.ts.map +1 -0
- package/dist/primitives/toggle.js +18 -0
- package/dist/primitives/toggle.js.map +1 -0
- package/dist/primitives/touchable.d.ts +10 -0
- package/dist/primitives/touchable.d.ts.map +1 -0
- package/dist/primitives/touchable.js +6 -0
- package/dist/primitives/touchable.js.map +1 -0
- package/dist/primitives/use-design-tokens.d.ts +127 -0
- package/dist/primitives/use-design-tokens.d.ts.map +1 -0
- package/dist/primitives/use-design-tokens.js +251 -0
- package/dist/primitives/use-design-tokens.js.map +1 -0
- package/dist/primitives/use-theme.d.ts +11 -0
- package/dist/primitives/use-theme.d.ts.map +1 -0
- package/dist/primitives/use-theme.js +17 -0
- package/dist/primitives/use-theme.js.map +1 -0
- package/dist/primitives/wizard.d.ts +11 -0
- package/dist/primitives/wizard.d.ts.map +1 -0
- package/dist/primitives/wizard.js +15 -0
- package/dist/primitives/wizard.js.map +1 -0
- package/dist/runtime/context-dispatcher.d.ts +3 -0
- package/dist/runtime/context-dispatcher.d.ts.map +1 -0
- package/dist/runtime/context-dispatcher.js +11 -0
- package/dist/runtime/context-dispatcher.js.map +1 -0
- package/dist/runtime/row-dispatcher.d.ts +19 -0
- package/dist/runtime/row-dispatcher.d.ts.map +1 -0
- package/dist/runtime/row-dispatcher.js +25 -0
- package/dist/runtime/row-dispatcher.js.map +1 -0
- package/dist/types.d.ts +10 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +2 -0
- package/dist/types.js.map +1 -0
- package/dist/use-device-context.d.ts +8 -0
- package/dist/use-device-context.d.ts.map +1 -0
- package/dist/use-device-context.js +54 -0
- package/dist/use-device-context.js.map +1 -0
- 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
|