@marver-design/marver 0.4.0 → 0.6.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 (33) hide show
  1. package/CHANGELOG.md +107 -0
  2. package/dist/{build-p3xmXU3b.mjs → build-BZaPa2DS.mjs} +11 -5
  3. package/dist/cli.mjs +3 -3
  4. package/dist/{dev-DZi1yRhn.mjs → dev-DaPQ9xA5.mjs} +57 -8
  5. package/dist/{init-Ck8z-HiD.mjs → init-DsCUmlCW.mjs} +1 -1
  6. package/dist/{manifest-DW-T52MM.mjs → manifest-C8FODq2S.mjs} +26 -1
  7. package/dist/{plugin-CtQqO5ZZ.mjs → plugin-wMY9lNf3.mjs} +67 -20
  8. package/package.json +2 -1
  9. package/src/client/content/diagram.tsx +46 -2
  10. package/src/client/content/img-lod.ts +107 -0
  11. package/src/client/content/index.tsx +37 -9
  12. package/src/client/content/md.ts +29 -0
  13. package/src/client/content/palette.ts +6 -0
  14. package/src/client/frame-host/bridge.js +40 -5
  15. package/src/client/frame-host/serialize.ts +195 -0
  16. package/src/client/shell/App.tsx +75 -18
  17. package/src/client/shell/Comments.tsx +5 -4
  18. package/src/client/shell/Play.tsx +24 -0
  19. package/src/client/shell/canvas/Canvas.tsx +69 -30
  20. package/src/client/shell/canvas/FrameNode.tsx +104 -8
  21. package/src/client/shell/canvas/camera-broadcast.ts +44 -0
  22. package/src/client/shell/canvas/frame-registry.ts +18 -0
  23. package/src/client/shell/canvas/snapshots.ts +233 -0
  24. package/src/client/shell/cursor-arrow-dark.svg +1 -0
  25. package/src/client/shell/cursor-arrow.svg +1 -0
  26. package/src/client/shell/labels.ts +10 -0
  27. package/src/client/shell/perf.ts +92 -0
  28. package/src/client/shell/store.ts +130 -22
  29. package/src/client/shell/styles.css +37 -7
  30. package/src/client/stage/main.tsx +2 -0
  31. package/templates/instructions/boards.md +9 -3
  32. package/templates/instructions/reference/color.md +22 -1
  33. package/templates/instructions/shape.md +44 -35
@@ -122,6 +122,13 @@ button { font-family: inherit }
122
122
  button svg { pointer-events: none } /* event targets stay on the button - pan exclusion depends on it */
123
123
  @media (prefers-reduced-motion: reduce) { *, *::before, *::after { transition: none !important } }
124
124
 
125
+ /* Default app pointer: the marver arrowhead (tilted left, rounded, thick border, soft shadow),
126
+ theme-adaptive - black/white on light, white/dark on dark. Hotspot at its tip (5,2). Overridden by
127
+ the state cursors: grab (space-pan), resize handles, comment-pin / laser-crosshair (set in-frame by
128
+ the bridge), and a normal pointer in interact / prototype mode. */
129
+ .sh-app { --sh-cursor: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='22' height='22' viewBox='0 0 26 26'%3E%3Cdefs%3E%3Cfilter id='s' x='-40%' y='-40%' width='180%' height='180%'%3E%3CfeDropShadow dx='0.4' dy='0.9' stdDeviation='0.7' flood-color='%23000000' flood-opacity='0.4'/%3E%3C/filter%3E%3C/defs%3E%3Cg filter='url(%23s)'%3E%3Cg transform='rotate(-20 6 3)'%3E%3Cpath fill='%23000000' stroke='%23FFFFFF' stroke-width='1.6' stroke-linejoin='round' stroke-linecap='round' d='M5.5 3.21V20.8c0 .45.54.67.85.35l4.86-4.86a1 1 0 0 1 .7-.3h6.87a1 1 0 0 0 .7-1.7L6.35 2.85a.5.5 0 0 0-.85.35Z'/%3E%3C/g%3E%3C/g%3E%3C/svg%3E") 5 2, default; cursor: var(--sh-cursor) }
130
+ .sh-app.dark { --sh-cursor: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='22' height='22' viewBox='0 0 26 26'%3E%3Cdefs%3E%3Cfilter id='s' x='-40%' y='-40%' width='180%' height='180%'%3E%3CfeDropShadow dx='0.4' dy='0.9' stdDeviation='0.7' flood-color='%23000000' flood-opacity='0.4'/%3E%3C/filter%3E%3C/defs%3E%3Cg filter='url(%23s)'%3E%3Cg transform='rotate(-20 6 3)'%3E%3Cpath fill='%23FFFFFF' stroke='%2318181b' stroke-width='1.6' stroke-linejoin='round' stroke-linecap='round' d='M5.5 3.21V20.8c0 .45.54.67.85.35l4.86-4.86a1 1 0 0 1 .7-.3h6.87a1 1 0 0 0 .7-1.7L6.35 2.85a.5.5 0 0 0-.85.35Z'/%3E%3C/g%3E%3C/g%3E%3C/svg%3E") 5 2, default }
131
+ .sh-app.interacting { cursor: auto }
125
132
  .sh-app { position: relative; height: 100%; color: var(--glass-ink); outline: none;
126
133
  transition: --accent .4s ease, --accent-ring .4s ease, --beam .4s ease,
127
134
  --edge-accent .4s ease, --inner-tint .4s ease, --accent-wash .4s ease,
@@ -141,15 +148,20 @@ button svg { pointer-events: none } /* event targets stay on the button - pan
141
148
  opacity: var(--grid-alpha, 1) }
142
149
  .sh-content { will-change: auto }
143
150
  #sh-world { position: relative; width: 1px; height: 1px }
144
- #sh-world.sh-gesturing iframe { pointer-events: none } /* law G-4 */
151
+ #sh-world.sh-gesturing .sh-live { pointer-events: none } /* law G-4 (live iframe only; lean is always pe:none) */
145
152
  .sh-gesturing .sh-content { will-change: transform } /* law G-3: gesture-scoped only */
146
153
  /* preset transitions: armed by animateLayout() around device/tidy mutations - nodes ease
147
154
  to their new frame on the same 320ms ease-out the camera fit animates with */
148
155
  #sh-world.sh-preset .sh-node { transition: transform .32s cubic-bezier(.25,.46,.45,.94),
149
156
  width .32s cubic-bezier(.25,.46,.45,.94), height .32s cubic-bezier(.25,.46,.45,.94) }
150
157
  #sh-world.sh-preset .sh-node-body,
151
- #sh-world.sh-preset .sh-node iframe { transition: width .32s cubic-bezier(.25,.46,.45,.94),
158
+ #sh-world.sh-preset .sh-node .sh-live { transition: width .32s cubic-bezier(.25,.46,.45,.94),
152
159
  height .32s cubic-bezier(.25,.46,.45,.94) }
160
+ /* SPEC-M5 device-sweep: a frame WITH a ready lean cover jumps its LIVE iframe straight to the final
161
+ width (ONE reflow, hidden under the cover) while the lean cover - being width:100% of the animating
162
+ body - reflows smoothly every frame (real CSS, the device-sweep fix). A frame WITHOUT a usable
163
+ cover keeps the live width animation above. */
164
+ body:not(.sh-laser):not(.sh-commenting) #sh-world.sh-preset .sh-node:has(.sh-lean[data-ready]):not(.interact) .sh-live { transition: none }
153
165
 
154
166
  /* cursor conventions (Figma): arrow everywhere; grab only while space is held */
155
167
  body.sh-space .sh-canvas { cursor: grab }
@@ -183,7 +195,7 @@ body.sh-space .sh-node { pointer-events: none } /* space-drag
183
195
  box-shadow: 0 0 0 calc(4px * var(--sh-inv, 1)) var(--accent-ring), var(--shadow-node) }
184
196
  .sh-node-head { height: 28px; display: flex; align-items: center; gap: 8px; padding: 0 11px;
185
197
  font-size: 11px; color: var(--head-dim);
186
- border-bottom: 1px solid var(--node-brd); cursor: default; user-select: none;
198
+ border-bottom: 1px solid var(--node-brd); user-select: none;
187
199
  border-radius: var(--r-node) var(--r-node) 0 0;
188
200
  background-color: var(--head-bg); background-image: var(--head-sheen);
189
201
  backdrop-filter: var(--blur); -webkit-backdrop-filter: var(--blur) }
@@ -197,7 +209,20 @@ body.sh-space .sh-node { pointer-events: none } /* space-drag
197
209
  .sh-node-head .dim { margin-left: auto; color: var(--head-dim); flex: none; font-variant-numeric: tabular-nums }
198
210
  .sh-node-body { position: relative; background: var(--node-bg); border-radius: 0 0 var(--r-node) var(--r-node); overflow: hidden }
199
211
  .sh-node iframe { border: 0; display: block }
200
- .sh-overlay { position: absolute; inset: 0; cursor: default }
212
+ /* SPEC-M5 LEAN-PRIMARY: the lean DOM-snapshot <iframe> (static html, 0 JS) is what you SEE for a
213
+ passive frame - at rest AND during pan/zoom/resize. There is NO per-gesture swap between the lean
214
+ and the live iframe (the swap shifted text ~1-2px = "jiggle", and flashed mermaid/theme colors,
215
+ because two documents never render pixel-identically). The live app (.sh-live) sits underneath and
216
+ shows ONLY when: the frame is interacted (.interact), laser/comment mode is on, or the lean is not
217
+ yet built (no data-ready = live-fallback). Hard cut, never a crossfade - two ~1px-offset text docs
218
+ would ghost into double text. */
219
+ .sh-lean { position: absolute; inset: 0; width: 100%; height: 100%; border: 0; z-index: 1;
220
+ opacity: 0; pointer-events: none; transition: none; background: var(--node-bg) }
221
+ .sh-lean[data-ready] { opacity: 1 }
222
+ .sh-node.interact .sh-lean,
223
+ body.sh-laser .sh-lean,
224
+ body.sh-commenting .sh-lean { opacity: 0 }
225
+ .sh-overlay { position: absolute; inset: 0; z-index: 2 } /* above the lean (z1) so drag-by-body works; inherits the app arrow cursor */
201
226
  .sh-node.interact { border-color: var(--interact);
202
227
  outline: calc(2px * var(--sh-inv, 1)) solid var(--interact);
203
228
  outline-offset: calc(-1px * var(--sh-inv, 1));
@@ -319,8 +344,11 @@ body.sh-space .sh-node { pointer-events: none } /* space-drag
319
344
  transform-origin: 22px 22px; transition: transform var(--morph), opacity .2s ease }
320
345
  .sh-panel.closed { transform: scale(.16); opacity: 0; pointer-events: none }
321
346
  .sh-panel-top { display: flex; align-items: center; gap: 8px; padding: 2px 2px 8px 8px }
322
- .sh-panel-top .mark { flex: none; color: var(--accent) }
323
- .sh-panel-top .name { font-weight: 700; font-size: 15px; flex: 1; letter-spacing: -0.01em }
347
+ .sh-panel-top .mark-link { flex: none; display: flex; border-radius: 6px; outline-offset: 2px; transition: opacity .15s ease }
348
+ .sh-panel-top .mark-link:hover { opacity: .72 }
349
+ .sh-panel-top .mark { flex: none; color: var(--accent); display: block }
350
+ .sh-panel-top .name { margin-right: auto; min-width: 0; font-weight: 700; font-size: 15px; letter-spacing: -0.01em;
351
+ overflow: hidden; text-overflow: ellipsis; white-space: nowrap }
324
352
  .sh-panel-scroll { overflow-y: auto; min-height: 0; padding-bottom: 2px; scrollbar-width: thin;
325
353
  scrollbar-color: var(--glass-ink-3) transparent }
326
354
  /* Sidebar list system (macOS-sidebar conventions): every row is 28px tall with an
@@ -449,7 +477,7 @@ body.sh-space .sh-node { pointer-events: none } /* space-drag
449
477
  /* ---- Play mode (SPEC-M2 §1): near-black stage, one centered device, auto-hiding bar.
450
478
  Fixed dark palette - the backdrop never follows the board theme, so no tokens here. */
451
479
  .sh-play { position: absolute; inset: 0; z-index: 30; background: #0a0a0b; display: flex;
452
- align-items: center; justify-content: center; animation: sh-play-in .28s ease-out }
480
+ align-items: center; justify-content: center; animation: sh-play-in .28s ease-out; cursor: auto }
453
481
  @keyframes sh-play-in { from { opacity: 0 } }
454
482
  /* the device wears the frame-node card language: same theme border tokens, the same
455
483
  masked 1px edge-light gradient ring (glass catching light), and a real drop shadow
@@ -498,6 +526,8 @@ body.sh-space .sh-node { pointer-events: none } /* space-drag
498
526
  /* board switcher */
499
527
  .sh-play-bar .bd-wrap { position: relative }
500
528
  .sh-play-bar .bd { width: auto; gap: 6px; padding: 0 10px 0 12px; white-space: nowrap; font: inherit }
529
+ .sh-play-bar .sh-play-update { width: auto; gap: 5px; padding: 0 11px; font-size: 12px; font-weight: 600; color: #6db3ff; white-space: nowrap }
530
+ .sh-play-bar .sh-play-update:hover { background: rgba(109, 179, 255, .16); color: #8ec5ff }
501
531
  /* no backdrop-filter here: the menu nests inside the blurred bar, and glass never nests
502
532
  (a nested backdrop-filter samples the bar, not the page) - near-opaque ink instead */
503
533
  .sh-play-menu { position: absolute; top: calc(100% + 10px); left: 0; min-width: 150px; padding: 4px;
@@ -31,6 +31,8 @@ window.addEventListener('unhandledrejection', (e) => post({ type: 'sh:stage-erro
31
31
  // pinch inside the stage must not zoom the parent page (same rule as the frame bridge)
32
32
  window.addEventListener('wheel', (e) => { if (e.ctrlKey || e.metaKey) e.preventDefault() }, { passive: false })
33
33
  document.addEventListener('gesturestart', (e) => e.preventDefault())
34
+ // B0.2: a nested scroll container hitting its boundary must not chain into the shell page
35
+ document.documentElement.style.overscrollBehavior = 'contain'
34
36
 
35
37
  interface Mounted { id: string; Frame: ComponentType; wrappers: ComponentType[] }
36
38
 
@@ -9,7 +9,7 @@ Minimal is enough - list the frames; the shell fills sizes from each frame's
9
9
  viewport and lays it out:
10
10
 
11
11
  ```json
12
- { "version": 1, "name": "checkout-compare", "auto": false,
12
+ { "version": 1, "name": "checkout-compare", "order": 1, "auto": false,
13
13
  "nodes": [ { "frame": "checkout-a/cart" }, { "frame": "checkout-b/cart" } ] }
14
14
  ```
15
15
 
@@ -18,8 +18,14 @@ viewport and lays it out:
18
18
  increasing `x`).
19
19
  - The human's tidy (`t`) and device views re-layout in frame-id order, so id
20
20
  ordering is the durable arrangement; explicit coordinates are one-off setups.
21
- - `auto: false` boards show exactly their list. `all-scenes` is auto-managed -
22
- never write it.
21
+ - **`"order": <n>` ranks the board in the switcher, and the LOWEST-ordered board is
22
+ the LANDING board the canvas opens on.** Rank them so the first is a tight, fast,
23
+ orienting board (an overview or the primary flow) - never a giant one. Boards
24
+ without an `order` sort after the ranked ones, by name. Set `order` deliberately on
25
+ every curated board; it is the first impression.
26
+ - `auto: false` boards show exactly their list. `all-scenes` is auto-managed (it holds
27
+ EVERY frame, so it is the heavy one) and always sinks to the BOTTOM of the switcher -
28
+ never the landing board, and never write its file.
23
29
  - Do not edit board files while the canvas is open unless asked; the shell owns
24
30
  their layout fields.
25
31
  - Use boards for comparisons: version A vs B vs C of a flow, side by side. Variant
@@ -46,8 +46,29 @@ present). Focus indicators need contrast AND a clearly visible change of appeara
46
46
  Check interactive states, text over images, and BOTH themes.
47
47
  Anything conveyed by color alone also needs text, shape, icon, or position.
48
48
 
49
+ ## Content-frame families (one palette, prose + diagrams)
50
+
51
+ In content frames (`Md`, `Diagram`), color-code by MEANING using the built-in family
52
+ names - never hand-roll hex or mermaid `classDef`. The same six families work in both,
53
+ so a sentence and the diagram beside it read as one color language:
54
+
55
+ - **Prose:** `:blue[the shipper's world]`, `:orange[the carrier market]`, `:purple[the
56
+ driver pool]`, `:green[...]`, `:red[...]`, `:gray[...]` inside any `Md` block.
57
+ - **Diagram nodes:** tag a node with the family - `HQ:::blue`, `Carriers:::orange`,
58
+ `Drivers:::purple`. No `classDef` needed; they're injected for you.
59
+
60
+ Pick ONE family per concept and hold it everywhere it appears (the intro word, the
61
+ diagram box, the section heading). That consistency is what lets a reader link the
62
+ picture to the prose at a glance. Reserve `gray` for the neutral/background ("the platform",
63
+ "out of scope"); use the vivid families for the actors that matter.
64
+
65
+ Node text: write labels as `Head :: gloss` - marver renders the head bold on top
66
+ with the gloss lighter and smaller below, so a box scans as label-then-detail,
67
+ never a run-on. Just the ` :: ` token; no backticks or `**` needed.
68
+
49
69
  ## Verify
50
70
 
51
71
  Every color has a stable role; attention lands on the intended action; the palette
52
72
  holds across quiet, dense, error, and empty states; both themes are composed; the
53
- result is recognizably THIS product, not a generic colorful treatment.
73
+ result is recognizably THIS product, not a generic colorful treatment. In content
74
+ frames, each concept keeps ONE family across prose and diagram.
@@ -92,10 +92,10 @@ export default () => (
92
92
  shows an in-frame card: fix the source, the frame heals live. Never hand-set
93
93
  colors or `%%{init}%%` themes - marver's palette is injected and source
94
94
  overrides are stripped.
95
- - `Img` shows `design/assets/<src>` with an optional caption; `h={n}` cover-crops
96
- to a fixed rendered height (the lever for optically aligning a mixed row - see
97
- "Images and mood boards" below). Blocks carry their own padding, border, and
98
- surface - never hand-manage spacing around them.
95
+ - `Img` shows `design/assets/<src>` with an optional caption, ALWAYS in full at its
96
+ natural aspect ratio - never cropped, never letterboxed. Size it by how many images
97
+ share its `Row` (fewer = bigger), not by a fixed height. Blocks carry their own
98
+ padding, border, and surface - never hand-manage spacing around them.
99
99
  - `intent` (`diagram` | `spec` | `moodboard` | `notes`) is the frame's PURPOSE,
100
100
  not its content mix - a frame with two diagrams and a paragraph is still the
101
101
  "diagram frame" if diagrams are why it exists. It drives the icon the human
@@ -110,27 +110,34 @@ a quadrant for prioritization, a state diagram for lifecycle logic, a sequence
110
110
  for API choreography. The syntax reference is the Mermaid docs:
111
111
  https://mermaid.js.org/intro/ - pull the one page you need, apply, return.
112
112
 
113
- **Color carries meaning - and the floor is never gray-on-gray.** The marver
114
- theme already colors every node (accent-washed fills, accent borders, both
115
- modes) - a default diagram looks designed with zero effort, so never hand-set
116
- grays "to be safe" and never re-theme (init directives are stripped anyway).
117
- Where the diagram has SEMANTICS, add them with `classDef` on top:
113
+ **Two pieces of built-in sugar make a flowchart read well with zero fiddling -
114
+ use them, don't hand-roll their raw mermaid equivalents.**
115
+
116
+ *Label hierarchy (`::`).* Write a node label as `Head :: gloss` and marver
117
+ renders the **head bold** on top with the gloss on a lighter, smaller line
118
+ below - no backticks, no `**`, no `<br>`. A box should scan as label-then-
119
+ detail, so lead with the actor and let the example follow:
118
120
 
119
121
  ```
120
122
  flowchart LR
121
- A[Draft] --> B{Review?} -->|approved| C[Published]
122
- B -->|rejected| D[Archived]
123
- classDef win fill:#34C759,stroke:#248A3D,color:#fff
124
- classDef stop fill:#FF383C,stroke:#D70015,color:#fff
125
- class C win
126
- class D stop
123
+ S["Shipper :: the company that needs freight moved"]:::blue
124
+ C["Carrier :: the trucking company that hauls it"]:::orange
125
+ D["Driver :: the person behind the wheel"]:::purple
126
+ S --> C --> D
127
127
  ```
128
128
 
129
- Use the system palette (the same 12 colors the series ramp uses), a few
130
- classes at most, and never encode meaning in color ALONE - the label or shape
131
- must carry it too. Check the diagram in BOTH themes (`d`): fills flip with
132
- the theme, hand-set classDef colors do not, so pick values that hold on both
133
- grounds (the system colors do).
129
+ *Family colors (`:::name`).* Tag a node with a built-in family and it gets a
130
+ filled, on-brand color with a legible border in both themes - no `classDef`.
131
+ The SAME six names work in `Md` prose (`:blue[the shipper's world]`), so a
132
+ sentence and the diagram beside it read as one color language:
133
+ `blue orange purple green red gray`. Pick ONE family per concept and hold it
134
+ everywhere; reserve `gray` for the neutral/background actor. Never encode
135
+ meaning in color ALONE - the label carries it too.
136
+
137
+ The marver theme already accent-washes every default node (both modes), so a
138
+ plain flowchart looks designed with zero effort - never hand-set grays "to be
139
+ safe" and never re-theme (init directives are stripped anyway). Check in BOTH
140
+ themes (`d`).
134
141
 
135
142
  ## Images and mood boards
136
143
 
@@ -147,21 +154,23 @@ screenshots, official brand logos, product visuals - download into
147
154
  of described imagery every time (the full asset rules: instructions/craft.md,
148
155
  "Real assets").
149
156
 
150
- **Size images to be SEEN, and make rows read as one set:**
151
-
152
- - An image that renders as a stamp is a defect. The image IS the content of a
153
- mood board - give the important one most of a row, let supporting shots share
154
- a row, and never pack so many into one `Row` that each collapses below
155
- legibility.
156
- - Equal component widths do NOT make unequal images look equal - aspect ratios
157
- and internal density differ, so a row of same-width `Img` blocks can still
158
- read ragged. Normalize a mixed row with a shared rendered height:
159
- `<Img src="..." h={240} />` cover-crops every image in the row to one height,
160
- which is what makes them READ aligned. Group similar aspects together when
161
- cropping would destroy the shot.
162
- - Alignment is judged on the RENDER, not the props: after composing, look at the
163
- actual frame (screenshot it if you can) and adjust until the rows sit
164
- optically consistent. "The code says they're the same width" proves nothing.
157
+ **Size images to be SEEN - by row grouping, never by cropping:**
158
+
159
+ - The image IS the content: it always renders in FULL at its natural aspect ratio, so
160
+ a screenshot stays fully legible and is never sliced. You size it by how many images
161
+ share a `Row` - fewer per row = larger. Give a detailed screenshot its own row or a
162
+ pair; let small supporting shots share a row of three or four. An image that renders
163
+ as a stamp is a defect - pull it into a shorter row.
164
+ - A row of same-aspect images (e.g. app screenshots, all the same window shape) lines
165
+ up as one set on its own: equal column width + equal aspect = equal height, with no
166
+ fixed-height prop. Only mixed aspects read ragged - split those into their own rows
167
+ by shape rather than forcing a height (forcing one would crop or letterbox the shot).
168
+ - Reach for `layout="wide"` on image-heavy reference frames so each shot has room, and
169
+ never set a frame height - the frame auto-heights to fit everything the canvas
170
+ measures. Marver renders images crisp and zooms fast, so fine detail is one zoom away.
171
+ - Judge on the RENDER, not the props: after composing, look at the actual frame
172
+ (screenshot it if you can) and adjust the per-row count until it reads well. "The code
173
+ says they're the same width" proves nothing.
165
174
 
166
175
  ## When Shape ends
167
176