@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.
- package/CHANGELOG.md +107 -0
- package/dist/{build-p3xmXU3b.mjs → build-BZaPa2DS.mjs} +11 -5
- package/dist/cli.mjs +3 -3
- package/dist/{dev-DZi1yRhn.mjs → dev-DaPQ9xA5.mjs} +57 -8
- package/dist/{init-Ck8z-HiD.mjs → init-DsCUmlCW.mjs} +1 -1
- package/dist/{manifest-DW-T52MM.mjs → manifest-C8FODq2S.mjs} +26 -1
- package/dist/{plugin-CtQqO5ZZ.mjs → plugin-wMY9lNf3.mjs} +67 -20
- package/package.json +2 -1
- package/src/client/content/diagram.tsx +46 -2
- package/src/client/content/img-lod.ts +107 -0
- package/src/client/content/index.tsx +37 -9
- package/src/client/content/md.ts +29 -0
- package/src/client/content/palette.ts +6 -0
- package/src/client/frame-host/bridge.js +40 -5
- package/src/client/frame-host/serialize.ts +195 -0
- package/src/client/shell/App.tsx +75 -18
- package/src/client/shell/Comments.tsx +5 -4
- package/src/client/shell/Play.tsx +24 -0
- package/src/client/shell/canvas/Canvas.tsx +69 -30
- package/src/client/shell/canvas/FrameNode.tsx +104 -8
- package/src/client/shell/canvas/camera-broadcast.ts +44 -0
- package/src/client/shell/canvas/frame-registry.ts +18 -0
- package/src/client/shell/canvas/snapshots.ts +233 -0
- package/src/client/shell/cursor-arrow-dark.svg +1 -0
- package/src/client/shell/cursor-arrow.svg +1 -0
- package/src/client/shell/labels.ts +10 -0
- package/src/client/shell/perf.ts +92 -0
- package/src/client/shell/store.ts +130 -22
- package/src/client/shell/styles.css +37 -7
- package/src/client/stage/main.tsx +2 -0
- package/templates/instructions/boards.md +9 -3
- package/templates/instructions/reference/color.md +22 -1
- 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
|
|
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
|
|
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);
|
|
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
|
-
|
|
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;
|
|
323
|
-
.sh-panel-top .
|
|
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
|
-
-
|
|
22
|
-
|
|
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
|
|
96
|
-
|
|
97
|
-
|
|
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
|
-
**
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
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
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
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
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
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
|
|
151
|
-
|
|
152
|
-
-
|
|
153
|
-
|
|
154
|
-
a row
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
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
|
|