@marver-design/marver 0.3.1 → 0.5.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 +151 -0
- package/README.md +2 -0
- package/dist/auth-B36fMCM3.mjs +245 -0
- package/dist/{build-Ckmyci3O.mjs → build-BrCl9hJS.mjs} +56 -14
- package/dist/cli.mjs +15 -6
- package/dist/collab-CXy8gqoz.mjs +297 -0
- package/dist/comments-Ba8mU600.mjs +90 -0
- package/dist/comments-odHzYdO3.mjs +179 -0
- package/dist/dev-DdeU-Jst.mjs +162 -0
- package/dist/{init-DolP_Ld4.mjs → init-DsCUmlCW.mjs} +2 -1
- package/dist/{manifest-DW-T52MM.mjs → manifest-C8FODq2S.mjs} +26 -1
- package/dist/{plugin-Crp4CAma.mjs → plugin-BtSGAm2h.mjs} +169 -19
- package/dist/rolldown-runtime-D7D4PA-g.mjs +13 -0
- package/dist/serve-D_KBK7Oy.mjs +431 -0
- package/dist/sync-CkBk-tUk.mjs +248 -0
- package/package.json +3 -1
- package/src/client/content/diagram.tsx +46 -2
- package/src/client/content/index.tsx +6 -2
- package/src/client/content/md.ts +29 -0
- package/src/client/content/palette.ts +6 -0
- package/src/client/frame-host/bridge.js +269 -3
- package/src/client/frame-host/serialize.ts +195 -0
- package/src/client/shell/App.tsx +131 -17
- package/src/client/shell/Comments.tsx +464 -0
- package/src/client/shell/Play.tsx +24 -0
- package/src/client/shell/canvas/Canvas.tsx +66 -29
- package/src/client/shell/canvas/FrameNode.tsx +130 -10
- package/src/client/shell/canvas/frame-registry.ts +18 -0
- package/src/client/shell/canvas/snapshots.ts +233 -0
- package/src/client/shell/comments-store.ts +180 -0
- package/src/client/shell/cursor-arrow-dark.svg +1 -0
- package/src/client/shell/cursor-arrow.svg +1 -0
- package/src/client/shell/hash.ts +14 -3
- package/src/client/shell/icons.tsx +6 -0
- package/src/client/shell/labels.ts +10 -0
- package/src/client/shell/perf.ts +92 -0
- package/src/client/shell/store.ts +101 -6
- package/src/client/shell/styles.css +196 -7
- package/src/client/stage/main.tsx +2 -0
- package/src/shared/events.ts +103 -0
- package/templates/AGENTS-embedded.md +13 -1
- package/templates/AGENTS-studio.md +13 -1
- package/templates/instructions/boards.md +6 -3
- package/templates/instructions/configure.md +28 -0
- package/templates/instructions/iterate.md +38 -0
- package/templates/instructions/publish.md +129 -0
- package/templates/instructions/reference/color.md +22 -1
- package/templates/instructions/shape.md +23 -16
- package/dist/dev-C2oTKuXe.mjs +0 -97
- package/dist/serve-OtA9Nlow.mjs +0 -207
|
@@ -28,6 +28,13 @@
|
|
|
28
28
|
--interact: #db35f2; --interact-ring: rgba(219, 53, 242, .16);
|
|
29
29
|
--interact-strong: rgba(219, 53, 242, 1); --interact-soft: rgba(234, 141, 255, .95);
|
|
30
30
|
--interact-deep: rgba(176, 47, 194, .85); --interact-spark: rgba(255, 255, 255, .95);
|
|
31
|
+
/* comments own a third mode color (Apple systemGreen): selection = blue,
|
|
32
|
+
interact = purple, comments = green - same geometry, unique hue per mode */
|
|
33
|
+
--comment: #34c759; --comment-ring: rgba(52, 199, 89, .22);
|
|
34
|
+
/* comment-card field surface: APP-scoped on purpose - the card keys to the board
|
|
35
|
+
theme, and node-scoped --node-bg would flip with the frame underneath it */
|
|
36
|
+
--cm-field: #fff;
|
|
37
|
+
--cm-modal-bg: rgba(255, 255, 255, .95);
|
|
31
38
|
|
|
32
39
|
--r-node: 10px; --r-panel: 24px; --edge: 10px;
|
|
33
40
|
--blur: blur(20px) saturate(150%);
|
|
@@ -60,6 +67,9 @@
|
|
|
60
67
|
--interact: #ea8dff; --interact-ring: rgba(219, 53, 242, .3);
|
|
61
68
|
--interact-strong: rgba(219, 53, 242, 1); --interact-soft: rgba(234, 141, 255, .95);
|
|
62
69
|
--interact-deep: rgba(203, 48, 224, .8); --interact-spark: rgba(255, 255, 255, .9);
|
|
70
|
+
--comment: #30d158; --comment-ring: rgba(48, 209, 88, .34);
|
|
71
|
+
--cm-field: #0f1015;
|
|
72
|
+
--cm-modal-bg: rgba(22, 22, 27, .96);
|
|
63
73
|
|
|
64
74
|
--shadow-node: 0 1px 2px rgba(0, 0, 0, .35); /* shadow-sm, alpha raised to read on dark */
|
|
65
75
|
}
|
|
@@ -112,6 +122,13 @@ button { font-family: inherit }
|
|
|
112
122
|
button svg { pointer-events: none } /* event targets stay on the button - pan exclusion depends on it */
|
|
113
123
|
@media (prefers-reduced-motion: reduce) { *, *::before, *::after { transition: none !important } }
|
|
114
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 }
|
|
115
132
|
.sh-app { position: relative; height: 100%; color: var(--glass-ink); outline: none;
|
|
116
133
|
transition: --accent .4s ease, --accent-ring .4s ease, --beam .4s ease,
|
|
117
134
|
--edge-accent .4s ease, --inner-tint .4s ease, --accent-wash .4s ease,
|
|
@@ -131,15 +148,20 @@ button svg { pointer-events: none } /* event targets stay on the button - pan
|
|
|
131
148
|
opacity: var(--grid-alpha, 1) }
|
|
132
149
|
.sh-content { will-change: auto }
|
|
133
150
|
#sh-world { position: relative; width: 1px; height: 1px }
|
|
134
|
-
#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) */
|
|
135
152
|
.sh-gesturing .sh-content { will-change: transform } /* law G-3: gesture-scoped only */
|
|
136
153
|
/* preset transitions: armed by animateLayout() around device/tidy mutations - nodes ease
|
|
137
154
|
to their new frame on the same 320ms ease-out the camera fit animates with */
|
|
138
155
|
#sh-world.sh-preset .sh-node { transition: transform .32s cubic-bezier(.25,.46,.45,.94),
|
|
139
156
|
width .32s cubic-bezier(.25,.46,.45,.94), height .32s cubic-bezier(.25,.46,.45,.94) }
|
|
140
157
|
#sh-world.sh-preset .sh-node-body,
|
|
141
|
-
#sh-world.sh-preset .sh-node
|
|
158
|
+
#sh-world.sh-preset .sh-node .sh-live { transition: width .32s cubic-bezier(.25,.46,.45,.94),
|
|
142
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 }
|
|
143
165
|
|
|
144
166
|
/* cursor conventions (Figma): arrow everywhere; grab only while space is held */
|
|
145
167
|
body.sh-space .sh-canvas { cursor: grab }
|
|
@@ -173,7 +195,7 @@ body.sh-space .sh-node { pointer-events: none } /* space-drag
|
|
|
173
195
|
box-shadow: 0 0 0 calc(4px * var(--sh-inv, 1)) var(--accent-ring), var(--shadow-node) }
|
|
174
196
|
.sh-node-head { height: 28px; display: flex; align-items: center; gap: 8px; padding: 0 11px;
|
|
175
197
|
font-size: 11px; color: var(--head-dim);
|
|
176
|
-
border-bottom: 1px solid var(--node-brd);
|
|
198
|
+
border-bottom: 1px solid var(--node-brd); user-select: none;
|
|
177
199
|
border-radius: var(--r-node) var(--r-node) 0 0;
|
|
178
200
|
background-color: var(--head-bg); background-image: var(--head-sheen);
|
|
179
201
|
backdrop-filter: var(--blur); -webkit-backdrop-filter: var(--blur) }
|
|
@@ -187,7 +209,20 @@ body.sh-space .sh-node { pointer-events: none } /* space-drag
|
|
|
187
209
|
.sh-node-head .dim { margin-left: auto; color: var(--head-dim); flex: none; font-variant-numeric: tabular-nums }
|
|
188
210
|
.sh-node-body { position: relative; background: var(--node-bg); border-radius: 0 0 var(--r-node) var(--r-node); overflow: hidden }
|
|
189
211
|
.sh-node iframe { border: 0; display: block }
|
|
190
|
-
|
|
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 */
|
|
191
226
|
.sh-node.interact { border-color: var(--interact);
|
|
192
227
|
outline: calc(2px * var(--sh-inv, 1)) solid var(--interact);
|
|
193
228
|
outline-offset: calc(-1px * var(--sh-inv, 1));
|
|
@@ -309,8 +344,11 @@ body.sh-space .sh-node { pointer-events: none } /* space-drag
|
|
|
309
344
|
transform-origin: 22px 22px; transition: transform var(--morph), opacity .2s ease }
|
|
310
345
|
.sh-panel.closed { transform: scale(.16); opacity: 0; pointer-events: none }
|
|
311
346
|
.sh-panel-top { display: flex; align-items: center; gap: 8px; padding: 2px 2px 8px 8px }
|
|
312
|
-
.sh-panel-top .mark { flex: none;
|
|
313
|
-
.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 }
|
|
314
352
|
.sh-panel-scroll { overflow-y: auto; min-height: 0; padding-bottom: 2px; scrollbar-width: thin;
|
|
315
353
|
scrollbar-color: var(--glass-ink-3) transparent }
|
|
316
354
|
/* Sidebar list system (macOS-sidebar conventions): every row is 28px tall with an
|
|
@@ -373,6 +411,8 @@ body.sh-space .sh-node { pointer-events: none } /* space-drag
|
|
|
373
411
|
.sh-pill-btn.pct { font-variant-numeric: tabular-nums; min-width: 46px }
|
|
374
412
|
.sh-pill-btn.off { color: var(--glass-ink-3); cursor: default }
|
|
375
413
|
.sh-pill-btn.off:hover { background: none; color: var(--glass-ink-3) }
|
|
414
|
+
.sh-pill-btn.on { color: var(--accent); background: color-mix(in srgb, var(--accent) 12%, transparent) }
|
|
415
|
+
.sh-pill-btn.on:hover { color: var(--accent); background: color-mix(in srgb, var(--accent) 18%, transparent) }
|
|
376
416
|
.sh-pill .sep { width: 1px; height: 16px; background: var(--glass-brd); margin: 0 3px }
|
|
377
417
|
|
|
378
418
|
/* dropdown menu (theme) - portaled to .sh-app (its own backdrop root), left-aligned to its trigger */
|
|
@@ -437,7 +477,7 @@ body.sh-space .sh-node { pointer-events: none } /* space-drag
|
|
|
437
477
|
/* ---- Play mode (SPEC-M2 §1): near-black stage, one centered device, auto-hiding bar.
|
|
438
478
|
Fixed dark palette - the backdrop never follows the board theme, so no tokens here. */
|
|
439
479
|
.sh-play { position: absolute; inset: 0; z-index: 30; background: #0a0a0b; display: flex;
|
|
440
|
-
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 }
|
|
441
481
|
@keyframes sh-play-in { from { opacity: 0 } }
|
|
442
482
|
/* the device wears the frame-node card language: same theme border tokens, the same
|
|
443
483
|
masked 1px edge-light gradient ring (glass catching light), and a real drop shadow
|
|
@@ -486,6 +526,8 @@ body.sh-space .sh-node { pointer-events: none } /* space-drag
|
|
|
486
526
|
/* board switcher */
|
|
487
527
|
.sh-play-bar .bd-wrap { position: relative }
|
|
488
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 }
|
|
489
531
|
/* no backdrop-filter here: the menu nests inside the blurred bar, and glass never nests
|
|
490
532
|
(a nested backdrop-filter samples the bar, not the page) - near-opaque ink instead */
|
|
491
533
|
.sh-play-menu { position: absolute; top: calc(100% + 10px); left: 0; min-width: 150px; padding: 4px;
|
|
@@ -633,3 +675,150 @@ body.sh-kbd .sh-app :focus-visible { outline: 2px solid var(--accent); outline-o
|
|
|
633
675
|
.sub.vgroup { cursor: pointer }
|
|
634
676
|
.sub.vgroup.held { background: color-mix(in srgb, var(--glass-hover) 55%, transparent) }
|
|
635
677
|
.sub.vgroup.held .gicon { color: var(--glass-ink-2) }
|
|
678
|
+
|
|
679
|
+
/* ---- comments (SPEC-M3 §6) ----------------------------------------------------------
|
|
680
|
+
The marker family (pins, stacks) is SOLID with the Figma teardrop silhouette - a
|
|
681
|
+
marker must hold contrast on any artwork. The surfaces (card, composer, dialog)
|
|
682
|
+
are OUR glass - the shell's own token language, theme-aware where Figma is flat. */
|
|
683
|
+
.cm-avatar { display: inline-flex; align-items: center; justify-content: center; border-radius: 50%;
|
|
684
|
+
color: #fff; font-weight: 700; font-family: -apple-system, system-ui, sans-serif;
|
|
685
|
+
object-fit: cover; flex: none; user-select: none }
|
|
686
|
+
|
|
687
|
+
/* markers - pins and stacks share one face: overlapping avatars + a total count.
|
|
688
|
+
A pin shows one thread's participants (tail points at its div); a stack shows
|
|
689
|
+
everyone across the frame's threads (tail points up-right at the frame corner). */
|
|
690
|
+
.cm-pin, .cm-stack { display: inline-flex; align-items: center; padding: 4px;
|
|
691
|
+
background: #fff; border: 1px solid rgba(24,24,27,.1); cursor: pointer;
|
|
692
|
+
font: 700 11px -apple-system, system-ui, sans-serif; color: #18181b }
|
|
693
|
+
.cm-pin .cm-avatar, .cm-stack .cm-avatar { border: 2px solid #fff }
|
|
694
|
+
.cm-pin .cm-avatar + .cm-avatar, .cm-stack .cm-avatar + .cm-avatar { margin-left: -8px }
|
|
695
|
+
.cm-pin b, .cm-stack b { padding: 0 8px 0 6px }
|
|
696
|
+
|
|
697
|
+
/* pin: the teardrop. Bottom-left tail points at the anchored element. */
|
|
698
|
+
.cm-pin { position: absolute; z-index: 30; transform: translate(-2px, -100%) scale(var(--sh-inv, 1));
|
|
699
|
+
transform-origin: bottom left; border-radius: 18px 18px 18px 4px;
|
|
700
|
+
box-shadow: 0 1px 2px rgba(24,24,27,.18), 0 6px 16px -6px rgba(24,24,27,.35);
|
|
701
|
+
transition: box-shadow .15s, transform .15s }
|
|
702
|
+
.cm-pin:hover { box-shadow: 0 1px 3px rgba(24,24,27,.22), 0 10px 24px -6px rgba(24,24,27,.45) }
|
|
703
|
+
/* selected pin wears the selected frame's exact geometry - border, 2px
|
|
704
|
+
outline hugging the edge, 4px ring halo - in the comment green (the pin
|
|
705
|
+
is already screen-space via scale(--sh-inv), so plain px match the
|
|
706
|
+
frame's calc'd screen px) */
|
|
707
|
+
.cm-pin.on { border-color: var(--comment);
|
|
708
|
+
outline: 2px solid var(--comment); outline-offset: -1px;
|
|
709
|
+
box-shadow: 0 0 0 4px var(--comment-ring), 0 1px 2px rgba(24,24,27,.18), 0 6px 16px -6px rgba(24,24,27,.35) }
|
|
710
|
+
.cm-pin.orphan { border-style: dashed; opacity: .75 }
|
|
711
|
+
|
|
712
|
+
/* stack: the frame-corner cluster - everyone across the frame's threads */
|
|
713
|
+
.cm-stack { position: absolute; top: 10px; right: 10px; z-index: 30;
|
|
714
|
+
transform: scale(var(--sh-inv, 1)); transform-origin: top right; border-radius: 18px 18px 4px 18px;
|
|
715
|
+
box-shadow: 0 1px 2px rgba(24,24,27,.15), 0 6px 16px -6px rgba(24,24,27,.3);
|
|
716
|
+
transition: box-shadow .15s }
|
|
717
|
+
.cm-stack:hover { box-shadow: 0 1px 3px rgba(24,24,27,.2), 0 10px 24px -6px rgba(24,24,27,.4) }
|
|
718
|
+
|
|
719
|
+
/* card: OUR glass - the floating-menu language, theme-aware */
|
|
720
|
+
.cm-card { position: absolute; z-index: 40; width: 300px; padding: 16px;
|
|
721
|
+
transform: scale(var(--sh-inv, 1)); transform-origin: top left;
|
|
722
|
+
background: var(--glass); backdrop-filter: var(--blur); -webkit-backdrop-filter: var(--blur);
|
|
723
|
+
/* an open card is the active selection - the frame's exact geometry, comment green */
|
|
724
|
+
color: var(--glass-ink); border-radius: 20px; border: 1px solid var(--comment);
|
|
725
|
+
outline: 2px solid var(--comment); outline-offset: -1px;
|
|
726
|
+
box-shadow: 0 0 0 4px var(--comment-ring), var(--shadow-glass), 0 16px 40px -12px rgba(24,24,27,.35);
|
|
727
|
+
display: flex; flex-direction: column; gap: 12px;
|
|
728
|
+
font: 400 13px -apple-system, system-ui, sans-serif; letter-spacing: -.005em; text-align: left }
|
|
729
|
+
.cm-card.flip { transform: translateX(-100%) scale(var(--sh-inv, 1)); transform-origin: top right }
|
|
730
|
+
.cm-card header { display: flex; align-items: center; gap: 9px }
|
|
731
|
+
.cm-card header b { font-size: 14px; font-weight: 600; letter-spacing: -.01em }
|
|
732
|
+
.cm-card .dim { color: var(--glass-ink-3); font-size: 11.5px; font-variant-numeric: tabular-nums }
|
|
733
|
+
.cm-actions { position: absolute; top: 13px; right: 12px; display: flex; gap: 2px }
|
|
734
|
+
/* the root header shares its line with the pinned actions - reserve their width
|
|
735
|
+
so a long display name ellipsizes instead of running under the icons */
|
|
736
|
+
.cm-card > header { padding-right: 84px }
|
|
737
|
+
.cm-card > header b { overflow: hidden; text-overflow: ellipsis; white-space: nowrap }
|
|
738
|
+
/* every message - root and replies - is the same unit: avatar + name + time,
|
|
739
|
+
body indented to the name column (24px avatar + 9px gap) */
|
|
740
|
+
.cm-card .cm-body { margin: 0; padding-left: 33px; font-size: 13px; line-height: 1.55;
|
|
741
|
+
color: var(--glass-ink); white-space: pre-wrap; overflow-wrap: break-word }
|
|
742
|
+
.cm-card > .cm-body { margin-top: -10px } /* pulls against the card's 12px gap -> 2px under the header */
|
|
743
|
+
.cm-msg { display: flex; flex-direction: column; gap: 2px }
|
|
744
|
+
.cm-icon { display: inline-flex; align-items: center; justify-content: center; width: 26px; height: 26px;
|
|
745
|
+
border: 0; border-radius: 8px; background: none; color: var(--glass-ink-3); cursor: pointer;
|
|
746
|
+
transition: background .12s, color .12s }
|
|
747
|
+
.cm-icon:hover { background: var(--glass-hover); color: var(--glass-ink) }
|
|
748
|
+
/* copy-link morphs into a check on the shell's morph curve, then reverts */
|
|
749
|
+
.cm-copy { position: relative }
|
|
750
|
+
.cm-copy .a, .cm-copy .b { position: absolute; inset: 0; display: flex; align-items: center; justify-content: center;
|
|
751
|
+
transition: transform var(--morph), opacity .2s ease }
|
|
752
|
+
.cm-copy .b { transform: scale(.4); opacity: 0; color: var(--comment) }
|
|
753
|
+
.cm-copy.ok .a { transform: scale(.4); opacity: 0 }
|
|
754
|
+
.cm-copy.ok .b { transform: scale(1); opacity: 1 }
|
|
755
|
+
/* composer row: my avatar (list size) + a pill input with a round send button inside */
|
|
756
|
+
.cm-compose { display: flex; align-items: center; gap: 8px }
|
|
757
|
+
.cm-inputwrap { position: relative; flex: 1; min-width: 0 }
|
|
758
|
+
.cm-compose input { width: 100%; height: 36px; padding: 0 42px 0 15px; border-radius: 999px;
|
|
759
|
+
border: 1px solid var(--glass-brd); background: var(--cm-field); color: var(--glass-ink);
|
|
760
|
+
font: 400 13px -apple-system, system-ui, sans-serif; outline: none;
|
|
761
|
+
transition: border-color .15s, box-shadow .15s }
|
|
762
|
+
.cm-compose input::placeholder { color: var(--glass-ink-3) }
|
|
763
|
+
.cm-compose input:focus { border-color: var(--comment); box-shadow: 0 0 0 3px var(--comment-ring) }
|
|
764
|
+
.cm-send { position: absolute; right: 4px; top: 50%; transform: translateY(-50%);
|
|
765
|
+
width: 28px; height: 28px; border-radius: 50%; border: 0; display: flex; align-items: center; justify-content: center;
|
|
766
|
+
background: var(--comment); color: #fff; cursor: pointer; transition: background .15s, opacity .15s }
|
|
767
|
+
.cm-send:hover:not(:disabled) { filter: brightness(1.08) }
|
|
768
|
+
.cm-send:disabled { background: var(--glass-ink-3); opacity: .5; cursor: default }
|
|
769
|
+
/* signed-out viewer: one clear button in place of the composer */
|
|
770
|
+
.cm-signin-cta { width: 100%; height: 40px; border: 0; border-radius: 999px;
|
|
771
|
+
background: var(--comment); color: #fff; cursor: pointer;
|
|
772
|
+
font: 600 13px -apple-system, system-ui, sans-serif; transition: filter .15s }
|
|
773
|
+
.cm-signin-cta:hover { filter: brightness(1.06) }
|
|
774
|
+
/* the draft composer is JUST the pill - no card chrome around a single input */
|
|
775
|
+
.cm-draft { padding: 0; width: 272px; background: none; border: none; box-shadow: none; outline: none;
|
|
776
|
+
backdrop-filter: none; -webkit-backdrop-filter: none }
|
|
777
|
+
.cm-draft input { height: 40px; box-shadow: 0 2px 6px rgba(24,24,27,.15), 0 12px 32px -8px rgba(24,24,27,.35) }
|
|
778
|
+
.cm-draft .cm-send { right: 6px } /* nudge in for the taller draft pill */
|
|
779
|
+
/* identity modal - same glass family */
|
|
780
|
+
.cm-modal-wrap { position: fixed; inset: 0; z-index: 300; display: flex; align-items: center; justify-content: center;
|
|
781
|
+
background: rgba(24,24,27,.45); backdrop-filter: blur(5px) }
|
|
782
|
+
/* the auth modal is a focused surface, not a floating glass panel - near-solid so
|
|
783
|
+
the canvas behind never bleeds through the fields */
|
|
784
|
+
.cm-modal { width: 344px; padding: 26px; border-radius: 24px;
|
|
785
|
+
background: var(--cm-modal-bg); backdrop-filter: var(--blur); -webkit-backdrop-filter: var(--blur);
|
|
786
|
+
color: var(--glass-ink); border: 1px solid var(--glass-brd);
|
|
787
|
+
display: flex; flex-direction: column; gap: 8px; font: 400 13px -apple-system, system-ui, sans-serif;
|
|
788
|
+
box-shadow: 0 1px 2px rgba(24,24,27,.1), 0 20px 60px -12px rgba(24,24,27,.5) }
|
|
789
|
+
.cm-modal h2 { margin: 0; font-size: 16px; letter-spacing: -.01em }
|
|
790
|
+
.cm-modal .dim { margin: 0 0 8px; color: var(--glass-ink-2); font-size: 12.5px; line-height: 1.5 }
|
|
791
|
+
.cm-fields { display: flex; flex-direction: column; gap: 9px }
|
|
792
|
+
.cm-modal input { height: 38px; padding: 0 14px; border-radius: 999px; border: 1px solid var(--glass-brd);
|
|
793
|
+
background: var(--cm-field); color: var(--glass-ink); font: inherit; outline: none }
|
|
794
|
+
.cm-modal input:focus { border-color: var(--comment); box-shadow: 0 0 0 3px var(--comment-ring) }
|
|
795
|
+
.cm-err { color: #b42318; font-size: 12px }
|
|
796
|
+
.cm-chip { height: 38px; box-sizing: border-box; padding: 0 14px; border-radius: 999px;
|
|
797
|
+
border: 1px solid transparent; background: var(--glass-hover);
|
|
798
|
+
display: flex; align-items: center; justify-content: space-between; gap: 10px; overflow: hidden }
|
|
799
|
+
.cm-div { border: 0; height: 1px; background: var(--glass-brd); margin: 1px 2px; align-self: stretch }
|
|
800
|
+
.cm-chip b { font-weight: 500; font-size: 13px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap }
|
|
801
|
+
.cm-chip span { font-size: 10.5px; font-weight: 600; letter-spacing: .04em; color: var(--glass-ink-3); flex: none }
|
|
802
|
+
.cm-idrow { display: flex; align-items: center; gap: 10px }
|
|
803
|
+
.cm-pfp { width: 44px; height: 44px; border-radius: 50%; border: 1.5px dashed var(--glass-ink-3);
|
|
804
|
+
background: var(--cm-field) center/cover no-repeat; color: var(--glass-ink-3); font-size: 18px; flex: none;
|
|
805
|
+
display: flex; align-items: center; justify-content: center; cursor: pointer; padding: 0;
|
|
806
|
+
transition: border-color .15s, color .15s }
|
|
807
|
+
.cm-pfp:hover { border-color: var(--comment); color: var(--comment) }
|
|
808
|
+
.cm-pfp.set { border-style: solid; border-color: var(--glass-brd); color: transparent }
|
|
809
|
+
.cm-row { display: flex; gap: 8px; margin-top: 12px }
|
|
810
|
+
.cm-row button:disabled, .cm-row button.cm-primary:disabled {
|
|
811
|
+
background: var(--glass-ink-3); color: var(--glass); cursor: default; opacity: 1 }
|
|
812
|
+
.cm-switch { margin: 2px auto 0; border: 0; background: none; padding: 4px 8px; cursor: pointer;
|
|
813
|
+
font: 500 12px -apple-system, system-ui, sans-serif; color: var(--glass-ink-3); border-radius: 999px }
|
|
814
|
+
.cm-switch:hover { color: var(--glass-ink); background: var(--glass-hover) }
|
|
815
|
+
.cm-row button { height: 36px; padding: 0 16px; border-radius: 999px; border: 1px solid var(--glass-brd);
|
|
816
|
+
background: none; color: var(--glass-ink); font: 600 12.5px -apple-system, system-ui, sans-serif; cursor: pointer }
|
|
817
|
+
.cm-row button:hover { background: var(--glass-hover) }
|
|
818
|
+
.cm-row button.cm-primary { background: var(--glass-ink); color: var(--glass); border-color: transparent; flex: 1 }
|
|
819
|
+
.cm-row button.cm-primary:hover { opacity: .88 }
|
|
820
|
+
|
|
821
|
+
/* the unclipped comment plane over each frame: itself transparent to the pointer,
|
|
822
|
+
its children (pins, cards, stacks) fully interactive */
|
|
823
|
+
.cm-layer { position: absolute; left: 0; width: 100%; pointer-events: none; z-index: 25 }
|
|
824
|
+
.cm-layer > * { pointer-events: auto }
|
|
@@ -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
|
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Comment events - the PURE half of the store (SPEC-M3 §1), shared by the node side
|
|
3
|
+
* (JSONL persistence in server/comments.ts) and the browser shell (thread state from
|
|
4
|
+
* fetched events). No node imports here, ever.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
export type EventType = 'create' | 'reply' | 'edit' | 'resolve' | 'reopen' | 'react' | 'profile'
|
|
8
|
+
|
|
9
|
+
export interface CommentEvent {
|
|
10
|
+
id: string // client-generated UUID - the idempotency key
|
|
11
|
+
ts: number // ms epoch at creation
|
|
12
|
+
type: EventType
|
|
13
|
+
commentId?: string // the comment this event belongs to (root id = thread id)
|
|
14
|
+
parentId?: string // replies: the ROOT comment id (threads are flat)
|
|
15
|
+
board?: string
|
|
16
|
+
nodeKey?: string // a frame can sit on a board twice - comments are node-scoped
|
|
17
|
+
frame?: string
|
|
18
|
+
anchor?: unknown // SPEC-M3 §5 bundle; absent = frame-level comment
|
|
19
|
+
author?: { email: string; name?: string; avatar?: string }
|
|
20
|
+
body?: string // plain text in v1
|
|
21
|
+
emoji?: string // react events
|
|
22
|
+
addressedIn?: string // resolve events: the variant frame that answered
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export interface Thread {
|
|
26
|
+
id: string
|
|
27
|
+
board?: string; nodeKey?: string; frame?: string
|
|
28
|
+
anchor?: unknown
|
|
29
|
+
author?: CommentEvent['author']
|
|
30
|
+
body?: string
|
|
31
|
+
ts: number
|
|
32
|
+
resolved: boolean
|
|
33
|
+
addressedIn?: string
|
|
34
|
+
replies: { id: string; author?: CommentEvent['author']; body?: string; ts: number }[]
|
|
35
|
+
reactions: Record<string, string[]> // emoji -> author emails (toggle semantics)
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/** Events the other side lacks, by id - the sync payload in either direction. */
|
|
39
|
+
export function diffEvents(mine: CommentEvent[], theirIds: Iterable<string>): CommentEvent[] {
|
|
40
|
+
const have = new Set(theirIds)
|
|
41
|
+
return mine.filter((e) => !have.has(e.id))
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/** Replay a board's events into current thread state. Deterministic: replay order is
|
|
45
|
+
* (ts, id) so two stores holding the same event SET always derive the same state.
|
|
46
|
+
* TWO passes - creates first, then everything else - so a reply whose author's clock
|
|
47
|
+
* ran ahead of the creator's still lands instead of being dropped forever. */
|
|
48
|
+
export function replay(events: CommentEvent[]): Thread[] {
|
|
49
|
+
const ordered = [...events].sort((a, b) => a.ts - b.ts || (a.id < b.id ? -1 : 1))
|
|
50
|
+
const threads = new Map<string, Thread>()
|
|
51
|
+
for (const ev of ordered) {
|
|
52
|
+
if (ev.type !== 'create') continue
|
|
53
|
+
if (!ev.commentId || threads.has(ev.commentId)) continue
|
|
54
|
+
threads.set(ev.commentId, {
|
|
55
|
+
id: ev.commentId, board: ev.board, nodeKey: ev.nodeKey, frame: ev.frame,
|
|
56
|
+
anchor: ev.anchor, author: ev.author, body: ev.body, ts: ev.ts,
|
|
57
|
+
resolved: false, replies: [], reactions: {},
|
|
58
|
+
})
|
|
59
|
+
}
|
|
60
|
+
for (const ev of ordered) {
|
|
61
|
+
switch (ev.type) {
|
|
62
|
+
case 'reply': {
|
|
63
|
+
const t = ev.parentId ? threads.get(ev.parentId) : undefined
|
|
64
|
+
if (!t || !ev.commentId || t.replies.some((r) => r.id === ev.commentId)) break
|
|
65
|
+
t.replies.push({ id: ev.commentId, author: ev.author, body: ev.body, ts: ev.ts })
|
|
66
|
+
break
|
|
67
|
+
}
|
|
68
|
+
case 'edit': {
|
|
69
|
+
if (!ev.commentId || ev.body === undefined) break
|
|
70
|
+
const t = threads.get(ev.commentId)
|
|
71
|
+
if (t) { t.body = ev.body; break }
|
|
72
|
+
for (const th of threads.values()) {
|
|
73
|
+
const r = th.replies.find((r) => r.id === ev.commentId)
|
|
74
|
+
if (r) { r.body = ev.body; break }
|
|
75
|
+
}
|
|
76
|
+
break
|
|
77
|
+
}
|
|
78
|
+
case 'resolve': {
|
|
79
|
+
const t = ev.commentId ? threads.get(ev.commentId) : undefined
|
|
80
|
+
if (t) { t.resolved = true; if (ev.addressedIn) t.addressedIn = ev.addressedIn }
|
|
81
|
+
break
|
|
82
|
+
}
|
|
83
|
+
case 'reopen': {
|
|
84
|
+
const t = ev.commentId ? threads.get(ev.commentId) : undefined
|
|
85
|
+
if (t) { t.resolved = false; t.addressedIn = undefined }
|
|
86
|
+
break
|
|
87
|
+
}
|
|
88
|
+
case 'react': {
|
|
89
|
+
// toggle keyed on comment+author+emoji: present removes, absent adds
|
|
90
|
+
const t = ev.commentId ? threads.get(ev.commentId) : undefined
|
|
91
|
+
const who = ev.author?.email
|
|
92
|
+
if (!t || !who || !ev.emoji) break
|
|
93
|
+
const users = (t.reactions[ev.emoji] ??= [])
|
|
94
|
+
const at = users.indexOf(who)
|
|
95
|
+
if (at === -1) users.push(who)
|
|
96
|
+
else { users.splice(at, 1); if (!users.length) delete t.reactions[ev.emoji] }
|
|
97
|
+
break
|
|
98
|
+
}
|
|
99
|
+
// 'profile' events update author snapshots at write time on the client - replay ignores them
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
return [...threads.values()]
|
|
103
|
+
}
|
|
@@ -21,7 +21,8 @@ file in design/instructions/ - they are short, strict, and part of this contract
|
|
|
21
21
|
| Build | hi-fi frames from real components | instructions/craft.md + components.md |
|
|
22
22
|
| Iterate | changing a frame the human has seen, or retiring explorations | instructions/iterate.md |
|
|
23
23
|
| Review | before presenting anything | instructions/review.md |
|
|
24
|
-
| Boards | creating a board
|
|
24
|
+
| Boards | creating a board, choosing what ships | instructions/boards.md |
|
|
25
|
+
| Publish | deploying the canvas: gate, volume, accounts, invites | instructions/publish.md |
|
|
25
26
|
|
|
26
27
|
Refining an existing screen: Configure must hold, then Build + Review. New work runs
|
|
27
28
|
the full ladder. Unsure which phase you are in? Ask the human - one question beats a
|
|
@@ -37,6 +38,17 @@ Stuck, or the human is unhappy with a result? instructions/reference/ holds the
|
|
|
37
38
|
guides (layout, typography, color, motion, copy, states, tuning, critique, concepts) -
|
|
38
39
|
the routing index is at the top of instructions/craft.md. Pull ONE file, apply, return.
|
|
39
40
|
|
|
41
|
+
## When the human points at a specific element
|
|
42
|
+
|
|
43
|
+
Two channels carry element-precise feedback - honor both:
|
|
44
|
+
- **A pasted address** like `design/scenes/hero/a.tsx · #root > div > h1 (a.tsx:12)` is
|
|
45
|
+
a LASER-COPIED pointer: the human pressed L (laser mode), hovered to see the element,
|
|
46
|
+
clicked it, and its exact address landed on their clipboard. Open that frame file and
|
|
47
|
+
go straight to that element - the css path (and source location, when present) are exact.
|
|
48
|
+
- **A pinned comment** on an element: run `npx marver comments list --open --json` - each
|
|
49
|
+
thread carries the anchored element (tag, quoted text, css path, frame). Work that queue
|
|
50
|
+
per instructions/iterate.md; the comment names the div, so read the anchor before the words.
|
|
51
|
+
|
|
40
52
|
## Frames
|
|
41
53
|
- A frame = one file: design/scenes/<scene>/<name>.tsx or .html. One frame, one surface.
|
|
42
54
|
- It default-exports a React component. No imports from the tool are needed. Optional:
|
|
@@ -21,7 +21,8 @@ file in design/instructions/ - they are short, strict, and part of this contract
|
|
|
21
21
|
| Build | hi-fi frames from real components | instructions/craft.md + components.md |
|
|
22
22
|
| Iterate | changing a frame the human has seen, or retiring explorations | instructions/iterate.md |
|
|
23
23
|
| Review | before presenting anything | instructions/review.md |
|
|
24
|
-
| Boards | creating a board
|
|
24
|
+
| Boards | creating a board, choosing what ships | instructions/boards.md |
|
|
25
|
+
| Publish | deploying the canvas: gate, volume, accounts, invites | instructions/publish.md |
|
|
25
26
|
|
|
26
27
|
Refining an existing screen: Configure must hold, then Build + Review. New work runs
|
|
27
28
|
the full ladder. Unsure which phase you are in? Ask the human - one question beats a
|
|
@@ -37,6 +38,17 @@ Stuck, or the human is unhappy with a result? instructions/reference/ holds the
|
|
|
37
38
|
guides (layout, typography, color, motion, copy, states, tuning, critique, concepts) -
|
|
38
39
|
the routing index is at the top of instructions/craft.md. Pull ONE file, apply, return.
|
|
39
40
|
|
|
41
|
+
## When the human points at a specific element
|
|
42
|
+
|
|
43
|
+
Two channels carry element-precise feedback - honor both:
|
|
44
|
+
- **A pasted address** like `design/scenes/hero/a.tsx · #root > div > h1 (a.tsx:12)` is
|
|
45
|
+
a LASER-COPIED pointer: the human pressed L (laser mode), hovered to see the element,
|
|
46
|
+
clicked it, and its exact address landed on their clipboard. Open that frame file and
|
|
47
|
+
go straight to that element - the css path (and source location, when present) are exact.
|
|
48
|
+
- **A pinned comment** on an element: run `npx marver comments list --open --json` - each
|
|
49
|
+
thread carries the anchored element (tag, quoted text, css path, frame). Work that queue
|
|
50
|
+
per instructions/iterate.md; the comment names the div, so read the anchor before the words.
|
|
51
|
+
|
|
40
52
|
## Frames
|
|
41
53
|
- A frame = one file: design/scenes/<scene>/<name>.tsx or .html. One frame, one surface.
|
|
42
54
|
- It default-exports a React component. No imports from the tool are needed. Optional:
|
|
@@ -78,9 +78,12 @@ ordered list of atoms and `{ "space": n }` tokens.
|
|
|
78
78
|
|
|
79
79
|
## Publishing
|
|
80
80
|
|
|
81
|
-
Boards are the unit of publishing
|
|
82
|
-
|
|
83
|
-
|
|
81
|
+
Boards are the unit of publishing: `design/publish.json` names each shipped board
|
|
82
|
+
with `read` or `comment` rights (`marver build` fails without it - default-closed;
|
|
83
|
+
`--boards <name>` overrides ad hoc). Every frame a published flow data-gotos must
|
|
84
|
+
be ON a published board - unlisted frames are excluded from the bundle at build
|
|
85
|
+
time. Deploying the built canvas - gate password, the collaboration volume,
|
|
86
|
+
accounts and invites - is its own phase: **instructions/publish.md**.
|
|
84
87
|
|
|
85
88
|
The published gate page shows the app's identity: `design/logo.svg` + the host
|
|
86
89
|
package name (overridable via config `share`). If the app has no logo asset yet,
|
|
@@ -39,6 +39,34 @@ canvas, the tour - that flow lives in instructions/welcome.md, run it alongside.
|
|
|
39
39
|
several half-brands - document the one the app actually ships in DESIGN.md and
|
|
40
40
|
name the others as legacy.
|
|
41
41
|
|
|
42
|
+
## Working with teammates (branches, merges, a second engineer)
|
|
43
|
+
|
|
44
|
+
The whole `design/` folder is git-tracked - boards, scenes, comment logs, the
|
|
45
|
+
publish policy, these instructions. Only `design/.local/` (this machine's
|
|
46
|
+
connect credential) and `design/.dist/` (built on the host) are ignored. So a
|
|
47
|
+
teammate gets the exact canvas by pulling the branch: `pnpm install` →
|
|
48
|
+
`npx marver dev`. Nothing else to sync.
|
|
49
|
+
|
|
50
|
+
How the three kinds of design state merge across branches:
|
|
51
|
+
|
|
52
|
+
- **Comment logs (`design/comments/*.jsonl`) merge themselves.** They're
|
|
53
|
+
append-only and keyed by event id, and init writes a `merge=union` git
|
|
54
|
+
attribute for them - two branches that both collected feedback union
|
|
55
|
+
cleanly, no conflict. Even a hand-botched merge self-heals: replay dedupes
|
|
56
|
+
by id. This is the point of the event-log design - multiplayer comments
|
|
57
|
+
Just Work through plain git.
|
|
58
|
+
- **Scenes (`design/scenes/**.tsx`) merge like any code** - they're React
|
|
59
|
+
components. Standard review, standard conflicts.
|
|
60
|
+
- **Boards (`design/boards/*.json`) are the one friction** - they're node
|
|
61
|
+
positions, so two people rearranging the same board conflict on x/y. It's
|
|
62
|
+
cosmetic: take either side and re-run Tidy, or give features their own
|
|
63
|
+
boards so layouts don't overlap. Never let a boards conflict block a merge.
|
|
64
|
+
|
|
65
|
+
If collaboration is deployed, each engineer runs `marver comments connect
|
|
66
|
+
<url>` ONCE with their own account (the owner invites them) to get the live
|
|
67
|
+
cloud sync on top of git. Git carries the committed comments; `connect` adds
|
|
68
|
+
the real-time stream from published viewers.
|
|
69
|
+
|
|
42
70
|
## When it breaks mid-project
|
|
43
71
|
|
|
44
72
|
Frames suddenly unstyled → the theme import path moved: fix `design/theme.css`.
|
|
@@ -48,3 +48,41 @@ The human picks a direction; then, in one pass:
|
|
|
48
48
|
Archived frames are history, not options: never link them from live flows,
|
|
49
49
|
never count them as current design. They exist so "what did we try for the
|
|
50
50
|
editor?" has a visual answer.
|
|
51
|
+
|
|
52
|
+
## Comments are your work queue
|
|
53
|
+
|
|
54
|
+
When collaboration is on, humans pin comments to specific elements inside
|
|
55
|
+
frames. Those threads are addressed to YOU as much as to the designer - treat
|
|
56
|
+
the open list as a queue:
|
|
57
|
+
|
|
58
|
+
```bash
|
|
59
|
+
npx marver comments list --open --json # what needs you (anchors included)
|
|
60
|
+
npx marver comments reply <thread> --body "…"
|
|
61
|
+
npx marver comments resolve <thread> --addressed-in <scene/frame>
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
The discipline:
|
|
65
|
+
|
|
66
|
+
1. **Read the anchor before the words.** Each thread carries the element it
|
|
67
|
+
points at - tag, quote, source hint, position. "Too cramped" pinned to a
|
|
68
|
+
button is a different task than "too cramped" on the whole frame.
|
|
69
|
+
2. **Fork, don't overwrite.** Address feedback by creating a NEW variant of the
|
|
70
|
+
frame (the letter convention above) and iterating there. The commented
|
|
71
|
+
frame stays as the before; your variant is the after.
|
|
72
|
+
3. **Resolve with the receipt.** `--addressed-in <the-new-variant>` records
|
|
73
|
+
WHICH frame answered the feedback - the thread becomes an auditable link
|
|
74
|
+
from complaint to fix. Reply first when the change deserves a sentence of
|
|
75
|
+
explanation; resolve silently only for trivial mechanical fixes.
|
|
76
|
+
4. **Never resolve what you didn't address.** Disagree? Reply with your
|
|
77
|
+
reasoning and leave the thread open - the human closes debates, you close
|
|
78
|
+
completed work.
|
|
79
|
+
5. **A frame with open comments is load-bearing - never delete, rename, or
|
|
80
|
+
gut it.** Its threads are anchored to elements INSIDE it; restructure the
|
|
81
|
+
frame and the anchors strand (a dead anchor parks the pin at the frame
|
|
82
|
+
edge; deleting the whole frame strands the thread off-canvas entirely -
|
|
83
|
+
never lost from the log, but invisible until the frame returns). So: fork
|
|
84
|
+
the variant and iterate THERE, leave the commented frame untouched as the
|
|
85
|
+
before, and only once you `resolve --addressed-in <variant>` its threads
|
|
86
|
+
may it move to `archive/`. Resolve first, restructure second - never the
|
|
87
|
+
reverse. Check `comments list --board <b>` (no `--open`) to see resolved
|
|
88
|
+
threads too; the full history lives in the append-only log and in git.
|