@marver-design/marver 0.17.0 → 0.19.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 +96 -0
- package/README.md +3 -1
- package/dist/bake-kaf5kGZ7.mjs +747 -0
- package/dist/{build-D_g53Bp2.mjs → build-7ed5H2vT.mjs} +215 -13
- package/dist/cli.mjs +19 -11
- package/dist/{comments-oYcZ3cE-.mjs → comments-ClVgfQib.mjs} +1 -1
- package/dist/{daemon-Bbh_jmui.mjs → daemon-DbHvLQUL.mjs} +1 -1
- package/dist/{dev-LnIISva5.mjs → dev-D3mP2x27.mjs} +205 -10
- package/dist/{init-B7YhcN2o.mjs → init-BQYCS3EU.mjs} +2 -2
- package/dist/{manifest-CaslQIAO.mjs → manifest-B01PSyDc.mjs} +34 -7
- package/dist/{marver-id-gate-D6By7XHj.mjs → marver-id-gate-B_idGdHm.mjs} +1 -1
- package/dist/{plugin-D2msH1cj.mjs → plugin-DI-7NAnx.mjs} +108 -29
- package/dist/{poster-BEjUcQP3.mjs → poster-DNh6N27C.mjs} +1 -1
- package/dist/publish-bakes-Dp-ZFk3d.mjs +216 -0
- package/dist/{serve-Bcwfpvhl.mjs → serve-z5qtj_wJ.mjs} +3 -3
- package/dist/{share-Gqo_Ygqw.mjs → share--bdSc4G5.mjs} +1 -1
- package/dist/{shot-BzQ0PXKH.mjs → shot-BFEuYbaz.mjs} +1 -1
- package/dist/{shot-DlmTO8AF.mjs → shot-DMDvDbeP.mjs} +14 -7
- package/dist/{work-lzC-lPY0.mjs → work-0YopuMt9.mjs} +1 -1
- package/docs/publish.md +27 -5
- package/docs/sticky-notes.md +43 -0
- package/package.json +2 -1
- package/src/client/content/diagram.tsx +27 -1
- package/src/client/content/index.tsx +2 -2
- package/src/client/content/md.ts +48 -0
- package/src/client/frame-host/bridge.js +4 -9
- package/src/client/frame-host/main.tsx +20 -6
- package/src/client/shell/App.tsx +14 -63
- package/src/client/shell/Comments.tsx +77 -14
- package/src/client/shell/Play.tsx +6 -3
- package/src/client/shell/canvas/Canvas.tsx +12 -3
- package/src/client/shell/canvas/FrameNode.tsx +99 -89
- package/src/client/shell/canvas/Sticky.tsx +284 -0
- package/src/client/shell/canvas/admission.ts +70 -0
- package/src/client/shell/canvas/sleep.ts +194 -0
- package/src/client/shell/goto.ts +72 -0
- package/src/client/shell/notes.ts +151 -0
- package/src/client/shell/store.ts +36 -18
- package/src/client/shell/styles.css +92 -25
- package/src/client/shell/tidy.ts +20 -2
- package/src/shared/sleep-rule.ts +41 -0
- package/templates/AGENTS-embedded.md +15 -0
- package/templates/AGENTS-studio.md +15 -0
- package/templates/instructions/craft.md +9 -0
- package/templates/instructions/publish.md +62 -9
- package/templates/instructions/shape.md +62 -0
- package/src/client/frame-host/serialize.ts +0 -195
- package/src/client/shell/canvas/snapshots.ts +0 -233
|
@@ -150,21 +150,17 @@ button svg { pointer-events: none } /* event targets stay on the button - pan
|
|
|
150
150
|
opacity: var(--grid-alpha, 1) }
|
|
151
151
|
.sh-content { will-change: auto }
|
|
152
152
|
#sh-world { position: relative; width: 1px; height: 1px }
|
|
153
|
-
#sh-world.sh-gesturing .sh-live { pointer-events: none } /* law G-4
|
|
154
|
-
|
|
153
|
+
#sh-world.sh-gesturing .sh-live { pointer-events: none } /* law G-4 */
|
|
154
|
+
/* no gesture promotion (a will-change layer on the world was measured harmful: 18-29 dropped frames
|
|
155
|
+
per pan on a 32-frame board against 0-3 without, spec 16) */
|
|
155
156
|
/* preset transitions: armed by animateLayout() around device/tidy mutations - nodes ease
|
|
156
157
|
to their new frame on the same 320ms ease-out the camera fit animates with */
|
|
157
158
|
#sh-world.sh-preset .sh-node { transition: transform .32s cubic-bezier(.25,.46,.45,.94),
|
|
158
159
|
width .32s cubic-bezier(.25,.46,.45,.94), height .32s cubic-bezier(.25,.46,.45,.94) }
|
|
159
|
-
#sh-world.sh-preset .sh-node-body,
|
|
160
|
-
#sh-world.sh-preset .sh-node .sh-live { transition: width .32s cubic-bezier(.25,.46,.45,.94),
|
|
160
|
+
#sh-world.sh-preset .sh-node-body { transition: width .32s cubic-bezier(.25,.46,.45,.94),
|
|
161
161
|
height .32s cubic-bezier(.25,.46,.45,.94) }
|
|
162
|
-
/*
|
|
163
|
-
|
|
164
|
-
body - reflows smoothly every frame (real CSS, the device-sweep fix). A frame WITHOUT a usable
|
|
165
|
-
cover keeps the live width animation above. */
|
|
166
|
-
body:not(.sh-laser):not(.sh-commenting) #sh-world.sh-preset .sh-node:has(.sh-lean[data-ready]):not(.interact) .sh-live { transition: none }
|
|
167
|
-
|
|
162
|
+
/* the live iframe takes its new size at once (ONE reflow per sweep) while the body's box eases to it;
|
|
163
|
+
the frame sleeps again under the new size once it settles */
|
|
168
164
|
/* cursor conventions (Figma): arrow everywhere; grab only while space is held */
|
|
169
165
|
body.sh-space .sh-canvas { cursor: grab }
|
|
170
166
|
body.sh-space.sh-panning .sh-canvas { cursor: grabbing }
|
|
@@ -199,8 +195,7 @@ body.sh-space .sh-node { pointer-events: none } /* space-drag
|
|
|
199
195
|
font-size: 11px; color: var(--head-dim);
|
|
200
196
|
border-bottom: 1px solid var(--node-brd); user-select: none;
|
|
201
197
|
border-radius: var(--r-node) var(--r-node) 0 0;
|
|
202
|
-
background-color: var(--head-bg); background-image: var(--head-sheen)
|
|
203
|
-
backdrop-filter: var(--blur); -webkit-backdrop-filter: var(--blur) }
|
|
198
|
+
background-color: var(--head-bg); background-image: var(--head-sheen) } /* no backdrop-filter: one render surface less per frame */
|
|
204
199
|
.sh-node-head .id { font-weight: 600; color: var(--head-ink); overflow: hidden; text-overflow: ellipsis; white-space: nowrap }
|
|
205
200
|
/* frame icons: every sidebar row leads with one */
|
|
206
201
|
.sh-node-head .iicon { flex: none; color: var(--head-dim) }
|
|
@@ -210,20 +205,9 @@ body.sh-space .sh-node { pointer-events: none } /* space-drag
|
|
|
210
205
|
.sh-panel .sub.vrow { padding-left: 51px }
|
|
211
206
|
.sh-node-head .dim { margin-left: auto; color: var(--head-dim); flex: none; font-variant-numeric: tabular-nums }
|
|
212
207
|
.sh-node-body { position: relative; background: var(--node-bg); border-radius: 0 0 var(--r-node) var(--r-node); overflow: hidden }
|
|
208
|
+
/* no content-visibility on the body: it flips frames between skipped and rendered as they cross the
|
|
209
|
+
viewport during a zoom, and the raster storm that follows left the page unfinished 2.5 s later */
|
|
213
210
|
.sh-node iframe { border: 0; display: block }
|
|
214
|
-
/* LEAN-PRIMARY: the lean DOM-snapshot <iframe> (static html, 0 JS) is what you SEE for a
|
|
215
|
-
passive frame - at rest AND during pan/zoom/resize. There is NO per-gesture swap between the lean
|
|
216
|
-
and the live iframe (the swap shifted text ~1-2px = "jiggle", and flashed mermaid/theme colors,
|
|
217
|
-
because two documents never render pixel-identically). The live app (.sh-live) sits underneath and
|
|
218
|
-
shows ONLY when: the frame is interacted (.interact), laser/comment mode is on, or the lean is not
|
|
219
|
-
yet built (no data-ready = live-fallback). Hard cut, never a crossfade - two ~1px-offset text docs
|
|
220
|
-
would ghost into double text. */
|
|
221
|
-
.sh-lean { position: absolute; inset: 0; width: 100%; height: 100%; border: 0; z-index: 1;
|
|
222
|
-
opacity: 0; pointer-events: none; transition: none; background: var(--node-bg) }
|
|
223
|
-
.sh-lean[data-ready] { opacity: 1 }
|
|
224
|
-
.sh-node.interact .sh-lean,
|
|
225
|
-
body.sh-laser .sh-lean,
|
|
226
|
-
body.sh-commenting .sh-lean { opacity: 0 }
|
|
227
211
|
.sh-overlay { position: absolute; inset: 0; z-index: 2 } /* above the lean (z1) so drag-by-body works; inherits the app arrow cursor */
|
|
228
212
|
.sh-node.interact { border-color: var(--interact);
|
|
229
213
|
outline: calc(2px * var(--sh-inv, 1)) solid var(--interact);
|
|
@@ -1038,6 +1022,8 @@ body.sh-hide-ui .sh-play-nav { display: none !important }
|
|
|
1038
1022
|
margin-left: calc(-1 * clamp(8px, calc(12px * var(--sh-inv, 1)), 36px)) }
|
|
1039
1023
|
.cm-card.parked.dock-l.flank-shim { margin-left: calc(-1 * clamp(18px, calc(28px * var(--sh-inv, 1)), 68px)) }
|
|
1040
1024
|
.cm-card.parked.dock-l.flank-badge { margin-left: calc(-1 * clamp(26px, calc(44px * var(--sh-inv, 1)), 108px)) }
|
|
1025
|
+
/* a sticky column (spec 18) is world-sized: the card clears it and its gutter */
|
|
1026
|
+
.cm-card.parked.dock-l.flank-note { margin-left: calc(-1 * (var(--sh-note-w, 260px) + 36px)) }
|
|
1041
1027
|
/* PLAY: fixed beside the static device, viewport-clamped (position comes inline). Near-solid
|
|
1042
1028
|
ground - the stage floats the card over arbitrary artwork on a dark room, glass is unreadable
|
|
1043
1029
|
there. No transform: play has no zoom, and a leaked --sh-inv must never scale it. */
|
|
@@ -1281,3 +1267,84 @@ body.sh-hide-ui .sh-play-nav { display: none !important }
|
|
|
1281
1267
|
.sh-slides-strip .bar { width: 140px; height: 3px; border-radius: 999px; background: var(--glass-hover); overflow: hidden }
|
|
1282
1268
|
.sh-slides-strip .bar i { display: block; height: 100%; border-radius: inherit; background: var(--accent); transition: width .25s ease }
|
|
1283
1269
|
@media (max-width: 480px) { .sh-slides-strip .bar { width: 80px } }
|
|
1270
|
+
|
|
1271
|
+
|
|
1272
|
+
/* ---- sticky notes (spec 18): the yellow column left of a frame ------------------------------
|
|
1273
|
+
World-sized (it scales with the canvas, like the artwork); the fold's hit target keeps a screen
|
|
1274
|
+
minimum through --sh-inv. Yellow in both shell themes - a sticky is yellow. */
|
|
1275
|
+
.sh-node { --note-bg: #fff3a3; --note-bg-2: #ffeb85; --note-ink: #2b2500; --note-dim: rgba(43, 37, 0, .62);
|
|
1276
|
+
--note-line: rgba(107, 90, 0, .28); --note-link: #7a4d00; --note-fold: #e9cf5e; --note-fold-2: #c9ae3c }
|
|
1277
|
+
/* dark canvas: a deeper mustard so the paper reads as paper, not as a highlight; ink darker for contrast */
|
|
1278
|
+
.sh-node[data-theme="dark"] { --note-bg: #e6c94f; --note-bg-2: #d6b73c; --note-ink: #1f1a00; --note-dim: rgba(31, 26, 0, .68);
|
|
1279
|
+
--note-line: rgba(70, 56, 0, .35); --note-link: #5c3a00; --note-fold: #c9aa33; --note-fold-2: #8f7518 }
|
|
1280
|
+
.sh-notes { position: absolute; right: 100%; top: 0; margin-right: 24px;
|
|
1281
|
+
display: flex; flex-direction: column; align-items: flex-end; gap: 12px; pointer-events: none }
|
|
1282
|
+
.sh-notes.below-vbadge { top: 72px }
|
|
1283
|
+
.sh-notes > * { pointer-events: auto }
|
|
1284
|
+
.sh-sticky { position: relative; box-sizing: border-box; background: var(--note-bg); color: var(--note-ink);
|
|
1285
|
+
border-radius: 2px 10px 2px 2px; padding: 14px 16px 12px;
|
|
1286
|
+
box-shadow: 0 1px 2px rgba(60, 45, 0, .18), 0 6px 18px -6px rgba(60, 45, 0, .35);
|
|
1287
|
+
font: 400 13px/1.45 -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
|
|
1288
|
+
transform-origin: top right; opacity: 1; visibility: visible;
|
|
1289
|
+
transition: transform .18s cubic-bezier(.2, .7, .3, 1), opacity .16s ease, visibility 0s linear 0s;
|
|
1290
|
+
user-select: text; -webkit-user-select: text; cursor: default }
|
|
1291
|
+
.sh-sticky[data-sticky="scene"] { padding: 18px 20px 16px; font-size: 14px }
|
|
1292
|
+
.sh-notes.off .sh-sticky { transform: scale(.04); opacity: 0; visibility: hidden;
|
|
1293
|
+
transition: transform .18s cubic-bezier(.4, 0, .8, .4), opacity .14s ease .04s, visibility 0s linear .18s; pointer-events: none }
|
|
1294
|
+
/* the dog-ear: on the column it is the top sticky's folded corner; folded, it is the tab that stays */
|
|
1295
|
+
.sh-notes-fold { position: absolute; top: 0; right: 0; z-index: 2; padding: 0; border: 0; cursor: pointer;
|
|
1296
|
+
width: clamp(22px, calc(14px * var(--sh-inv, 1)), 72px); height: clamp(22px, calc(14px * var(--sh-inv, 1)), 72px);
|
|
1297
|
+
border-radius: 0 10px 0 4px;
|
|
1298
|
+
background: linear-gradient(225deg, transparent 50%, var(--note-fold) 50%, var(--note-fold-2) 100%);
|
|
1299
|
+
transition: background .18s ease, border-radius .18s ease, box-shadow .18s ease, transform .18s ease }
|
|
1300
|
+
.sh-notes-fold:hover { filter: brightness(.96) }
|
|
1301
|
+
.sh-notes-fold:focus-visible { outline: 2px solid var(--accent, #2f6df6); outline-offset: 2px }
|
|
1302
|
+
.sh-notes.off .sh-notes-fold { border-radius: 3px 3px 3px 8px; background: linear-gradient(160deg, var(--note-bg) 0%, var(--note-bg-2) 100%);
|
|
1303
|
+
box-shadow: 0 1px 2px rgba(60, 45, 0, .25), 0 3px 8px -2px rgba(60, 45, 0, .35) }
|
|
1304
|
+
.sh-notes.off .sh-notes-fold::after { content: ""; position: absolute; right: 0; top: 0; width: 38%; height: 38%;
|
|
1305
|
+
border-radius: 0 3px 0 3px; background: linear-gradient(225deg, transparent 50%, var(--note-fold-2) 50%) }
|
|
1306
|
+
@media (prefers-reduced-motion: reduce) { .sh-sticky, .sh-notes-fold { transition: none } }
|
|
1307
|
+
/* comment mode: hover names the element the click would pick; the lock is the shell's own outline */
|
|
1308
|
+
body.sh-commenting .sh-sticky-body :is(p, li, h1, h2, h3, h4, blockquote, pre, img, table, .sh-sticky-diagram):hover { outline: 1.5px solid var(--accent, #2f6df6); outline-offset: 2px; border-radius: 3px }
|
|
1309
|
+
body.sh-commenting .sh-sticky-body { cursor: crosshair }
|
|
1310
|
+
.sh-sticky-body [data-sh-lock] { outline: 2px solid hsl(var(--cm-h, 48) 80% 42%); outline-offset: 3px; border-radius: 3px;
|
|
1311
|
+
box-shadow: 0 0 0 5px hsl(var(--cm-h, 48) 90% 60% / .28) }
|
|
1312
|
+
/* prose: the Md block's vocabulary, sized for an aside */
|
|
1313
|
+
.sh-sticky-body > :first-child { margin-top: 0 }
|
|
1314
|
+
.sh-sticky-body > :last-child { margin-bottom: 0 }
|
|
1315
|
+
.sh-sticky-body h1, .sh-sticky-body h2, .sh-sticky-body h3, .sh-sticky-body h4 { margin: 0 0 6px; line-height: 1.25; letter-spacing: -0.01em; text-wrap: balance }
|
|
1316
|
+
.sh-sticky-body h1 { font-size: 1.28em; font-weight: 700 }
|
|
1317
|
+
.sh-sticky-body h2 { font-size: 1.14em; font-weight: 650 }
|
|
1318
|
+
.sh-sticky-body h3, .sh-sticky-body h4 { font-size: 1em; font-weight: 650 }
|
|
1319
|
+
.sh-sticky-body p, .sh-sticky-body ul, .sh-sticky-body ol, .sh-sticky-body blockquote, .sh-sticky-body pre, .sh-sticky-body table { margin: 0 0 8px }
|
|
1320
|
+
.sh-sticky-body ul, .sh-sticky-body ol { padding-left: 18px }
|
|
1321
|
+
.sh-sticky-body li { margin: 2px 0 }
|
|
1322
|
+
.sh-sticky-body li > ul, .sh-sticky-body li > ol { margin-bottom: 0 }
|
|
1323
|
+
.sh-sticky-body blockquote { padding: 2px 0 2px 10px; border-left: 3px solid var(--note-line); color: var(--note-dim) }
|
|
1324
|
+
.sh-sticky-body a { color: var(--note-link); text-decoration: underline; text-underline-offset: 2px; text-decoration-color: rgba(122, 77, 0, .45) }
|
|
1325
|
+
.sh-sticky-body a:hover { text-decoration-color: currentColor }
|
|
1326
|
+
.sh-sticky-body a[data-goto]::after { content: "\2197"; font-size: .8em; margin-left: 2px; text-decoration: none; display: inline-block }
|
|
1327
|
+
.sh-sticky-body code { font: 500 .9em/1.4 ui-monospace, SFMono-Regular, Menlo, Consolas, monospace; background: rgba(43, 37, 0, .07); padding: 1px 4px; border-radius: 3px }
|
|
1328
|
+
.sh-sticky-body pre { padding: 8px 10px; border-radius: 6px; background: rgba(43, 37, 0, .07); overflow-x: auto; white-space: pre }
|
|
1329
|
+
.sh-sticky-body pre code { background: none; padding: 0; font-weight: 400 }
|
|
1330
|
+
.sh-sticky-body pre.err { background: rgba(190, 40, 20, .1); color: #7a1f0f }
|
|
1331
|
+
.sh-sticky-body hr { border: 0; border-top: 1px solid var(--note-line); margin: 10px 0 }
|
|
1332
|
+
.sh-sticky-body img { max-width: 100%; border-radius: 4px; display: block }
|
|
1333
|
+
.sh-sticky-body strong { font-weight: 650 }
|
|
1334
|
+
.sh-sticky-body table { border-collapse: collapse; font-size: .92em; width: 100%; table-layout: auto }
|
|
1335
|
+
.sh-sticky-body td, .sh-sticky-body th { overflow-wrap: anywhere }
|
|
1336
|
+
.sh-sticky-body th, .sh-sticky-body td { border: 1px solid var(--note-line); padding: 3px 6px; text-align: left; vertical-align: top }
|
|
1337
|
+
.sh-sticky-body th { font-weight: 650; background: rgba(43, 37, 0, .05) }
|
|
1338
|
+
.sh-sticky-body input[type="checkbox"] { margin: 0 6px 0 0; vertical-align: -1px }
|
|
1339
|
+
.sh-sticky-body .mv-md-noimg { color: var(--note-dim); font-style: italic }
|
|
1340
|
+
.sh-sticky-body .mv-c-blue { color: #0070d6 } .sh-sticky-body .mv-c-orange { color: #c9640a } .sh-sticky-body .mv-c-purple { color: #9a23ad }
|
|
1341
|
+
.sh-sticky-body .mv-c-green { color: #1a8a3f } .sh-sticky-body .mv-c-red { color: #c72a22 } .sh-sticky-body .mv-c-gray { color: #6b7480 }
|
|
1342
|
+
/* the hand-drawn diagram: the fence's place, the paper's colors, scaled to the column */
|
|
1343
|
+
.sh-sticky-diagram { margin: 4px 0 10px; overflow: visible; display: flex; justify-content: center }
|
|
1344
|
+
.sh-sticky-diagram svg { display: block; max-width: 100%; height: auto; margin: 0 auto;
|
|
1345
|
+
/* the canvas scales the world with a CSS transform, and Chrome lays SVG text out ONCE for the
|
|
1346
|
+
scale it was inserted at (a diagram born at 13 % zoom keeps 13 %-zoom glyph positions at
|
|
1347
|
+
140 %: labels drift, some vanish). geometricPrecision lays glyphs out scale-free - measured,
|
|
1348
|
+
research/notes/diagprobe5.ts */
|
|
1349
|
+
text-rendering: geometricPrecision }
|
|
1350
|
+
.sh-sticky-diagram svg text, .sh-sticky-diagram svg tspan { text-rendering: geometricPrecision }
|
package/src/client/shell/tidy.ts
CHANGED
|
@@ -1,4 +1,10 @@
|
|
|
1
|
-
export interface TidyNode {
|
|
1
|
+
export interface TidyNode {
|
|
2
|
+
key: string; frame: string; scene: string; group?: string; variant?: string; w: number; h: number
|
|
3
|
+
/** Sticky notes (spec 18), world px INCLUDING the gutter: reserved in front of this node
|
|
4
|
+
* (its own note), and in front of the scene's first placed node (the scene note - carried on
|
|
5
|
+
* every member, applied once). A column holds both, so the wider wins, never the sum. */
|
|
6
|
+
noteW?: number; sceneNoteW?: number
|
|
7
|
+
}
|
|
2
8
|
export interface Placed { key: string; x: number; y: number }
|
|
3
9
|
|
|
4
10
|
// Lane-flow grammar: one shape at both scopes. A scope is rows XOR columns
|
|
@@ -43,7 +49,7 @@ const box = (id: string, parts: Array<{ key: string; dx: number; dy: number; w:
|
|
|
43
49
|
const runBox = (id: string, run: TidyNode[]): Box => {
|
|
44
50
|
const parts: Array<{ key: string; dx: number; dy: number; w: number; h: number }> = []
|
|
45
51
|
let dx = 0
|
|
46
|
-
for (const n of run) { parts.push({ key: n.key, dx, dy: 0, w: n.w, h: n.h }); dx += n.w + frameGapX(n.w) }
|
|
52
|
+
for (const n of run) { dx += n.noteW ?? 0; parts.push({ key: n.key, dx, dy: 0, w: n.w, h: n.h }); dx += n.w + frameGapX(n.w) }
|
|
47
53
|
return box(id, parts)
|
|
48
54
|
}
|
|
49
55
|
|
|
@@ -264,6 +270,18 @@ export function tidy(nodes: TidyNode[], layout?: BoardLayout, warn: Warn = () =>
|
|
|
264
270
|
for (const scene of scenes) {
|
|
265
271
|
const members = nodes.filter((n) => n.scene === scene)
|
|
266
272
|
const m = layoutScene(scene, members, layout?.scenes?.[scene], warn)
|
|
273
|
+
// the scene note sits in front of the scene's first node in reading order (the host the
|
|
274
|
+
// shell picks): widen the scene box on the left by what that node's own note does not cover
|
|
275
|
+
const sceneNoteW = Math.max(0, ...members.map((n) => n.sceneNoteW ?? 0))
|
|
276
|
+
if (sceneNoteW) {
|
|
277
|
+
const placed = members.filter((n) => m.has(n.key))
|
|
278
|
+
const first = placed.reduce<TidyNode | null>((best, n) => {
|
|
279
|
+
const p = m.get(n.key)!, b = best && m.get(best.key)!
|
|
280
|
+
return !b || p.y < b.y || (p.y === b.y && p.x < b.x) ? n : best
|
|
281
|
+
}, null)
|
|
282
|
+
const extra = sceneNoteW - (first?.noteW ?? 0)
|
|
283
|
+
if (extra > 0) for (const [k, p] of m) m.set(k, { x: p.x + extra, y: p.y })
|
|
284
|
+
}
|
|
267
285
|
sceneMaps.set(scene, m)
|
|
268
286
|
const parts = members
|
|
269
287
|
.filter((n) => m.has(n.key))
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The paint override a sleeping backdrop-filter element wears (spec 16) - ONE definition, used by
|
|
3
|
+
* the compiler inside the frame it certifies (src/server/bake.ts embeds these functions' source in
|
|
4
|
+
* a page script) and by the shell inside the frame it puts to sleep (src/client/shell/canvas/sleep.ts).
|
|
5
|
+
* Plain ES2020 with no imports or closures, so `Function.prototype.toString` carries it whole.
|
|
6
|
+
*
|
|
7
|
+
* The composition, bottom to top: the certified texture (the element's filtered backdrop, clipped to
|
|
8
|
+
* its border box the way the effect is), the element's own colour under the clip the author gave it,
|
|
9
|
+
* the element's own images, then its content.
|
|
10
|
+
*
|
|
11
|
+
* `backdrop-filter: none`: the element must stop being an effect layer. An effect layer is its own
|
|
12
|
+
* compositor layer with its own tiles, and thirty of them per frame are what a headed Chrome cannot
|
|
13
|
+
* re-raster fast enough under a pan or a zoom - the presented frames show the frame body missing
|
|
14
|
+
* while the pills draw (research/hifi/glitch.ts). `blur(0px)` measured better only in headless
|
|
15
|
+
* Chrome, whose forced screenshots never show a missing tile. A static `filter` keeps what the
|
|
16
|
+
* backdrop-filter gave layout - the containing block of fixed descendants (a hidden checkbox input
|
|
17
|
+
* is one) and the stacking context - without a direct compositing reason (a video, a canvas or a
|
|
18
|
+
* will-change inside still promotes that one element): `opacity(1)` when the author set none, the
|
|
19
|
+
* author's own filter otherwise.
|
|
20
|
+
*/
|
|
21
|
+
export interface OwnBackground { img: string; color: string; size: string; pos: string; rep: string; org: string; clip: string; filter: string }
|
|
22
|
+
|
|
23
|
+
/** The element's own background and filter, read once BEFORE any override touches it. */
|
|
24
|
+
export function readOwn(cs: CSSStyleDeclaration): OwnBackground {
|
|
25
|
+
return { img: cs.backgroundImage, color: cs.backgroundColor, size: cs.backgroundSize, pos: cs.backgroundPosition, rep: cs.backgroundRepeat, org: cs.backgroundOrigin, clip: cs.backgroundClip, filter: cs.filter }
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function sleepRule(selector: string, o: OwnBackground, texture: string): string {
|
|
29
|
+
const img = o.img === 'none' ? '' : o.img + ','
|
|
30
|
+
// the authored colour paints under the LAST layer's clip (CSS Backgrounds 3)
|
|
31
|
+
const colorClip = o.clip.split(',').pop()!.trim() || 'border-box'
|
|
32
|
+
return selector + '{backdrop-filter:none!important;-webkit-backdrop-filter:none!important;' +
|
|
33
|
+
(o.filter === 'none' ? 'filter:opacity(1)!important;' : '') +
|
|
34
|
+
'background-color:transparent!important;' +
|
|
35
|
+
'background-image:' + img + 'linear-gradient(' + o.color + ',' + o.color + '),url("' + texture + '")!important;' +
|
|
36
|
+
'background-size:' + (img ? o.size + ',' : '') + 'auto,100% 100%!important;' +
|
|
37
|
+
'background-position:' + (img ? o.pos + ',' : '') + '0 0,0 0!important;' +
|
|
38
|
+
'background-repeat:' + (img ? o.rep + ',' : '') + 'no-repeat,no-repeat!important;' +
|
|
39
|
+
'background-origin:' + (img ? o.org + ',' : '') + 'border-box,border-box!important;' +
|
|
40
|
+
'background-clip:' + (img ? o.clip + ',' : '') + colorClip + ',border-box!important}'
|
|
41
|
+
}
|
|
@@ -118,6 +118,21 @@ Report where the request came from: chat requests get chat replies; only comment
|
|
|
118
118
|
- CONTENT frames (specs, mermaid diagrams, mood boards) are ordinary tsx frames built
|
|
119
119
|
from the block primitives in '@marver-design/marver/content' - import them directly
|
|
120
120
|
in the frame file and declare meta.intent. Full guide: instructions/shape.md.
|
|
121
|
+
- STICKY NOTES: the aside beside a frame. One markdown file, nothing to declare:
|
|
122
|
+
`design/scenes/<scene>/<frame>.note.md` beside the frame file (tsx, jsx or html),
|
|
123
|
+
`design/scenes/<scene>/_note.md` for the scene (it shows beside the scene's first frame),
|
|
124
|
+
`design/components/<name>.note.md` for a component. It renders as a yellow note left of the
|
|
125
|
+
frame on every canvas, dev and published. Write one when a reader needs what the screen
|
|
126
|
+
cannot say: what the frame is for, how two variations differ, how a mechanism works, an
|
|
127
|
+
open question. Markdown (headings, lists, tables, emphasis, code), `[text](goto:scene/frame)`
|
|
128
|
+
links that jump to a frame, images from design/assets/, and ```mermaid fences drawn
|
|
129
|
+
hand-sketched in the note's own yellow - plain mermaid, any family, no init/theme/style
|
|
130
|
+
lines, no URLs (flowchart, sequence, state, class, ER, pie, mindmap read well at note
|
|
131
|
+
width; gantt, journey and timeline are wide by nature - few items, or a content frame) -
|
|
132
|
+
raw HTML is inert. Readers comment on a note's text like on a frame element,
|
|
133
|
+
fold it to its corner, hide all with N. An edit lands live without reloading the frame.
|
|
134
|
+
Keep it an aside, a screen's worth at most: specs, flows and mood boards stay content frames.
|
|
135
|
+
Full guide: instructions/shape.md.
|
|
121
136
|
|
|
122
137
|
## Structure ladder (embedded mode: screens live in src/)
|
|
123
138
|
1. First pass: write the whole page inline in the frame file. Diverge fast.
|
|
@@ -118,6 +118,21 @@ Report where the request came from: chat requests get chat replies; only comment
|
|
|
118
118
|
- CONTENT frames (specs, mermaid diagrams, mood boards) are ordinary tsx frames built
|
|
119
119
|
from the block primitives in '@marver-design/marver/content' - import them directly
|
|
120
120
|
in the frame file and declare meta.intent. Full guide: instructions/shape.md.
|
|
121
|
+
- STICKY NOTES: the aside beside a frame. One markdown file, nothing to declare:
|
|
122
|
+
`design/scenes/<scene>/<frame>.note.md` beside the frame file (tsx, jsx or html),
|
|
123
|
+
`design/scenes/<scene>/_note.md` for the scene (it shows beside the scene's first frame),
|
|
124
|
+
`design/components/<name>.note.md` for a component. It renders as a yellow note left of the
|
|
125
|
+
frame on every canvas, dev and published. Write one when a reader needs what the screen
|
|
126
|
+
cannot say: what the frame is for, how two variations differ, how a mechanism works, an
|
|
127
|
+
open question. Markdown (headings, lists, tables, emphasis, code), `[text](goto:scene/frame)`
|
|
128
|
+
links that jump to a frame, images from design/assets/, and ```mermaid fences drawn
|
|
129
|
+
hand-sketched in the note's own yellow - plain mermaid, any family, no init/theme/style
|
|
130
|
+
lines, no URLs (flowchart, sequence, state, class, ER, pie, mindmap read well at note
|
|
131
|
+
width; gantt, journey and timeline are wide by nature - few items, or a content frame) -
|
|
132
|
+
raw HTML is inert. Readers comment on a note's text like on a frame element,
|
|
133
|
+
fold it to its corner, hide all with N. An edit lands live without reloading the frame.
|
|
134
|
+
Keep it an aside, a screen's worth at most: specs, flows and mood boards stay content frames.
|
|
135
|
+
Full guide: instructions/shape.md.
|
|
121
136
|
|
|
122
137
|
## Structure ladder
|
|
123
138
|
1. First pass: write the whole page inline in the frame file. Diverge fast.
|
|
@@ -158,3 +158,12 @@ app, and the human attributes the fault to your frame, not to a library.
|
|
|
158
158
|
WILL sweep devices with keys 1-5.
|
|
159
159
|
- Live fully inside the settled visual world. A direction executed at full commitment
|
|
160
160
|
can be judged and improved; a hedged one can only be redone.
|
|
161
|
+
|
|
162
|
+
## Glass on the canvas
|
|
163
|
+
|
|
164
|
+
While a frame rests on the canvas, marver compiles every `backdrop-filter` element into a still
|
|
165
|
+
texture of its own filtered backdrop, certified pixel by pixel, so a board of hi-fi glass pans
|
|
166
|
+
like a board of statics; the frame wakes the moment it is interacted with. Three things cannot
|
|
167
|
+
be compiled and stay live: glass inside glass, a glass element with a `mix-blend-mode`, and a
|
|
168
|
+
frame whose paint at rest is not a function of its URL (random data at boot, a clock, a
|
|
169
|
+
count-up). Use them on the one frame that needs them, not as a house style.
|
|
@@ -77,9 +77,39 @@ The host does two things, and the deploy config names both. **`design/.dist` is
|
|
|
77
77
|
gitignored - it is built ON THE HOST at deploy time, never committed.**
|
|
78
78
|
|
|
79
79
|
- **build command**: `<install> && npx marver build` (respects `publish.json`,
|
|
80
|
-
seeds comment logs into the bundle)
|
|
80
|
+
seeds comment logs into the bundle, compiles the glass textures - see below)
|
|
81
81
|
- **start command**: `npx marver serve` (reads `PORT` + the env vars above)
|
|
82
82
|
- a **persistent volume** mounted at some path, named by `MARVER_DATA_DIR`
|
|
83
|
+
- **Chrome or Chromium on the build machine**, for the glass textures. Without one
|
|
84
|
+
the build still succeeds and says `textures: none - no Chrome on this machine`;
|
|
85
|
+
the canvas ships and every feature works, but hi-fi frames with `backdrop-filter`
|
|
86
|
+
rest with their glass live and pan the way they did before 0.18.0.
|
|
87
|
+
|
|
88
|
+
## Glass textures at build (0.18.0)
|
|
89
|
+
|
|
90
|
+
A hi-fi frame at rest sleeps under certified textures of its blurred backdrops
|
|
91
|
+
(the dev canvas compiles them on the fly). A published canvas has no compiler,
|
|
92
|
+
so `marver build` compiles them once, against the exact site it just built -
|
|
93
|
+
every published node, at its size on its board, in every theme - and ships them
|
|
94
|
+
under `design/.dist/__mv/bakes/`. The build log tells you what happened:
|
|
95
|
+
|
|
96
|
+
```
|
|
97
|
+
textures: 8 frame views asleep under certified glass, 0 with live glass, 34 without effects (42 asked: every published node x 2 themes; 806 KB, 35 s)
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
- `asleep under certified glass` is the number you want to see for hi-fi boards.
|
|
101
|
+
`with live glass` counts views the compiler refused (glass inside glass, blend
|
|
102
|
+
modes, paint that is not a function of the URL): they show exactly as before.
|
|
103
|
+
- Budget: about 1-3 s per hi-fi frame and theme, under a second per frame without
|
|
104
|
+
glass; a 4-frame hi-fi board plus a 128-frame lo-fi board took 35 s.
|
|
105
|
+
- **Bundle your fonts** (`@fontsource-*`, or files under `public/`). The textures
|
|
106
|
+
are certified as the build machine renders the frame; a font that only exists on
|
|
107
|
+
the designer's laptop renders differently in the build container and the
|
|
108
|
+
visitor's browser then refuses those textures (the frame rests live, nothing
|
|
109
|
+
breaks).
|
|
110
|
+
- Skip it on purpose with `npx marver build --no-textures`, or `MARVER_NO_TEXTURES=1`
|
|
111
|
+
in a CI that has no Chrome and should stay quiet about it.
|
|
112
|
+
- `MARVER_CHROME=/path/to/chrome` names a browser marver does not find on its own.
|
|
83
113
|
|
|
84
114
|
The `@marver-design/marver` dependency must resolve from the registry (a local
|
|
85
115
|
`link:`/`file:` dep cannot ride to a remote host) - a normal registry version (`npm i -D @marver-design/marver@latest`) in
|
|
@@ -87,15 +117,36 @@ The `@marver-design/marver` dependency must resolve from the registry (a local
|
|
|
87
117
|
|
|
88
118
|
## Railway quickstart
|
|
89
119
|
|
|
90
|
-
Commit a `
|
|
120
|
+
Commit a `Dockerfile` at the upload root - Railway detects it, and it is the one
|
|
121
|
+
path that gives the build a browser for the glass textures:
|
|
122
|
+
|
|
123
|
+
```dockerfile
|
|
124
|
+
FROM node:22-slim
|
|
125
|
+
# Chromium for `marver build` (the glass textures); fonts-liberation so system-ui text
|
|
126
|
+
# has a face in the container. marver finds /usr/bin/chromium on its own and runs it
|
|
127
|
+
# with the container flags a root build needs.
|
|
128
|
+
RUN apt-get update && apt-get install -y --no-install-recommends chromium fonts-liberation \
|
|
129
|
+
&& rm -rf /var/lib/apt/lists/*
|
|
130
|
+
WORKDIR /app
|
|
131
|
+
COPY package.json ./
|
|
132
|
+
RUN npm install
|
|
133
|
+
COPY . .
|
|
134
|
+
RUN npx marver build
|
|
135
|
+
ENV PORT=8080
|
|
136
|
+
CMD ["npx", "marver", "serve"]
|
|
137
|
+
```
|
|
91
138
|
|
|
92
|
-
```json
|
|
93
|
-
{
|
|
94
|
-
"$schema": "https://railway.com/railway.schema.json",
|
|
95
|
-
"build": { "builder": "NIXPACKS", "buildCommand": "pnpm install && npx marver build" },
|
|
96
|
-
"deploy": { "startCommand": "npx marver serve" }
|
|
97
|
-
}
|
|
98
139
|
```
|
|
140
|
+
# .dockerignore
|
|
141
|
+
node_modules
|
|
142
|
+
design/.dist
|
|
143
|
+
design/.local
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
(A `railway.json` with the NIXPACKS builder - `"buildCommand": "pnpm install && npx marver build"`,
|
|
147
|
+
`"startCommand": "npx marver serve"` - works too, but a Nixpacks image has no browser: the build
|
|
148
|
+
prints `textures: none` and ships the hi-fi frames with live glass. Add chromium to it, or use the
|
|
149
|
+
Dockerfile.)
|
|
99
150
|
|
|
100
151
|
Then, once per service:
|
|
101
152
|
|
|
@@ -117,7 +168,9 @@ railway logs # with a PASSWORD gate, the owner claim link p
|
|
|
117
168
|
|
|
118
169
|
Republishing is just `railway up` again: the server unions the seeded logs on
|
|
119
170
|
boot, so collected feedback is NEVER clobbered by a new build. Run ONE instance -
|
|
120
|
-
the event log is single-writer by design.
|
|
171
|
+
the event log is single-writer by design. A republish mints new glass textures;
|
|
172
|
+
a tab that was already open keeps asking for the old ones and rests its glass
|
|
173
|
+
live until it reloads - by design, so an old shell never dresses new frames.
|
|
121
174
|
|
|
122
175
|
## What the deployed gate offers (so you know what you're wiring)
|
|
123
176
|
|
|
@@ -187,6 +187,68 @@ of described imagery every time (the full asset rules: instructions/craft.md,
|
|
|
187
187
|
has the shell-less way) and adjust the per-row count until it reads well. "The code
|
|
188
188
|
says they're the same width" proves nothing.
|
|
189
189
|
|
|
190
|
+
## Sticky notes - the aside beside a frame
|
|
191
|
+
|
|
192
|
+
A sticky note is one markdown file beside the thing it explains. It renders as a yellow note
|
|
193
|
+
left of the frame on the canvas, in dev and in every published or shared canvas. Nothing to
|
|
194
|
+
declare, no board node, no imports, no iframe:
|
|
195
|
+
|
|
196
|
+
| For | Write | Shows |
|
|
197
|
+
|---|---|---|
|
|
198
|
+
| a frame `checkout/cart` (`cart.tsx`, `cart.jsx` or `cart.html`) | `design/scenes/checkout/cart.note.md` | left of that frame, 260 wide |
|
|
199
|
+
| a scene `checkout` | `design/scenes/checkout/_note.md` | left of the scene's first frame on the board, 380 wide |
|
|
200
|
+
| a component frame | `design/components/<name>.note.md` | as a frame note |
|
|
201
|
+
|
|
202
|
+
Write one when a reader needs something the screen cannot say: what the frame is for, what
|
|
203
|
+
differs between two variations, how a mechanism works, an open question. Markdown, the Md
|
|
204
|
+
block's rules: `[text](goto:scene/frame)` links jump to a frame on the canvas, `http(s)`
|
|
205
|
+
links open a new tab, images are `design/assets/` paths, raw HTML is inert, and a
|
|
206
|
+
` ```mermaid ` fence renders hand-drawn, in the note's own yellow. Readers can comment on any
|
|
207
|
+
element of a note exactly as on a frame element.
|
|
208
|
+
|
|
209
|
+
````md
|
|
210
|
+
## Why the jobs list leads
|
|
211
|
+
|
|
212
|
+
Drivers ask "where am I going first" - the list beats the map here.
|
|
213
|
+
Compare with the [empty day](goto:app-home/today-empty): same header, one call to action.
|
|
214
|
+
|
|
215
|
+
| Reason | Effect |
|
|
216
|
+
|---|---|
|
|
217
|
+
| Too far | lane weight down |
|
|
218
|
+
|
|
219
|
+
```mermaid
|
|
220
|
+
flowchart LR
|
|
221
|
+
Login --> Today --> Jobs
|
|
222
|
+
```
|
|
223
|
+
````
|
|
224
|
+
|
|
225
|
+
### Diagrams in a note
|
|
226
|
+
|
|
227
|
+
A ```mermaid fence in a note renders hand-sketched on the yellow paper: rough boxes, hatched
|
|
228
|
+
fills, handwriting labels, one ink. You write plain mermaid and nothing else - no `%%{init}%%`,
|
|
229
|
+
no theme, no colours, no `style` lines (the note has one look and applies it to every family),
|
|
230
|
+
no URLs or images in the source (refused). Every family works: flowchart, sequence, state,
|
|
231
|
+
class, ER, pie, mindmap, timeline, gantt, journey, quadrant, git graph, block.
|
|
232
|
+
|
|
233
|
+
Fit the note: flowchart (`TD` for a tall note, `LR` for three or four steps), sequence, state,
|
|
234
|
+
class, ER, pie and mindmap read well at 260 or 380 wide. Gantt, journey, timeline, quadrant and
|
|
235
|
+
git graphs are drawn at the note's width too but are wide by nature - keep them to a handful of
|
|
236
|
+
items, or give them a content frame. Five to eight nodes is the sweet spot; short labels
|
|
237
|
+
(two or three words); one diagram per note, above or below the prose it explains.
|
|
238
|
+
|
|
239
|
+
```mermaid
|
|
240
|
+
sequenceDiagram
|
|
241
|
+
participant D as Driver
|
|
242
|
+
participant G as Gate
|
|
243
|
+
D->>G: scan gate code
|
|
244
|
+
G-->>D: bay + lot map
|
|
245
|
+
```
|
|
246
|
+
|
|
247
|
+
Keep it an aside: a screen's worth of reading at most. The spec, the flow, the mood board stay
|
|
248
|
+
content frames - a note explains, it does not document. Every viewer can fold a note to its
|
|
249
|
+
corner tab (the choice is theirs, never saved to the board) and hide all of them with `N`.
|
|
250
|
+
Edits to a note file land on the canvas as you save, without reloading the frame.
|
|
251
|
+
|
|
190
252
|
## When Shape ends
|
|
191
253
|
|
|
192
254
|
The board holds the agreed flow, spec, and direction. Wireframe picks up from
|