@marver-design/marver 0.2.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 (51) hide show
  1. package/LICENSE +202 -0
  2. package/NOTICE +4 -0
  3. package/README.md +38 -0
  4. package/dist/build-D0GnIR4G.mjs +244 -0
  5. package/dist/cli.mjs +58 -0
  6. package/dist/config-DMBEpdEN.mjs +132 -0
  7. package/dist/dev-BK4x3PBr.mjs +76 -0
  8. package/dist/init-DcOy1krf.mjs +168 -0
  9. package/dist/plugin-DB5t2WUl.mjs +437 -0
  10. package/dist/serve-BPNmWeJx.mjs +200 -0
  11. package/package.json +59 -0
  12. package/src/client/const.ts +5 -0
  13. package/src/client/frame-host/bridge.js +37 -0
  14. package/src/client/frame-host/index.html +11 -0
  15. package/src/client/frame-host/main.tsx +73 -0
  16. package/src/client/frame-host/registry.ts +40 -0
  17. package/src/client/runtime/index.ts +8 -0
  18. package/src/client/shell/App.tsx +633 -0
  19. package/src/client/shell/Play.tsx +399 -0
  20. package/src/client/shell/Tip.tsx +43 -0
  21. package/src/client/shell/canvas/Canvas.tsx +285 -0
  22. package/src/client/shell/canvas/FrameNode.tsx +194 -0
  23. package/src/client/shell/favicon/android-chrome-192x192-interactive.png +0 -0
  24. package/src/client/shell/favicon/android-chrome-192x192.png +0 -0
  25. package/src/client/shell/favicon/android-chrome-512x512-interactive.png +0 -0
  26. package/src/client/shell/favicon/android-chrome-512x512.png +0 -0
  27. package/src/client/shell/favicon/apple-touch-icon-interactive.png +0 -0
  28. package/src/client/shell/favicon/apple-touch-icon.png +0 -0
  29. package/src/client/shell/favicon/favicon-16x16-interactive.png +0 -0
  30. package/src/client/shell/favicon/favicon-16x16.png +0 -0
  31. package/src/client/shell/favicon/favicon-32x32-interactive.png +0 -0
  32. package/src/client/shell/favicon/favicon-32x32.png +0 -0
  33. package/src/client/shell/favicon/favicon.ico +0 -0
  34. package/src/client/shell/favicon/site.webmanifest +11 -0
  35. package/src/client/shell/hash.ts +67 -0
  36. package/src/client/shell/icons.tsx +53 -0
  37. package/src/client/shell/index.html +17 -0
  38. package/src/client/shell/main.tsx +9 -0
  39. package/src/client/shell/store.ts +540 -0
  40. package/src/client/shell/styles.css +508 -0
  41. package/src/client/shell/tidy.ts +24 -0
  42. package/src/client/stage/index.html +11 -0
  43. package/src/client/stage/main.tsx +160 -0
  44. package/templates/AGENTS-embedded.md +76 -0
  45. package/templates/AGENTS-studio.md +76 -0
  46. package/templates/demo/_fixtures.ts +9 -0
  47. package/templates/demo/dark.tsx +17 -0
  48. package/templates/demo/form.tsx +51 -0
  49. package/templates/demo/welcome.tsx +21 -0
  50. package/templates/design-tsconfig.json +8 -0
  51. package/templates/root-layout.tsx +7 -0
@@ -0,0 +1,508 @@
1
+ /* Theme-aware glass shell. Tokens live on .sh-app; .sh-app.dark overrides them.
2
+ Light board -> light glass chrome over a paper canvas; dark board -> dark glass
3
+ chrome over a dark canvas. Frames are cards; chrome is glass. */
4
+
5
+ .sh-app {
6
+ --ground: #e7e9ef; --dot: #c9cbd5;
7
+ --accent: #0088ff; --accent-ring: rgba(0, 136, 255, .2); --beam: rgba(0, 136, 255, .42); --beam-blur: 6px; --beam-bottom: -13px; --beam-h: 12px; --beam-inset: 12%; --edge-accent: rgba(0, 136, 255, .8); --inner-tint: rgba(0, 136, 255, .13); --tip-bg: #18181b; --tip-ink: #fafafa; --accent-wash: rgba(0, 136, 255, .07);
8
+
9
+ --glass: rgba(255, 255, 255, .64);
10
+ --glass-brd: rgba(24, 24, 27, .1);
11
+ --glass-ink: #18181b;
12
+ --glass-ink-2: rgba(24, 24, 27, .66);
13
+ --glass-ink-3: rgba(24, 24, 27, .4);
14
+ --glass-hover: rgba(24, 24, 27, .06);
15
+ --glass-accent: #0077e6;
16
+ --glass-accent-bg: rgba(0, 136, 255, .13);
17
+ --glass-good: #0e6b57;
18
+ --glass-warn: #7a4a00;
19
+ --shadow-glass: inset 0 1px 0 rgba(255, 255, 255, .65), 0 1px 2px rgba(24, 24, 27, .05), 0 4px 12px -6px rgba(24, 24, 27, .12);
20
+
21
+ --node-bg: #fff; --node-brd: #dddee5;
22
+ --head-bg: rgba(255, 255, 255, .55);
23
+ --head-sheen: linear-gradient(100deg, rgba(255, 255, 255, .85), rgba(255, 255, 255, .1) 45%, rgba(255, 255, 255, .45));
24
+ --edge-light: linear-gradient(155deg, rgba(255, 255, 255, .95), rgba(255, 255, 255, .2) 30%, rgba(24, 24, 27, .04) 65%, rgba(255, 255, 255, .55));
25
+ --head-ink: #18181b; --head-dim: rgba(24, 24, 27, .4);
26
+ --card-err-ink: #b3261e; --card-err-bg: #fceceb;
27
+ --card-warn-ink: #7a4a00; --card-warn-bg: #fdf3e1;
28
+ --interact: #db35f2; --interact-ring: rgba(219, 53, 242, .16);
29
+ --interact-strong: rgba(219, 53, 242, 1); --interact-soft: rgba(234, 141, 255, .95);
30
+ --interact-deep: rgba(176, 47, 194, .85); --interact-spark: rgba(255, 255, 255, .95);
31
+
32
+ --r-node: 10px; --r-panel: 24px; --edge: 10px;
33
+ --blur: blur(20px) saturate(150%);
34
+ --shadow-node: 0 1px 2px rgba(24, 24, 27, .05); /* tailwind shadow-sm: present, not loud */
35
+ --morph: .26s cubic-bezier(.32, .72, .35, 1);
36
+ }
37
+ .sh-app.dark {
38
+ --ground: #1b1c23; --dot: #35363f;
39
+ --accent: #0088ff; --accent-ring: rgba(0, 136, 255, .32); --beam: rgba(0, 136, 255, .5); --beam-blur: 3px; --beam-bottom: -13px; --beam-h: 16px; --beam-inset: 20%; --edge-accent: rgba(92, 179, 255, .85); --inner-tint: rgba(92, 179, 255, .16); --tip-bg: #f4f4f5; --tip-ink: #18181b; --accent-wash: rgba(0, 136, 255, .14);
40
+
41
+ --glass: rgba(12, 12, 17, .72);
42
+ --glass-brd: rgba(255, 255, 255, .16);
43
+ --glass-ink: #f5f5f7;
44
+ --glass-ink-2: rgba(245, 245, 247, .64);
45
+ --glass-ink-3: rgba(245, 245, 247, .4);
46
+ --glass-hover: rgba(255, 255, 255, .1);
47
+ --glass-accent: #5cb3ff;
48
+ --glass-accent-bg: rgba(0, 136, 255, .28);
49
+ --glass-good: #7fe0c3;
50
+ --glass-warn: #ffd9a0;
51
+ --shadow-glass: inset 0 1px 0 rgba(255, 255, 255, .09), 0 1px 2px rgba(10, 10, 16, .2), 0 4px 14px -8px rgba(10, 10, 16, .4);
52
+
53
+ --node-bg: #0f1015; --node-brd: #2c2d38;
54
+ --head-bg: rgba(22, 23, 30, .6);
55
+ --head-sheen: linear-gradient(100deg, rgba(255, 255, 255, .1), rgba(255, 255, 255, .015) 45%, rgba(255, 255, 255, .06));
56
+ --edge-light: linear-gradient(155deg, rgba(255, 255, 255, .3), rgba(255, 255, 255, .05) 30%, rgba(255, 255, 255, .02) 65%, rgba(255, 255, 255, .16));
57
+ --head-ink: #ececf1; --head-dim: rgba(236, 236, 241, .42);
58
+ --card-err-ink: #ff9d94; --card-err-bg: #2a1512;
59
+ --card-warn-ink: #ffd9a0; --card-warn-bg: #2a2113;
60
+ --interact: #ea8dff; --interact-ring: rgba(219, 53, 242, .3);
61
+ --interact-strong: rgba(219, 53, 242, 1); --interact-soft: rgba(234, 141, 255, .95);
62
+ --interact-deep: rgba(203, 48, 224, .8); --interact-spark: rgba(255, 255, 255, .9);
63
+
64
+ --shadow-node: 0 1px 2px rgba(0, 0, 0, .35); /* shadow-sm, alpha raised to read on dark */
65
+ }
66
+
67
+ @property --ground { syntax: '<color>'; inherits: true; initial-value: #e7e9ef; }
68
+ @property --dot { syntax: '<color>'; inherits: true; initial-value: #c9cbd5; }
69
+ @property --glass { syntax: '<color>'; inherits: true; initial-value: rgba(255, 255, 255, .64); }
70
+ @property --glass-brd { syntax: '<color>'; inherits: true; initial-value: rgba(24, 24, 27, .1); }
71
+ @property --glass-ink { syntax: '<color>'; inherits: true; initial-value: #18181b; }
72
+ @property --glass-ink-2 { syntax: '<color>'; inherits: true; initial-value: rgba(24, 24, 27, .66); }
73
+ @property --glass-ink-3 { syntax: '<color>'; inherits: true; initial-value: rgba(24, 24, 27, .4); }
74
+ @property --glass-hover { syntax: '<color>'; inherits: true; initial-value: rgba(24, 24, 27, .06); }
75
+ @property --node-bg { syntax: '<color>'; inherits: true; initial-value: #ffffff; }
76
+ @property --node-brd { syntax: '<color>'; inherits: true; initial-value: #dddee5; }
77
+ @property --head-bg { syntax: '<color>'; inherits: true; initial-value: rgba(255, 255, 255, .55); }
78
+ @property --head-ink { syntax: '<color>'; inherits: true; initial-value: #18181b; }
79
+ @property --head-dim { syntax: '<color>'; inherits: true; initial-value: rgba(24, 24, 27, .4); }
80
+ @property --tip-bg { syntax: '<color>'; inherits: true; initial-value: #18181b; }
81
+ @property --tip-ink { syntax: '<color>'; inherits: true; initial-value: #fafafa; }
82
+ @property --accent { syntax: '<color>'; inherits: true; initial-value: #0088ff; }
83
+ @property --accent-ring { syntax: '<color>'; inherits: true; initial-value: rgba(0, 136, 255, .2); }
84
+ @property --beam { syntax: '<color>'; inherits: true; initial-value: rgba(0, 136, 255, .42); }
85
+ @property --edge-accent { syntax: '<color>'; inherits: true; initial-value: rgba(0, 136, 255, .8); }
86
+ @property --inner-tint { syntax: '<color>'; inherits: true; initial-value: rgba(0, 136, 255, .13); }
87
+ @property --accent-wash { syntax: '<color>'; inherits: true; initial-value: rgba(0, 136, 255, .07); }
88
+ @property --glass-accent { syntax: '<color>'; inherits: true; initial-value: #0077e6; }
89
+ @property --glass-accent-bg { syntax: '<color>'; inherits: true; initial-value: rgba(0, 136, 255, .13); }
90
+
91
+ /* interact mode: the whole shell shifts to the interactive purple - one token override,
92
+ and every accent-derived surface (mark, sidebar actives, selection bar, beams, rings,
93
+ handles, device chips) follows without knowing the mode exists */
94
+ .sh-app.interacting {
95
+ --accent: #db35f2; --accent-ring: rgba(219, 53, 242, .2); --beam: rgba(219, 53, 242, .42);
96
+ --edge-accent: rgba(219, 53, 242, .8); --inner-tint: rgba(219, 53, 242, .13);
97
+ --accent-wash: rgba(219, 53, 242, .07);
98
+ --glass-accent: #b02fc2; --glass-accent-bg: rgba(219, 53, 242, .13);
99
+ }
100
+ .sh-app.dark.interacting {
101
+ --accent: #db35f2; --accent-ring: rgba(219, 53, 242, .32); --beam: rgba(219, 53, 242, .5);
102
+ --edge-accent: rgba(234, 141, 255, .85); --inner-tint: rgba(234, 141, 255, .16);
103
+ --accent-wash: rgba(219, 53, 242, .14);
104
+ --glass-accent: #ea8dff; --glass-accent-bg: rgba(219, 53, 242, .28);
105
+ }
106
+
107
+ * { box-sizing: border-box }
108
+ html, body, #root { height: 100%; margin: 0 }
109
+ body { font-family: -apple-system, BlinkMacSystemFont, system-ui, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
110
+ -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale; overflow: hidden }
111
+ button { font-family: inherit }
112
+ button svg { pointer-events: none } /* event targets stay on the button - pan exclusion depends on it */
113
+ @media (prefers-reduced-motion: reduce) { *, *::before, *::after { transition: none !important } }
114
+
115
+ .sh-app { position: relative; height: 100%; color: var(--glass-ink); outline: none;
116
+ transition: --accent .4s ease, --accent-ring .4s ease, --beam .4s ease,
117
+ --edge-accent .4s ease, --inner-tint .4s ease, --accent-wash .4s ease,
118
+ --glass-accent .4s ease, --glass-accent-bg .4s ease,
119
+ --ground .4s ease, --dot .4s ease, --glass .4s ease, --glass-brd .4s ease,
120
+ --glass-ink .4s ease, --glass-ink-2 .4s ease, --glass-ink-3 .4s ease,
121
+ --glass-hover .4s ease, --node-bg .4s ease, --node-brd .4s ease,
122
+ --head-bg .4s ease, --head-ink .4s ease, --head-dim .4s ease,
123
+ --tip-bg .4s ease, --tip-ink .4s ease }
124
+ .sh-canvas { width: 100% !important; height: 100% !important;
125
+ background-color: var(--ground) }
126
+ /* world-space dot grid: size/position/alpha driven per-frame from the transform */
127
+ .sh-canvas::before { content: ''; position: absolute; inset: 0; pointer-events: none;
128
+ background-image: radial-gradient(var(--dot) 1px, transparent 1px);
129
+ background-size: var(--grid-size, 20px 20px);
130
+ background-position: var(--grid-pos, 0 0);
131
+ opacity: var(--grid-alpha, 1) }
132
+ .sh-content { will-change: auto }
133
+ #sh-world { position: relative; width: 1px; height: 1px }
134
+ #sh-world.sh-gesturing iframe { pointer-events: none } /* law G-4 */
135
+ .sh-gesturing .sh-content { will-change: transform } /* law G-3: gesture-scoped only */
136
+ /* preset transitions: armed by animateLayout() around device/tidy mutations - nodes ease
137
+ to their new frame on the same 320ms ease-out the camera fit animates with */
138
+ #sh-world.sh-preset .sh-node { transition: transform .32s cubic-bezier(.25,.46,.45,.94),
139
+ width .32s cubic-bezier(.25,.46,.45,.94), height .32s cubic-bezier(.25,.46,.45,.94) }
140
+ #sh-world.sh-preset .sh-node-body,
141
+ #sh-world.sh-preset .sh-node iframe { transition: width .32s cubic-bezier(.25,.46,.45,.94),
142
+ height .32s cubic-bezier(.25,.46,.45,.94) }
143
+
144
+ /* cursor conventions (Figma): arrow everywhere; grab only while space is held */
145
+ body.sh-space .sh-canvas { cursor: grab }
146
+ body.sh-space.sh-panning .sh-canvas { cursor: grabbing }
147
+ body.sh-space .sh-node { pointer-events: none } /* space-drag pans from anywhere */
148
+
149
+ /* frame nodes: theme-aware cards with a glass header strip. Each node re-declares its
150
+ card tokens from its OWN theme, so a dark frame on a light board wears a dark header
151
+ (easier on the eyes than light chrome over a dark page) - and vice versa. */
152
+ .sh-node { position: absolute; top: 0; left: 0; border-radius: var(--r-node); background: transparent;
153
+ border: 1px solid var(--node-brd); box-shadow: var(--shadow-node); display: flex; flex-direction: column;
154
+ transition: border-color .3s ease, --node-bg .4s ease, --node-brd .4s ease,
155
+ --head-bg .4s ease, --head-ink .4s ease, --head-dim .4s ease }
156
+ .sh-node[data-theme="light"] {
157
+ --node-bg: #fff; --node-brd: #dddee5;
158
+ --head-bg: rgba(255, 255, 255, .55);
159
+ --head-sheen: linear-gradient(100deg, rgba(255, 255, 255, .85), rgba(255, 255, 255, .1) 45%, rgba(255, 255, 255, .45));
160
+ --head-ink: #18181b; --head-dim: rgba(24, 24, 27, .4);
161
+ }
162
+ .sh-node[data-theme="dark"] {
163
+ --node-bg: #0f1015; --node-brd: #2c2d38;
164
+ --head-bg: rgba(22, 23, 30, .6);
165
+ --head-sheen: linear-gradient(100deg, rgba(255, 255, 255, .1), rgba(255, 255, 255, .015) 45%, rgba(255, 255, 255, .06));
166
+ --head-ink: #ececf1; --head-dim: rgba(236, 236, 241, .42);
167
+ }
168
+ /* selection + interact indicators live in world space but keep constant SCREEN width
169
+ via --sh-inv (1/zoom): 2 screen px at every zoom level */
170
+ .sh-node.sel { border-color: var(--accent);
171
+ outline: calc(2px * var(--sh-inv, 1)) solid var(--accent);
172
+ outline-offset: calc(-1px * var(--sh-inv, 1));
173
+ box-shadow: 0 0 0 calc(4px * var(--sh-inv, 1)) var(--accent-ring), var(--shadow-node) }
174
+ .sh-node-head { height: 28px; display: flex; align-items: center; gap: 8px; padding: 0 11px;
175
+ font-size: 11px; color: var(--head-dim);
176
+ border-bottom: 1px solid var(--node-brd); cursor: default; user-select: none;
177
+ border-radius: var(--r-node) var(--r-node) 0 0;
178
+ background-color: var(--head-bg); background-image: var(--head-sheen);
179
+ backdrop-filter: var(--blur); -webkit-backdrop-filter: var(--blur) }
180
+ .sh-node-head .id { font-weight: 600; color: var(--head-ink); overflow: hidden; text-overflow: ellipsis; white-space: nowrap }
181
+ .sh-node-head .dim { margin-left: auto; color: var(--head-dim); flex: none; font-variant-numeric: tabular-nums }
182
+ .sh-node-body { position: relative; background: var(--node-bg); border-radius: 0 0 var(--r-node) var(--r-node); overflow: hidden }
183
+ .sh-node iframe { border: 0; display: block }
184
+ .sh-overlay { position: absolute; inset: 0; cursor: default }
185
+ .sh-node.interact { border-color: var(--interact);
186
+ outline: calc(2px * var(--sh-inv, 1)) solid var(--interact);
187
+ outline-offset: calc(-1px * var(--sh-inv, 1));
188
+ box-shadow: 0 0 0 calc(4px * var(--sh-inv, 1)) var(--interact-ring), var(--shadow-node) }
189
+
190
+ .sh-handle { position: absolute; background: var(--node-bg); border: 1.5px solid var(--accent); border-radius: 4px; z-index: 3;
191
+ transform: scale(var(--sh-inv, 1)) } /* constant screen size; hit target grows as you zoom out */
192
+ .sh-handle.e { right: -5px; top: 50%; width: 9px; height: 26px; margin-top: -13px; cursor: ew-resize }
193
+ .sh-handle.s { bottom: -5px; left: 50%; width: 26px; height: 9px; margin-left: -13px; cursor: ns-resize }
194
+ .sh-handle.se { right: -5px; bottom: -5px; width: 11px; height: 11px; cursor: nwse-resize }
195
+
196
+ /* glass surfaces share one recipe; chrome text is never selectable (shift-click = multi-select) */
197
+ .sh-ctx, .sh-panel, .sh-fab, .sh-pill, .sh-pill-fab, .sh-menu, .sh-banner, .sh-toast {
198
+ background: var(--glass); backdrop-filter: var(--blur); -webkit-backdrop-filter: var(--blur);
199
+ border: 1px solid var(--glass-brd); box-shadow: var(--shadow-glass); position: relative;
200
+ user-select: none; -webkit-user-select: none }
201
+
202
+ /* edge light: a 1px gradient ring over the border, so glass reads as glass even
203
+ with nothing behind it to blur (masked-border technique) */
204
+ .sh-ctx::after, .sh-panel::after, .sh-fab::after, .sh-pill::after, .sh-pill-fab::after,
205
+ .sh-menu::after, .sh-banner::after, .sh-toast::after, .sh-node::after {
206
+ content: ''; position: absolute; inset: -1px; padding: 1px; border-radius: inherit;
207
+ background: var(--edge-light); pointer-events: none;
208
+ -webkit-mask: linear-gradient(#000 0 0) content-box, linear-gradient(#000 0 0);
209
+ -webkit-mask-composite: xor;
210
+ mask: linear-gradient(#000 0 0) content-box, linear-gradient(#000 0 0);
211
+ mask-composite: exclude }
212
+ .sh-node.sel::after { display: none } /* the accent ring owns the selected edge */
213
+ @property --sh-rot { syntax: '<angle>'; inherits: false; initial-value: 0deg; }
214
+ /* one orbiting light, two intensities: a faint shimmer riding ON the solid border
215
+ (::before, low opacity over the still outline) and the clear glow just outside
216
+ (::after, lightly blurred, sharp glints). Same duration = perfectly in sync. */
217
+ .sh-node.interact::before, .sh-node.interact::after {
218
+ content: ''; position: absolute; border-radius: inherit; pointer-events: none;
219
+ background: conic-gradient(from var(--sh-rot),
220
+ var(--interact-strong) 0deg,
221
+ var(--interact-strong) 105deg,
222
+ var(--interact-spark) 135deg,
223
+ var(--interact-strong) 165deg,
224
+ var(--interact-strong) 285deg,
225
+ var(--interact-spark) 315deg,
226
+ var(--interact-strong) 345deg,
227
+ var(--interact-strong) 360deg);
228
+ -webkit-mask: linear-gradient(#000 0 0) content-box, linear-gradient(#000 0 0);
229
+ -webkit-mask-composite: xor;
230
+ mask: linear-gradient(#000 0 0) content-box, linear-gradient(#000 0 0);
231
+ mask-composite: exclude;
232
+ animation: sh-orbit 4.2s linear infinite;
233
+ }
234
+ .sh-node.interact::before {
235
+ inset: calc(-2.5px * var(--sh-inv, 1));
236
+ padding: calc(2.5px * var(--sh-inv, 1));
237
+ border-radius: calc(var(--r-node) + 2px * var(--sh-inv, 1));
238
+ opacity: .6;
239
+ }
240
+ .sh-node.interact::after {
241
+ inset: calc(-9px * var(--sh-inv, 1));
242
+ padding: calc(5px * var(--sh-inv, 1));
243
+ border-radius: calc(var(--r-node) + 8px * var(--sh-inv, 1));
244
+ filter: blur(calc(2px * var(--sh-inv, 1)));
245
+ opacity: .95;
246
+ }
247
+ @keyframes sh-orbit { to { --sh-rot: 360deg } }
248
+ @media (prefers-reduced-motion: reduce) {
249
+ .sh-node.interact::before, .sh-node.interact::after { animation: none } }
250
+
251
+ /* selection toolbar: screen-space overlay, constant size at any zoom */
252
+ .sh-ctx { position: absolute; display: flex; align-items: center; gap: 1px;
253
+ border-radius: 999px; padding: 4px; z-index: 9; white-space: nowrap;
254
+ /* a teeny inner glow hugging the bottom edge - 8px tall, cannot climb into the body */
255
+ background-image: radial-gradient(46% 8px at 50% calc(100% + 1px), var(--inner-tint), transparent 80%) }
256
+ /* the accent lives ON the border line, not in the body: layered into the masked 1px
257
+ edge ring, so the bottom border glows blue at center and fades along the arc -
258
+ the bar's surface stays untouched */
259
+ .sh-ctx::after {
260
+ background:
261
+ radial-gradient(52% 16px at 50% 100%, var(--edge-accent), transparent 75%),
262
+ var(--edge-light) }
263
+ /* connection glow: a soft accent beam under the middle ~60% of the bar - deepest and
264
+ tallest at the center, fading to nothing at the edges - ties the bar to its frame */
265
+ .sh-ctx::before { content: ''; position: absolute; left: var(--beam-inset); right: var(--beam-inset);
266
+ bottom: var(--beam-bottom); height: var(--beam-h);
267
+ background: radial-gradient(50% 100% at 50% 0%, var(--beam), transparent 70%);
268
+ filter: blur(var(--beam-blur, 4px)); pointer-events: none; z-index: -1 }
269
+ .sh-ctx button { display: inline-flex; align-items: center; justify-content: center; gap: 6px; height: 28px;
270
+ font: 600 12px -apple-system, system-ui, sans-serif; color: var(--glass-ink-2); background: none; border: 0;
271
+ padding: 0 11px; border-radius: 999px; cursor: pointer }
272
+ .sh-ctx button.icon { min-width: 28px; width: 30px; padding: 0 }
273
+ .sh-ctx .cnt { font: 600 11px -apple-system, system-ui, sans-serif; font-variant-numeric: tabular-nums; color: var(--glass-ink-2); background: var(--glass-hover);
274
+ border-radius: 999px; min-width: 22px; height: 22px; display: inline-flex; align-items: center;
275
+ justify-content: center; padding: 0 6px; margin: 0 2px 0 3px }
276
+ .sh-ctx button.on { background: var(--glass-accent-bg); color: var(--glass-accent) }
277
+ .sh-ctx button:hover:not(.on) { background: var(--glass-hover); color: var(--glass-ink) }
278
+ .sh-ctx .sep { width: 1px; height: 14px; background: var(--glass-brd); margin: 0 3px }
279
+
280
+ /* error / missing cards (inside the frame body) */
281
+ .sh-card { position: absolute; inset: 0; z-index: 2; display: flex; flex-direction: column; gap: 6px;
282
+ padding: 14px; font-size: 12px; background: var(--node-bg) }
283
+ .sh-card.err { color: var(--card-err-ink); background: var(--card-err-bg) }
284
+ .sh-card.warn { color: var(--card-warn-ink); background: var(--card-warn-bg) }
285
+ .sh-card b { font-size: 12px }
286
+ .sh-card .msg { white-space: pre-wrap; overflow: auto }
287
+ .sh-card .dim { opacity: .55 }
288
+ .sh-card .row { display: flex; gap: 6px; margin-top: auto }
289
+ .sh-card button { display: inline-flex; align-items: center; gap: 5px; font: 600 11px -apple-system, system-ui, sans-serif;
290
+ border: 1px solid currentColor; background: transparent; color: inherit; border-radius: 999px;
291
+ padding: 4px 10px; cursor: pointer }
292
+
293
+ /* shared icon button (glass surfaces) */
294
+ .sh-ibtn { display: grid; place-items: center; width: 28px; height: 28px; border-radius: 999px;
295
+ border: 0; background: none; color: var(--glass-ink-3); cursor: pointer }
296
+ .sh-ibtn:hover { background: var(--glass-hover); color: var(--glass-ink) }
297
+
298
+ /* floating glass panel; collapses into the fab with a scale-fade morph */
299
+ .sh-panel { position: absolute; left: var(--edge); top: var(--edge); width: 236px;
300
+ max-height: calc(100% - 2 * var(--edge)); z-index: 10;
301
+ border-radius: var(--r-panel); padding: 10px; display: flex; flex-direction: column;
302
+ font-size: 13px; color: var(--glass-ink);
303
+ transform-origin: 22px 22px; transition: transform var(--morph), opacity .2s ease }
304
+ .sh-panel.closed { transform: scale(.16); opacity: 0; pointer-events: none }
305
+ .sh-panel-top { display: flex; align-items: center; gap: 8px; padding: 2px 2px 8px 8px }
306
+ .sh-panel-top .mark { flex: none; color: var(--accent) }
307
+ .sh-panel-top .name { font-weight: 700; font-size: 15px; flex: 1; letter-spacing: -0.01em }
308
+ .sh-panel-scroll { overflow-y: auto; min-height: 0; padding-bottom: 2px; scrollbar-width: thin;
309
+ scrollbar-color: var(--glass-ink-3) transparent }
310
+ /* Sidebar list system (macOS-sidebar conventions): every row is 28px tall with an
311
+ 8px radius and a fixed 16px icon column, so all labels share one vertical line.
312
+ Hierarchy is carried by section labels + indent + ink - never bold-on-bold:
313
+ boards and scene folders read at 500/primary, frames at 400/secondary one indent
314
+ in, counts and idle icons tertiary. Actives use the accent wash + accent ink. */
315
+ .sh-panel .hd { font: 600 11px -apple-system, system-ui, sans-serif; letter-spacing: .02em;
316
+ color: var(--glass-ink-3); margin: 16px 10px 5px }
317
+ .sh-panel .hd:first-child { margin-top: 2px }
318
+ .sh-panel .it { width: 100%; height: 28px; padding: 0 8px; border-radius: 8px; color: var(--glass-ink);
319
+ font: 500 13px -apple-system, system-ui, sans-serif; background: none; border: 0; cursor: pointer; text-align: left;
320
+ display: flex; align-items: center; gap: 7px; margin-bottom: 1px }
321
+ /* a scene holding a selected frame keeps a half-strength wash (Xcode-style ancestor
322
+ highlight) so the selection's home stays visible even with the group collapsed;
323
+ same specificity as :hover but declared first, so hover still wins under pointer */
324
+ .sh-panel .it.held { background: color-mix(in srgb, var(--glass-hover) 55%, transparent) }
325
+ .sh-panel .it:hover { background: var(--glass-hover) }
326
+ .sh-panel .it .tw { color: var(--glass-ink-3); flex: none; margin: 0 2.5px; transition: transform .15s ease }
327
+ .sh-panel .it span { flex: 1; overflow: hidden; text-overflow: ellipsis; white-space: nowrap }
328
+ .sh-panel .it small { color: var(--glass-ink-3); font: 500 11px -apple-system, system-ui, sans-serif; font-variant-numeric: tabular-nums }
329
+ /* board rows: places, not folders - icon idles tertiary, lights accent when active */
330
+ .sh-panel .it.board svg { color: var(--glass-ink-3); flex: none; margin: 0 1px; transition: color .2s ease }
331
+ .sh-panel .it.board.cur { background: var(--accent-wash); color: var(--glass-accent); font-weight: 600 }
332
+ .sh-panel .it.board.cur svg { color: var(--glass-accent) }
333
+ /* frame rows: one indent in (aligned to the label line), quieter ink */
334
+ .sh-panel .sub { display: flex; align-items: center; height: 28px; padding: 0 8px 0 31px; margin-bottom: 1px;
335
+ color: var(--glass-ink-2); font-size: 13px; font-weight: 400; border-radius: 8px; cursor: pointer }
336
+ .sh-panel .sub:hover { background: var(--glass-hover); color: var(--glass-ink) }
337
+ .sh-panel .sub.on { background: var(--accent-wash); color: var(--glass-accent); font-weight: 500 }
338
+ .sh-panel .sub.dim { color: var(--glass-ink-3); cursor: default; line-height: 1.4; height: auto; padding-top: 4px; padding-bottom: 4px }
339
+ .sh-panel .sub.dim:hover { background: none; color: var(--glass-ink-3) }
340
+
341
+ /* collapsed panel: a round glass button; morphs against the panel */
342
+ .sh-fab { position: absolute; left: var(--edge); top: var(--edge); z-index: 11; width: 44px; height: 44px;
343
+ display: grid; place-items: center; border-radius: 999px;
344
+ color: var(--glass-ink-2); cursor: pointer;
345
+ transition: transform var(--morph), opacity .2s ease }
346
+ .sh-fab:hover { color: var(--glass-ink); background: var(--glass-hover) }
347
+ .sh-fab.hidden { transform: scale(.5); opacity: 0; pointer-events: none }
348
+
349
+ /* pill nav */
350
+ /* uniform 5px inset on every side: 34px buttons + 2*5px = 44px, same as the fab */
351
+ .sh-pill { position: absolute; right: var(--edge); top: var(--edge); z-index: 10; display: flex; align-items: center; gap: 1px;
352
+ border-radius: 999px; padding: 5px;
353
+ transform-origin: calc(100% - 22px) 22px; transition: transform var(--morph), opacity .2s ease }
354
+ .sh-pill.closed { transform: scale(.16); opacity: 0; pointer-events: none }
355
+ /* the pill's expand chip - the sidebar fab, mirrored top-right: same glass (shared
356
+ surface rule above), same morph, the bar shrinking into the chip's spot */
357
+ .sh-pill-fab { position: absolute; right: var(--edge); top: var(--edge); z-index: 11; width: 44px; height: 44px;
358
+ display: grid; place-items: center; border-radius: 999px;
359
+ color: var(--glass-ink-2); cursor: pointer;
360
+ transition: transform var(--morph), opacity .2s ease }
361
+ .sh-pill-fab:hover { color: var(--glass-ink); background: var(--glass-hover) }
362
+ .sh-pill-fab.hidden { transform: scale(.5); opacity: 0; pointer-events: none }
363
+ .sh-pill-btn { display: inline-flex; align-items: center; justify-content: center; gap: 4px; height: 34px;
364
+ min-width: 34px; font: 500 12px -apple-system, system-ui, sans-serif; color: var(--glass-ink-2); background: none; border: 0;
365
+ padding: 0 10px; border-radius: 999px; cursor: pointer; white-space: nowrap }
366
+ .sh-pill-btn:hover { background: var(--glass-hover); color: var(--glass-ink) }
367
+ .sh-pill-btn.pct { font-variant-numeric: tabular-nums; min-width: 46px }
368
+ .sh-pill-btn.off { color: var(--glass-ink-3); cursor: default }
369
+ .sh-pill-btn.off:hover { background: none; color: var(--glass-ink-3) }
370
+ .sh-pill .sep { width: 1px; height: 16px; background: var(--glass-brd); margin: 0 3px }
371
+
372
+ /* dropdown menu (theme) - portaled to .sh-app (its own backdrop root), left-aligned to its trigger */
373
+ .sh-theme { position: relative }
374
+ .sh-menu { position: absolute; min-width: 136px; z-index: 20;
375
+ border-radius: 16px; padding: 5px; display: flex; flex-direction: column;
376
+ transform-origin: top left; animation: sh-pop .14s ease }
377
+ @keyframes sh-pop { from { opacity: 0; transform: scale(.95) translateY(-4px) } }
378
+ .sh-menu button { display: flex; align-items: center; gap: 8px; height: 30px; padding: 0 10px;
379
+ font: 500 12px -apple-system, system-ui, sans-serif; color: var(--glass-ink); background: none; border: 0; border-radius: 12px;
380
+ cursor: pointer; text-align: left }
381
+ .sh-menu button:hover { background: var(--glass-hover) }
382
+ .sh-menu button span { flex: 1; text-transform: capitalize }
383
+ .sh-menu .chk { color: var(--glass-accent) }
384
+ .sh-menu kbd { font: 500 10.5px -apple-system, system-ui, sans-serif; color: var(--glass-ink-3); background: none; border: 0 }
385
+ .sh-menu .div { height: 1px; background: var(--glass-brd); margin: 4px 6px }
386
+
387
+ /* shadcn-style tooltip: contrast-flipped solid, zoom-fade in */
388
+ .sh-tip { position: absolute; z-index: 40; pointer-events: none; padding: 5px 10px; border-radius: 8px;
389
+ font: 500 12px -apple-system, system-ui, sans-serif; white-space: nowrap; transform: translate(-50%, -100%);
390
+ display: flex; flex-direction: row; align-items: center; gap: 10px;
391
+ background: var(--tip-bg); color: var(--tip-ink);
392
+ box-shadow: 0 2px 8px -2px rgba(0, 0, 0, .25);
393
+ animation: sh-tip-pop .12s ease }
394
+ @keyframes sh-tip-pop { from { opacity: 0; transform: translate(-50%, -100%) scale(.95) } }
395
+ /* top-of-screen chrome flips its tooltip below the trigger */
396
+ .sh-tip.below { transform: translate(-50%, 0); animation-name: sh-tip-pop-b }
397
+ @keyframes sh-tip-pop-b { from { opacity: 0; transform: translate(-50%, 0) scale(.95) } }
398
+ /* play chrome is fixed dark glass regardless of board theme - its tips pin the light bubble */
399
+ .sh-tip.inv { background: #f4f4f5; color: #18181b }
400
+ .sh-tip b { font-weight: 600 }
401
+ .sh-tip span { opacity: .55; font-size: 11px; margin-left: auto }
402
+ .sh-tip span.k { opacity: .4; margin-left: 0; font-weight: 600 }
403
+
404
+ .sh-banner { position: absolute; bottom: var(--edge); right: var(--edge); z-index: 10; font: 500 12px -apple-system, system-ui, sans-serif;
405
+ color: var(--glass-warn); border-radius: 999px; padding: 8px 14px }
406
+
407
+ .sh-toasts { position: absolute; left: var(--edge); bottom: var(--edge); z-index: 12; display: flex; flex-direction: column; gap: 6px }
408
+ .sh-toast { display: flex; align-items: center; gap: 6px; font: 500 12px -apple-system, system-ui, sans-serif;
409
+ color: var(--glass-ink-2); border-radius: 999px; padding: 7px 13px }
410
+ .sh-toast svg { color: var(--glass-good); flex: none }
411
+
412
+ /* ---- Play mode (SPEC-M2 §1): near-black stage, one centered device, auto-hiding bar.
413
+ Fixed dark palette - the backdrop never follows the board theme, so no tokens here. */
414
+ .sh-play { position: absolute; inset: 0; z-index: 30; background: #0a0a0b; display: flex;
415
+ align-items: center; justify-content: center; animation: sh-play-in .28s ease-out }
416
+ @keyframes sh-play-in { from { opacity: 0 } }
417
+ /* the device wears the frame-node card language: same theme border tokens, the same
418
+ masked 1px edge-light gradient ring (glass catching light), and a real drop shadow
419
+ below. Radius + clipping live on the WRAPPER at whole pixels - the iframe is
420
+ transparent and scaled to land exactly on its edges, which killed the corner seams.
421
+ The ring is an inside overlay, not a border: border-box would clip a pixel of content. */
422
+ /* LIGHT device: its own treatment, fully separate from dark - no masked border, just a
423
+ soft gray RING hugging the outside edge plus the luminous halo lifting it off black */
424
+ .sh-play .dev { position: relative; overflow: hidden; border-radius: 14px;
425
+ --pdev-bg: #fff;
426
+ background: var(--pdev-bg);
427
+ box-shadow: 0 0 0 1px rgba(148, 152, 164, .6), 0 0 54px rgba(255, 255, 255, .12),
428
+ 0 30px 70px -16px rgba(0, 0, 0, .85), 0 8px 24px rgba(0, 0, 0, .45);
429
+ transition: width .28s ease, height .28s ease }
430
+ /* DARK device: the original glint recipe, verbatim - do not touch */
431
+ .sh-play .dev[data-theme="dark"] { --pdev-bg: #0f1015;
432
+ box-shadow: 0 30px 70px -16px rgba(0, 0, 0, .85), 0 8px 24px rgba(0, 0, 0, .45) }
433
+ .sh-play .dev[data-theme="dark"]::after { content: ''; position: absolute; inset: 0; padding: 1px; border-radius: inherit;
434
+ background-color: #2c2d38;
435
+ background-image: linear-gradient(155deg, rgba(255, 255, 255, .3), rgba(255, 255, 255, .05) 30%, rgba(255, 255, 255, .02) 65%, rgba(255, 255, 255, .16));
436
+ pointer-events: none;
437
+ -webkit-mask: linear-gradient(#000 0 0) content-box, linear-gradient(#000 0 0);
438
+ -webkit-mask-composite: xor;
439
+ mask: linear-gradient(#000 0 0) content-box, linear-gradient(#000 0 0);
440
+ mask-composite: exclude }
441
+ .sh-play .dev iframe { display: block; border: 0; background: transparent; transform-origin: 0 0 }
442
+ .sh-play.fill .dev { border-radius: 0; box-shadow: none; transition: none }
443
+ .sh-play.fill .dev::after { display: none }
444
+
445
+ /* the two floating chrome pieces share one glass language and one idle behavior */
446
+ .sh-play-bar, .sh-play-nav { position: absolute; display: flex; align-items: center; gap: 1px;
447
+ padding: 4px; border-radius: 999px;
448
+ background: rgba(18, 18, 24, .82); border: 1px solid rgba(255, 255, 255, .12);
449
+ backdrop-filter: blur(16px); -webkit-backdrop-filter: blur(16px);
450
+ transition: opacity .35s ease; font: 500 12px -apple-system, system-ui, sans-serif; color: #f5f5f7 }
451
+ .sh-play-bar { top: 14px; right: 16px }
452
+ .sh-play-nav { bottom: 16px; left: 16px }
453
+ .sh-play-bar.idle, .sh-play-nav.idle { opacity: 0; pointer-events: none }
454
+ .sh-play-bar .sep, .sh-play-nav .sep { width: 1px; height: 16px; background: rgba(255, 255, 255, .14); margin: 0 4px }
455
+ .sh-play-bar button, .sh-play-nav button { display: flex; align-items: center; justify-content: center;
456
+ width: 30px; height: 30px; border: 0; border-radius: 999px; background: transparent;
457
+ color: rgba(245, 245, 247, .64); cursor: pointer }
458
+ .sh-play-bar button:hover, .sh-play-nav button:hover { background: rgba(255, 255, 255, .1); color: #f5f5f7 }
459
+ .sh-play-bar button.on { background: rgba(255, 255, 255, .14); color: #f5f5f7 }
460
+
461
+ /* board switcher */
462
+ .sh-play-bar .bd-wrap { position: relative }
463
+ .sh-play-bar .bd { width: auto; gap: 6px; padding: 0 10px 0 12px; white-space: nowrap; font: inherit }
464
+ /* no backdrop-filter here: the menu nests inside the blurred bar, and glass never nests
465
+ (a nested backdrop-filter samples the bar, not the page) - near-opaque ink instead */
466
+ .sh-play-menu { position: absolute; top: calc(100% + 10px); left: 0; min-width: 150px; padding: 4px;
467
+ border-radius: 12px; background: rgba(20, 20, 26, .97); border: 1px solid rgba(255, 255, 255, .12);
468
+ box-shadow: 0 12px 40px rgba(0, 0, 0, .5); display: flex; flex-direction: column; gap: 1px }
469
+ .sh-play-menu button { width: 100%; height: 28px; justify-content: flex-start; gap: 8px; padding: 0 10px;
470
+ border-radius: 8px; font: inherit }
471
+ .sh-play-menu button span { flex: 1; text-align: left; white-space: nowrap }
472
+
473
+ /* walk position readout */
474
+ .sh-play-nav .pos { padding: 0 8px; color: rgba(245, 245, 247, .64); font-variant-numeric: tabular-nums; white-space: nowrap }
475
+ .sh-play-nav .pos em { font-style: normal; opacity: .45; margin: 0 2px }
476
+
477
+ /* collapsed chip - the bar's fab. Always mounted so collapse/expand crossfades. */
478
+ .sh-play-chip { position: absolute; top: 14px; right: 16px; width: 38px; height: 38px;
479
+ display: grid; place-items: center; border-radius: 999px;
480
+ background: rgba(18, 18, 24, .82); border: 1px solid rgba(255, 255, 255, .12);
481
+ backdrop-filter: blur(16px); -webkit-backdrop-filter: blur(16px);
482
+ color: rgba(245, 245, 247, .64); cursor: pointer;
483
+ transition: opacity .35s ease, transform .35s ease }
484
+ .sh-play-chip:hover { color: #f5f5f7 }
485
+ .sh-play-chip.idle { opacity: 0; pointer-events: none; transform: scale(.6) }
486
+
487
+ /* the H coach bubble: hairline, translucent, teaches the way back from immersive mode.
488
+ Same pill geometry and separators as the bar it stands in for. */
489
+ .sh-play-hint { position: absolute; top: 14px; right: 16px; display: flex; align-items: center; gap: 3px;
490
+ padding: 4px 4px 4px 14px; border-radius: 999px;
491
+ background: rgba(18, 18, 24, .6); border: 1px solid rgba(255, 255, 255, .14);
492
+ backdrop-filter: blur(16px); -webkit-backdrop-filter: blur(16px);
493
+ font: 500 12px -apple-system, system-ui, sans-serif; color: rgba(245, 245, 247, .85);
494
+ animation: sh-play-in .3s ease-out }
495
+ .sh-play-hint .sep { width: 1px; height: 16px; background: rgba(255, 255, 255, .14); margin: 0 8px }
496
+ .sh-play-hint kbd { font: 600 11px -apple-system, system-ui, sans-serif; padding: 1px 6px; border-radius: 5px;
497
+ background: rgba(255, 255, 255, .12); border: 1px solid rgba(255, 255, 255, .16) }
498
+ .sh-play-hint button { height: 30px; padding: 0 12px; border: 0; border-radius: 999px; cursor: pointer;
499
+ font: 600 11px -apple-system, system-ui, sans-serif; background: rgba(255, 255, 255, .14); color: #f5f5f7 }
500
+ .sh-play-hint button:hover { background: rgba(255, 255, 255, .22) }
501
+ .sh-play-hint button.dim { background: transparent; color: rgba(245, 245, 247, .5); font-weight: 500 }
502
+ .sh-play-hint button.dim:hover { color: rgba(245, 245, 247, .8); background: rgba(255, 255, 255, .08) }
503
+
504
+ /* ---- Focus: silent for pointer + shortcut use; ONE branded inset ring, and only while
505
+ the user is actually Tab-navigating (body.sh-kbd, armed by Tab, disarmed by pointer).
506
+ Inset so scroll containers can never clip it; outline follows the border radius. */
507
+ .sh-app :focus { outline: none }
508
+ body.sh-kbd .sh-app :focus-visible { outline: 2px solid var(--accent); outline-offset: -2px }
@@ -0,0 +1,24 @@
1
+ export interface TidyNode { key: string; scene: string; w: number; h: number }
2
+ export interface Placed { key: string; x: number; y: number }
3
+
4
+ const GUTTER = 72
5
+ const SCENE_GAP = 96
6
+
7
+ /** Pure: rows per scene (scenes alphabetical, node order preserved within a scene). Spec §7. */
8
+ export function tidy(nodes: TidyNode[]): Placed[] {
9
+ const scenes = [...new Set(nodes.map((n) => n.scene))].sort()
10
+ const out: Placed[] = []
11
+ let y = 0
12
+ for (const scene of scenes) {
13
+ const row = nodes.filter((n) => n.scene === scene)
14
+ let x = 0
15
+ let rowH = 0
16
+ for (const n of row) {
17
+ out.push({ key: n.key, x, y })
18
+ x += n.w + GUTTER
19
+ rowH = Math.max(rowH, n.h)
20
+ }
21
+ y += rowH + SCENE_GAP
22
+ }
23
+ return out
24
+ }
@@ -0,0 +1,11 @@
1
+ <!doctype html>
2
+ <html>
3
+ <head>
4
+ <meta charset="utf-8" />
5
+ <meta name="viewport" content="width=device-width, initial-scale=1" />
6
+ </head>
7
+ <body>
8
+ <div id="root"></div>
9
+ <script type="module" src="{{ENTRY}}"></script>
10
+ </body>
11
+ </html>