@marver-design/marver 0.6.0 → 0.8.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 +95 -0
- package/dist/{build-BZaPa2DS.mjs → build-B8z902TW.mjs} +2 -2
- package/dist/cli.mjs +5 -5
- package/dist/{collab-CXy8gqoz.mjs → collab-pLGzSvm5.mjs} +16 -5
- package/dist/{comments-odHzYdO3.mjs → comments-BrpC86Kf.mjs} +9 -15
- package/dist/{comments-Ba8mU600.mjs → comments-DSwMhdbZ.mjs} +2 -8
- package/dist/daemon-C-huGHAM.mjs +899 -0
- package/dist/{dev-DaPQ9xA5.mjs → dev-BYm9ebfN.mjs} +14 -3
- package/dist/events-BMtBvvgU.mjs +101 -0
- package/dist/{init-DsCUmlCW.mjs → init-DrLomSWq.mjs} +13 -1
- package/dist/ledger-wFvEIEGi.mjs +64 -0
- package/dist/{manifest-C8FODq2S.mjs → manifest-D3eaARf4.mjs} +16 -1
- package/dist/{plugin-wMY9lNf3.mjs → plugin-BVFuRfEo.mjs} +56 -40
- package/dist/profile-BkiWglVE.mjs +39 -0
- package/dist/{serve-D_KBK7Oy.mjs → serve-CwAfayJk.mjs} +3 -3
- package/dist/{sync-CkBk-tUk.mjs → sync-Ch4Bymb1.mjs} +4 -102
- package/package.json +1 -1
- package/src/client/frame-host/bridge.js +28 -242
- package/src/client/frame-host/inspect.d.ts +6 -0
- package/src/client/frame-host/inspect.js +359 -0
- package/src/client/shell/App.tsx +107 -110
- package/src/client/shell/Comments.tsx +465 -70
- package/src/client/shell/Play.tsx +218 -155
- package/src/client/shell/Toolbar.tsx +201 -0
- package/src/client/shell/canvas/FrameNode.tsx +59 -8
- package/src/client/shell/comments-store.ts +47 -6
- package/src/client/shell/icons.tsx +5 -0
- package/src/client/shell/mentions.ts +18 -0
- package/src/client/shell/store.ts +30 -2
- package/src/client/shell/styles.css +356 -30
- package/src/client/stage/main.tsx +35 -11
- package/src/shared/events.ts +22 -5
- package/templates/AGENTS-embedded.md +1 -0
- package/templates/AGENTS-studio.md +1 -0
- package/templates/instructions/configure.md +15 -0
- package/templates/instructions/jam.md +85 -0
- package/dist/rolldown-runtime-D7D4PA-g.mjs +0 -13
|
@@ -287,7 +287,102 @@ body.sh-commenting .sh-lean { opacity: 0 }
|
|
|
287
287
|
}
|
|
288
288
|
@keyframes sh-orbit { to { --sh-rot: 360deg } }
|
|
289
289
|
@media (prefers-reduced-motion: reduce) {
|
|
290
|
-
.sh-node.interact::before, .sh-node.interact::after
|
|
290
|
+
.sh-node.interact::before, .sh-node.interact::after,
|
|
291
|
+
.sh-node.working::before { animation: none } }
|
|
292
|
+
|
|
293
|
+
/* Live Jam "working" (SPEC §10): the SELECTED state's exact geometry - crisp border + 2px screen-px
|
|
294
|
+
outline + 4px ring, edge-light off - in MARVER BLUE (Nic's pick over yellow), composed of three
|
|
295
|
+
layers: the border shimmer (::before orbit), the periodic content WAVE (.sh-work-wave), and the
|
|
296
|
+
shimmer MATRIX of tiny marks on the left flank (.sh-work-ind). Every animated layer is phased by
|
|
297
|
+
--mv-w0 (negative ms since THIS frame's job started), so parallel frames never pulse in sync.
|
|
298
|
+
When a human is actually IN the frame, interact's magenta treatment wins. */
|
|
299
|
+
.sh-node.working { --work: var(--mv-blue); --work-ring: rgba(0, 136, 255, .2) }
|
|
300
|
+
.sh-app.dark .sh-node.working { --work: #4db1ff; --work-ring: rgba(0, 136, 255, .3) }
|
|
301
|
+
.sh-node.working:not(.interact) { border-color: var(--work);
|
|
302
|
+
outline: calc(2px * var(--sh-inv, 1)) solid var(--work);
|
|
303
|
+
outline-offset: calc(-1px * var(--sh-inv, 1));
|
|
304
|
+
box-shadow: 0 0 0 calc(4px * var(--sh-inv, 1)) var(--work-ring), var(--shadow-node) }
|
|
305
|
+
.sh-node.working:not(.interact)::after { display: none } /* the blue geometry owns the edge, like .sel */
|
|
306
|
+
.sh-node.working:not(.interact)::before {
|
|
307
|
+
content: ''; position: absolute; pointer-events: none;
|
|
308
|
+
inset: calc(-2.5px * var(--sh-inv, 1));
|
|
309
|
+
padding: calc(2.5px * var(--sh-inv, 1));
|
|
310
|
+
border-radius: calc(var(--r-node) + 2px * var(--sh-inv, 1));
|
|
311
|
+
background: conic-gradient(from var(--sh-rot),
|
|
312
|
+
var(--work) 0deg,
|
|
313
|
+
var(--work) 105deg,
|
|
314
|
+
rgba(255, 255, 255, .95) 135deg,
|
|
315
|
+
var(--work) 165deg,
|
|
316
|
+
var(--work) 285deg,
|
|
317
|
+
rgba(255, 255, 255, .95) 315deg,
|
|
318
|
+
var(--work) 345deg,
|
|
319
|
+
var(--work) 360deg);
|
|
320
|
+
-webkit-mask: linear-gradient(#000 0 0) content-box, linear-gradient(#000 0 0);
|
|
321
|
+
-webkit-mask-composite: xor;
|
|
322
|
+
mask: linear-gradient(#000 0 0) content-box, linear-gradient(#000 0 0);
|
|
323
|
+
mask-composite: exclude;
|
|
324
|
+
opacity: .6;
|
|
325
|
+
animation: sh-orbit 4.2s linear infinite;
|
|
326
|
+
animation-delay: var(--mv-w0, 0s) }
|
|
327
|
+
|
|
328
|
+
/* THE WAVE (Option A: content sweep). One 36s cycle holds five ~1.7s sweeps - dense right after
|
|
329
|
+
the trigger (0s, 5s, 10s), then spaced (19s, 29s) - so a fresh job announces itself and a long
|
|
330
|
+
one keeps a calmer heartbeat. Jumps between sweeps happen at opacity 0 (invisible). */
|
|
331
|
+
.sh-work-wave { position: absolute; inset: 0; overflow: hidden; border-radius: inherit;
|
|
332
|
+
pointer-events: none; z-index: 4 }
|
|
333
|
+
.sh-work-wave::before { content: ''; position: absolute; left: 0; right: 0; top: -55%; height: 55%;
|
|
334
|
+
opacity: 0;
|
|
335
|
+
background: linear-gradient(180deg,
|
|
336
|
+
rgba(0, 136, 255, 0) 0%, rgba(0, 136, 255, .07) 30%, rgba(0, 136, 255, .16) 52%,
|
|
337
|
+
rgba(0, 136, 255, .07) 74%, rgba(0, 136, 255, 0) 100%);
|
|
338
|
+
animation: sh-work-wave 36s linear infinite;
|
|
339
|
+
animation-delay: var(--mv-w0, 0s) }
|
|
340
|
+
@keyframes sh-work-wave {
|
|
341
|
+
0% { transform: translateY(0); opacity: 0 }
|
|
342
|
+
0.4% { opacity: 1 }
|
|
343
|
+
3.9% { opacity: 1 }
|
|
344
|
+
4.7% { transform: translateY(282%); opacity: 0 }
|
|
345
|
+
13.8% { transform: translateY(0); opacity: 0 }
|
|
346
|
+
14.2% { opacity: 1 }
|
|
347
|
+
17.7% { opacity: 1 }
|
|
348
|
+
18.5% { transform: translateY(282%); opacity: 0 }
|
|
349
|
+
27.7% { transform: translateY(0); opacity: 0 }
|
|
350
|
+
28.1% { opacity: 1 }
|
|
351
|
+
31.6% { opacity: 1 }
|
|
352
|
+
32.4% { transform: translateY(282%); opacity: 0 }
|
|
353
|
+
52.7% { transform: translateY(0); opacity: 0 }
|
|
354
|
+
53.1% { opacity: 1 }
|
|
355
|
+
56.6% { opacity: 1 }
|
|
356
|
+
57.4% { transform: translateY(282%); opacity: 0 }
|
|
357
|
+
80.5% { transform: translateY(0); opacity: 0 }
|
|
358
|
+
80.9% { opacity: 1 }
|
|
359
|
+
84.4% { opacity: 1 }
|
|
360
|
+
85.2% { transform: translateY(282%); opacity: 0 }
|
|
361
|
+
100% { transform: translateY(282%); opacity: 0 }
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
/* THE SHIMMER MATRIX: 3 x 6 tiny marver marks on the left flank, top-aligned (the vbadge lane).
|
|
365
|
+
Screen-constant via --sh-inv (tight at every zoom); each mark twinkles on its own scattered
|
|
366
|
+
beat, phased per frame by --mv-w0. */
|
|
367
|
+
/* CLAMPED scaling (the vbadge pattern): screen-constant ~4px marks in the working zoom band,
|
|
368
|
+
but floored at 3 world px (deep zoom-in: grows slightly on screen instead of vanishing) and
|
|
369
|
+
capped at 13 world px (far zoom-out: shrinks with the canvas instead of dominating the frame
|
|
370
|
+
and bleeding onto the left neighbor). No transform scale - the sizes ARE the clamp. */
|
|
371
|
+
.sh-work-ind { position: absolute; top: 0;
|
|
372
|
+
--shm: clamp(3px, calc(4px * var(--sh-inv, 1)), 13px);
|
|
373
|
+
--shg: clamp(1px, calc(1.5px * var(--sh-inv, 1)), 4px);
|
|
374
|
+
right: calc(100% + clamp(6px, calc(10px * var(--sh-inv, 1)), 26px));
|
|
375
|
+
display: grid; grid-template-columns: repeat(2, var(--shm)); gap: var(--shg);
|
|
376
|
+
pointer-events: none; z-index: 5 }
|
|
377
|
+
/* a variant badge (A / B / C + name) owns the top of the left flank - the shimmer sits below it.
|
|
378
|
+
The offset mirrors the badge's own zoom clamps (letter 40px-120px + label), so it tracks the
|
|
379
|
+
badge as it grows and shrinks across the zoom range. */
|
|
380
|
+
.sh-work-ind.below-vbadge { top: min(clamp(64px, calc(32px * var(--sh-inv, 1)), 170px), calc(64px * var(--sh-inv, 1))) }
|
|
381
|
+
.sh-work-ind svg { display: block; width: var(--shm); height: var(--shm); color: var(--work, var(--mv-blue));
|
|
382
|
+
opacity: .12; animation: sh-work-shim .95s ease-in-out infinite }
|
|
383
|
+
@keyframes sh-work-shim { 0%, 100% { opacity: .12; transform: scale(.75) } 35% { opacity: 1; transform: scale(1.15) } }
|
|
384
|
+
@media (prefers-reduced-motion: reduce) {
|
|
385
|
+
.sh-work-wave::before, .sh-work-ind svg { animation: none } }
|
|
291
386
|
|
|
292
387
|
/* selection toolbar: screen-space overlay, constant size at any zoom */
|
|
293
388
|
.sh-ctx { position: absolute; display: flex; align-items: center; gap: 1px;
|
|
@@ -417,7 +512,9 @@ body.sh-commenting .sh-lean { opacity: 0 }
|
|
|
417
512
|
|
|
418
513
|
/* dropdown menu (theme) - portaled to .sh-app (its own backdrop root), left-aligned to its trigger */
|
|
419
514
|
.sh-theme { position: relative }
|
|
420
|
-
.sh-
|
|
515
|
+
/* z above the play backdrop (.sh-play is z-30): a dropdown portals to .sh-app, so in the
|
|
516
|
+
prototype it must clear the full-window overlay or it renders behind it, invisible */
|
|
517
|
+
.sh-menu { position: absolute; min-width: 136px; z-index: 40;
|
|
421
518
|
border-radius: 16px; padding: 5px; display: flex; flex-direction: column;
|
|
422
519
|
transform-origin: top left; animation: sh-pop .14s ease }
|
|
423
520
|
@keyframes sh-pop { from { opacity: 0; transform: scale(.95) translateY(-4px) } }
|
|
@@ -431,7 +528,7 @@ body.sh-commenting .sh-lean { opacity: 0 }
|
|
|
431
528
|
.sh-menu .div { height: 1px; background: var(--glass-brd); margin: 4px 6px }
|
|
432
529
|
|
|
433
530
|
/* shadcn-style tooltip: contrast-flipped solid, zoom-fade in */
|
|
434
|
-
.sh-tip { position: absolute; z-index:
|
|
531
|
+
.sh-tip { position: absolute; z-index: 400; pointer-events: none; padding: 5px 10px; border-radius: 8px; /* topmost ephemeral chrome - above modals (300) or in-dialog tips vanish */
|
|
435
532
|
font: 500 12px -apple-system, system-ui, sans-serif; white-space: nowrap; transform: translate(-50%, -100%);
|
|
436
533
|
display: flex; flex-direction: row; align-items: center; gap: 10px;
|
|
437
534
|
background: var(--tip-bg); color: var(--tip-ink);
|
|
@@ -469,10 +566,63 @@ body.sh-commenting .sh-lean { opacity: 0 }
|
|
|
469
566
|
transition: color .15s, background .15s }
|
|
470
567
|
.sh-update .x:hover { background: var(--glass-hover); color: var(--glass-ink) }
|
|
471
568
|
|
|
472
|
-
|
|
569
|
+
/* bottom-RIGHT now (SPEC-live-jam §9): jam reply pills live here; plain toasts ride along */
|
|
570
|
+
.sh-toasts { position: absolute; right: var(--edge); bottom: var(--edge); z-index: 32; display: flex;
|
|
571
|
+
flex-direction: column; align-items: flex-end; gap: 8px; max-width: min(360px, 40vw) }
|
|
473
572
|
.sh-toast { display: flex; align-items: center; gap: 6px; font: 500 12px -apple-system, system-ui, sans-serif;
|
|
474
573
|
color: var(--glass-ink-2); border-radius: 999px; padding: 7px 13px }
|
|
475
574
|
.sh-toast svg { color: var(--glass-good); flex: none }
|
|
575
|
+
/* the jam pill's svgs are NOT the toast checkmark: the mark stays white/90 on blue, the X keeps
|
|
576
|
+
its own muted ink - undo the blanket green above */
|
|
577
|
+
.sh-toast.jam svg { color: currentColor }
|
|
578
|
+
.sh-toast-more { color: var(--glass-ink-3); font-size: 11px; padding: 2px 10px }
|
|
579
|
+
|
|
580
|
+
/* the Live Jam reply pill: our full glass treatment, full radius, one row (§9) */
|
|
581
|
+
.sh-toast.jam { gap: 12px; padding: 12px 12px 12px 14px; width: 380px; max-width: 100%; border-radius: 999px;
|
|
582
|
+
background: var(--glass); backdrop-filter: var(--blur); -webkit-backdrop-filter: var(--blur);
|
|
583
|
+
border: 1px solid var(--glass-brd); box-shadow: var(--shadow-glass), 0 12px 32px -12px rgba(24,24,27,.4);
|
|
584
|
+
color: var(--glass-ink); animation: sh-jam-in .18s ease }
|
|
585
|
+
@keyframes sh-jam-in { from { opacity: 0; transform: translateY(6px) } to { opacity: 1; transform: none } }
|
|
586
|
+
/* Marver is ALWAYS its own blue - never the shell accent, which swaps to magenta in interact mode
|
|
587
|
+
(.sh-app.interacting). A fixed var the swap can't touch keeps the avatar + notification on-brand. */
|
|
588
|
+
:root { --mv-blue: #0088ff }
|
|
589
|
+
.sh-jam-mark { flex: none; width: 34px; height: 34px; border-radius: 50%; background: var(--mv-blue); color: rgba(255,255,255,.9);
|
|
590
|
+
display: inline-flex; align-items: center; justify-content: center }
|
|
591
|
+
.sh-jam-txt { flex: 1; min-width: 0; display: flex; flex-direction: column; gap: 0; line-height: 1.25 }
|
|
592
|
+
/* row 1: the FRAME is the news - its title in blue (the mark already says it is Marver) */
|
|
593
|
+
.sh-jam-frame { display: flex; align-items: center; font-size: 13.5px; font-weight: 600; color: var(--mv-blue); min-width: 0 }
|
|
594
|
+
.sh-jam-frame .t { overflow: hidden; text-overflow: ellipsis; white-space: nowrap }
|
|
595
|
+
/* row 2: the message alone, darker for readability */
|
|
596
|
+
.sh-jam-prev { font-size: 12.5px; color: var(--glass-ink-2, var(--glass-ink)); overflow: hidden; text-overflow: ellipsis; white-space: nowrap }
|
|
597
|
+
|
|
598
|
+
/* the DECK (3+): newest pill on top, two card edges peeking beneath; +N badge; click to expand */
|
|
599
|
+
.sh-jam-deck { position: relative; cursor: pointer }
|
|
600
|
+
.sh-jam-deck .sh-toast.jam { position: relative; margin: 0 }
|
|
601
|
+
.sh-jam-ghost { position: absolute; left: 0; right: 0; height: 100%; border-radius: 999px;
|
|
602
|
+
background: var(--glass); border: 1px solid var(--glass-brd); box-shadow: var(--shadow-glass) }
|
|
603
|
+
.sh-jam-ghost.g1 { top: 6px; left: 5px; right: 5px; opacity: .8 }
|
|
604
|
+
.sh-jam-ghost.g2 { top: 12px; left: 11px; right: 11px; opacity: .5 }
|
|
605
|
+
.sh-jam-deck { margin-bottom: 12px } /* room for the peeking edges */
|
|
606
|
+
.sh-jam-badge { flex: none; font: 700 11px -apple-system, system-ui, sans-serif; color: var(--glass-ink);
|
|
607
|
+
background: var(--glass-hover); border-radius: 999px; padding: 4px 9px }
|
|
608
|
+
|
|
609
|
+
/* the EXPANDED list: newest first, header with count + Clear all, scrolls past ~6.
|
|
610
|
+
The scroller CLIPS at its box - pad it out to the pills' shadow reach (12px drop, 32px
|
|
611
|
+
blur) and cancel with negative margins, so the shadows breathe and nothing moves. */
|
|
612
|
+
.sh-jam-list { display: flex; flex-direction: column; gap: 8px; align-items: stretch;
|
|
613
|
+
max-height: min(60vh, 520px); overflow-y: auto; overscroll-behavior: contain; scrollbar-width: thin;
|
|
614
|
+
padding: 22px 26px 32px; margin: -22px -26px -32px }
|
|
615
|
+
.sh-jam-listhead { display: flex; align-items: center; gap: 8px; padding: 0 6px;
|
|
616
|
+
font: 700 10px -apple-system, system-ui, sans-serif; letter-spacing: .06em; color: var(--glass-ink-3) }
|
|
617
|
+
.sh-jam-listhead button { margin-left: auto; border: 0; background: none; cursor: pointer;
|
|
618
|
+
font: 600 11px -apple-system, system-ui, sans-serif; color: var(--mv-blue); padding: 3px 6px }
|
|
619
|
+
.sh-jam-listhead button + button { margin-left: 0; color: var(--glass-ink-3); display: inline-flex }
|
|
620
|
+
.sh-jam-view { flex: none; border: 0; border-radius: 999px; padding: 5px 12px; cursor: pointer;
|
|
621
|
+
background: var(--mv-blue); color: #fff; font: 600 12px -apple-system, system-ui, sans-serif; transition: filter .15s }
|
|
622
|
+
.sh-jam-view:hover { filter: brightness(1.08) }
|
|
623
|
+
.sh-jam-x { flex: none; display: inline-flex; align-items: center; justify-content: center; width: 24px; height: 24px;
|
|
624
|
+
border: 0; border-radius: 50%; background: none; color: var(--glass-ink-3); cursor: pointer; transition: background .12s, color .12s }
|
|
625
|
+
.sh-jam-x:hover { background: var(--glass-hover); color: var(--glass-ink) }
|
|
476
626
|
|
|
477
627
|
/* ---- Play mode (SPEC-M2 §1): near-black stage, one centered device, auto-hiding bar.
|
|
478
628
|
Fixed dark palette - the backdrop never follows the board theme, so no tokens here. */
|
|
@@ -643,6 +793,57 @@ body.sh-kbd .sh-app :focus-visible { outline: 2px solid var(--accent); outline-o
|
|
|
643
793
|
divider needs a touch more air before the first chip */
|
|
644
794
|
.sh-play-nav .sep + .vchip { margin-left: 5px }
|
|
645
795
|
|
|
796
|
+
/* --- prototype-review: shared toolbar in play, Hide-UI, comment overlay --- */
|
|
797
|
+
|
|
798
|
+
/* Hide-UI: one binary body class (canvas + prototype) drops all operating chrome. Not
|
|
799
|
+
persisted - a page refresh always restores it. Comment pins/threads (the review
|
|
800
|
+
annotations) stay; only the marver chrome hides, for a clean point-and-shoot frame. */
|
|
801
|
+
body.sh-hide-ui .sh-pill,
|
|
802
|
+
body.sh-hide-ui .sh-pill-fab,
|
|
803
|
+
body.sh-hide-ui .sh-panel,
|
|
804
|
+
body.sh-hide-ui .sh-fab,
|
|
805
|
+
body.sh-hide-ui .sh-ctx,
|
|
806
|
+
body.sh-hide-ui .sh-update,
|
|
807
|
+
body.sh-hide-ui .sh-toasts,
|
|
808
|
+
body.sh-hide-ui .sh-banner,
|
|
809
|
+
body.sh-hide-ui .sh-play-nav { display: none !important }
|
|
810
|
+
|
|
811
|
+
/* the prototype pill reuses the shared pill LAYOUT but wears the discreet DARK skin of the
|
|
812
|
+
bottom-left navigator (not the bright canvas glass), so it recedes into the near-black
|
|
813
|
+
stage: dark translucent fill, a faint white hairline, light-on-dark controls. */
|
|
814
|
+
.sh-play-pill, .sh-play .sh-pill-fab { z-index: 32 }
|
|
815
|
+
.sh-play-pill.sh-pill, .sh-play .sh-pill-fab {
|
|
816
|
+
background: rgba(18, 18, 24, .8); border: 1px solid rgba(255, 255, 255, .1);
|
|
817
|
+
backdrop-filter: blur(16px); -webkit-backdrop-filter: blur(16px);
|
|
818
|
+
box-shadow: 0 8px 28px rgba(0, 0, 0, .38) }
|
|
819
|
+
.sh-play-pill::after, .sh-play .sh-pill-fab::after { display: none } /* no light edge-light overlay */
|
|
820
|
+
.sh-play-pill .sh-pill-btn, .sh-play .sh-pill-fab { color: rgba(245, 245, 247, .6) }
|
|
821
|
+
.sh-play-pill .sh-pill-btn:hover, .sh-play .sh-pill-fab:hover { background: rgba(255, 255, 255, .1); color: #f5f5f7 }
|
|
822
|
+
.sh-play-pill .sh-pill-btn.on { background: rgba(255, 255, 255, .14); color: #f5f5f7 }
|
|
823
|
+
.sh-play-pill .sep { background: rgba(255, 255, 255, .12) }
|
|
824
|
+
.sh-play-pill .bd { color: rgba(245, 245, 247, .82) }
|
|
825
|
+
.sh-pill-btn.bd { width: auto; gap: 6px; padding: 0 8px 0 11px; white-space: nowrap; font-weight: 600 }
|
|
826
|
+
.sh-play-update { width: auto !important; gap: 5px; padding: 0 11px !important; font-size: 12px; font-weight: 600 }
|
|
827
|
+
/* the picker dropdowns portal to .sh-app; in play they carry .sh-menu-dark for the dark skin */
|
|
828
|
+
.sh-menu-dark { background: rgba(20, 20, 26, .97); border-color: rgba(255, 255, 255, .12); box-shadow: 0 12px 40px rgba(0, 0, 0, .5) }
|
|
829
|
+
.sh-menu-dark::after { display: none }
|
|
830
|
+
.sh-menu-dark button { color: rgba(245, 245, 247, .82) }
|
|
831
|
+
.sh-menu-dark button:hover { background: rgba(255, 255, 255, .1) }
|
|
832
|
+
.sh-menu-dark .chk { color: #6db3ff }
|
|
833
|
+
.sh-menu-dark kbd { color: rgba(245, 245, 247, .5) }
|
|
834
|
+
.sh-menu-dark .div { background: rgba(255, 255, 255, .1) }
|
|
835
|
+
|
|
836
|
+
/* device + comment overlay share one flex-centered box; the overlay is inset:0 over it, a
|
|
837
|
+
SIBLING of the clipping .dev so a thread card is never eaten by the device's overflow.
|
|
838
|
+
overflow:visible lets a card spill past the device edge; only its markers catch the
|
|
839
|
+
pointer, the stage below stays fully interactive. */
|
|
840
|
+
.sh-play-stage { position: relative; flex: none }
|
|
841
|
+
/* the canvas writes --sh-inv (1/zoom) for screen-constant pins; the prototype has no
|
|
842
|
+
zoom, so neutralise it or the leaked value scales every comment pin by ~12x at overview */
|
|
843
|
+
.sh-play { --sh-inv: 1 }
|
|
844
|
+
.sh-play-comments { position: absolute; inset: 0; z-index: 31; overflow: visible; pointer-events: none }
|
|
845
|
+
.sh-play-comments > * { pointer-events: auto }
|
|
846
|
+
|
|
646
847
|
/* the current variant's name trails the chips at natural width - the chips anchor,
|
|
647
848
|
so a name change grows the pill rightward without shifting any control underfoot.
|
|
648
849
|
A max caps runaway titles; the Tip carries the full name. */
|
|
@@ -704,10 +905,16 @@ body.sh-kbd .sh-app :focus-visible { outline: 2px solid var(--accent); outline-o
|
|
|
704
905
|
outline hugging the edge, 4px ring halo - in the comment green (the pin
|
|
705
906
|
is already screen-space via scale(--sh-inv), so plain px match the
|
|
706
907
|
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) }
|
|
908
|
+
.cm-pin.on { border-color: var(--cm-line, var(--comment));
|
|
909
|
+
outline: 2px solid var(--cm-line, var(--comment)); outline-offset: -1px;
|
|
910
|
+
box-shadow: 0 0 0 4px var(--cm-line-ring, var(--comment-ring)), 0 1px 2px rgba(24,24,27,.18), 0 6px 16px -6px rgba(24,24,27,.35) }
|
|
710
911
|
.cm-pin.orphan { border-style: dashed; opacity: .75 }
|
|
912
|
+
/* the open pin's tail leans TOWARD its card. Default (sharp bottom-left) already reads "left";
|
|
913
|
+
card on the RIGHT mirrors the pin around its anchor - sharp corner bottom-right, box extending
|
|
914
|
+
left+up, so the tail points right at the conversation. */
|
|
915
|
+
.cm-pin.tail-r { border-radius: 18px 18px 4px 18px;
|
|
916
|
+
transform: translate(calc(-100% + 2px), -100%) scale(var(--sh-inv, 1));
|
|
917
|
+
transform-origin: bottom right }
|
|
711
918
|
|
|
712
919
|
/* stack: the frame-corner cluster - everyone across the frame's threads */
|
|
713
920
|
.cm-stack { position: absolute; top: 10px; right: 10px; z-index: 30;
|
|
@@ -721,25 +928,73 @@ body.sh-kbd .sh-app :focus-visible { outline: 2px solid var(--accent); outline-o
|
|
|
721
928
|
transform: scale(var(--sh-inv, 1)); transform-origin: top left;
|
|
722
929
|
background: var(--glass); backdrop-filter: var(--blur); -webkit-backdrop-filter: var(--blur);
|
|
723
930
|
/* 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);
|
|
931
|
+
color: var(--glass-ink); border-radius: 20px; border: 1px solid var(--cm-line, var(--comment));
|
|
932
|
+
outline: 2px solid var(--cm-line, var(--comment)); outline-offset: -1px;
|
|
933
|
+
box-shadow: 0 0 0 4px var(--cm-line-ring, var(--comment-ring)), var(--shadow-glass), 0 16px 40px -12px rgba(24,24,27,.35);
|
|
727
934
|
display: flex; flex-direction: column; gap: 12px;
|
|
728
935
|
font: 400 13px -apple-system, system-ui, sans-serif; letter-spacing: -.005em; text-align: left }
|
|
729
936
|
.cm-card.flip { transform: translateX(-100%) scale(var(--sh-inv, 1)); transform-origin: top right }
|
|
937
|
+
/* the open thread on the CANVAS is UI, not artwork: a FIXED screen-space panel that never zooms or
|
|
938
|
+
pans with the canvas. Viewport-bounded; the messages scroll inside (.cm-scroll), the composer
|
|
939
|
+
stays pinned at the bottom. */
|
|
940
|
+
/* parked beside the frame (canvas): a WORLD object - it stays put with its frame through panning.
|
|
941
|
+
Scale + max-height come inline (clamped 0.9..1.35 of screen-constant; height capped at the
|
|
942
|
+
frame's). A screen-ish gutter separates card from frame. Long threads scroll inside. */
|
|
943
|
+
/* parked beside the frame (canvas): a world object that scales EXACTLY like the pins - the base
|
|
944
|
+
.cm-card scale(--sh-inv) - so it zooms smoothly with the board and stays readable at any zoom.
|
|
945
|
+
Screen-constant gutters via margins (the left one wider - it clears the working shimmer). */
|
|
946
|
+
.cm-card.parked { width: 320px; overflow: hidden; z-index: 50 }
|
|
947
|
+
.cm-card.parked:hover { z-index: 60 } /* reading/replying wins over neighbouring chrome */
|
|
948
|
+
/* gutters: SNUG by default (~12px screen), and only as wide as what actually occupies the flank -
|
|
949
|
+
the working shimmer strip (.flank-shim) or a variant badge (.flank-badge, the widest). All
|
|
950
|
+
screen-targeted with world caps so they stay sane at both zoom extremes. */
|
|
951
|
+
.cm-card.parked.dock-r { transform-origin: top left; margin-left: clamp(8px, calc(12px * var(--sh-inv, 1)), 36px) }
|
|
952
|
+
.cm-card.parked.dock-l { transform: translateX(-100%) scale(var(--sh-inv, 1)); transform-origin: top right;
|
|
953
|
+
margin-left: calc(-1 * clamp(8px, calc(12px * var(--sh-inv, 1)), 36px)) }
|
|
954
|
+
.cm-card.parked.dock-l.flank-shim { margin-left: calc(-1 * clamp(18px, calc(28px * var(--sh-inv, 1)), 68px)) }
|
|
955
|
+
.cm-card.parked.dock-l.flank-badge { margin-left: calc(-1 * clamp(26px, calc(44px * var(--sh-inv, 1)), 108px)) }
|
|
956
|
+
/* PLAY: fixed beside the static device, viewport-clamped (position comes inline). Near-solid
|
|
957
|
+
ground - the stage floats the card over arbitrary artwork on a dark room, glass is unreadable
|
|
958
|
+
there. No transform: play has no zoom, and a leaked --sh-inv must never scale it. */
|
|
959
|
+
.cm-card.stage-dock { width: 320px; overflow: hidden; z-index: 60; transform: none;
|
|
960
|
+
background: var(--cm-modal-bg, var(--glass)) }
|
|
961
|
+
/* the scroller reaches INTO the card's right padding so its scrollbar rides the card's far
|
|
962
|
+
edge - right of the close button, 3px off the border, a 13px gap to the text. The bar is
|
|
963
|
+
OURS: a slim glass-gray thumb on a transparent track, and it only exists when the thread
|
|
964
|
+
actually overflows. Text width never shifts when it appears. */
|
|
965
|
+
.cm-scroll { display: flex; flex-direction: column; gap: 12px; min-height: 0; overflow-y: auto;
|
|
966
|
+
overscroll-behavior: contain; margin-right: -13px; padding-right: 13px }
|
|
967
|
+
.cm-scroll::-webkit-scrollbar { width: 6px }
|
|
968
|
+
.cm-scroll::-webkit-scrollbar-track { background: transparent }
|
|
969
|
+
.cm-scroll::-webkit-scrollbar-thumb { border-radius: 99px;
|
|
970
|
+
background: color-mix(in srgb, var(--glass-ink-3) 50%, transparent) }
|
|
971
|
+
.cm-scroll::-webkit-scrollbar-thumb:hover { background: var(--glass-ink-3) }
|
|
972
|
+
@supports not selector(::-webkit-scrollbar) {
|
|
973
|
+
.cm-scroll { scrollbar-width: thin;
|
|
974
|
+
scrollbar-color: color-mix(in srgb, var(--glass-ink-3) 50%, transparent) transparent }
|
|
975
|
+
}
|
|
976
|
+
/* "there is more": the scroller masks its OWN content at the overflowing edge - a clean fade of
|
|
977
|
+
the text itself, theme-proof (no paint layer fighting the glass) */
|
|
978
|
+
.cm-scroll.fade-top { -webkit-mask-image: linear-gradient(180deg, transparent, #000 30px); mask-image: linear-gradient(180deg, transparent, #000 30px) }
|
|
979
|
+
.cm-scroll.fade-bot { -webkit-mask-image: linear-gradient(180deg, #000 calc(100% - 30px), transparent); mask-image: linear-gradient(180deg, #000 calc(100% - 30px), transparent) }
|
|
980
|
+
.cm-scroll.fade-top.fade-bot { -webkit-mask-image: linear-gradient(180deg, transparent, #000 30px, #000 calc(100% - 30px), transparent); mask-image: linear-gradient(180deg, transparent, #000 30px, #000 calc(100% - 30px), transparent) }
|
|
730
981
|
.cm-card header { display: flex; align-items: center; gap: 9px }
|
|
731
982
|
.cm-card header b { font-size: 14px; font-weight: 600; letter-spacing: -.01em }
|
|
732
983
|
.cm-card .dim { color: var(--glass-ink-3); font-size: 11.5px; font-variant-numeric: tabular-nums }
|
|
733
|
-
|
|
984
|
+
/* pinned above the scroller so the close button always wins the click - bare buttons on the
|
|
985
|
+
card's own glass, no chip: the custom scrollbar lives at the card's far edge, right of the
|
|
986
|
+
X, so the two never collide. */
|
|
987
|
+
.cm-actions { position: absolute; top: 13px; right: 12px; display: flex; gap: 2px; z-index: 5 }
|
|
734
988
|
/* 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-
|
|
737
|
-
.cm-
|
|
989
|
+
so a long display name ellipsizes instead of running under the icons.
|
|
990
|
+
(root header + body live INSIDE .cm-scroll - scope there, not .cm-card >) */
|
|
991
|
+
.cm-scroll > header { padding-right: 84px }
|
|
992
|
+
.cm-scroll > header b { overflow: hidden; text-overflow: ellipsis; white-space: nowrap }
|
|
738
993
|
/* every message - root and replies - is the same unit: avatar + name + time,
|
|
739
994
|
body indented to the name column (24px avatar + 9px gap) */
|
|
740
995
|
.cm-card .cm-body { margin: 0; padding-left: 33px; font-size: 13px; line-height: 1.55;
|
|
741
996
|
color: var(--glass-ink); white-space: pre-wrap; overflow-wrap: break-word }
|
|
742
|
-
.cm-
|
|
997
|
+
.cm-scroll > .cm-body { margin-top: -10px } /* pulls against the scroller's 12px gap -> the same 2px replies get */
|
|
743
998
|
.cm-msg { display: flex; flex-direction: column; gap: 2px }
|
|
744
999
|
.cm-icon { display: inline-flex; align-items: center; justify-content: center; width: 26px; height: 26px;
|
|
745
1000
|
border: 0; border-radius: 8px; background: none; color: var(--glass-ink-3); cursor: pointer;
|
|
@@ -752,30 +1007,85 @@ body.sh-kbd .sh-app :focus-visible { outline: 2px solid var(--accent); outline-o
|
|
|
752
1007
|
.cm-copy .b { transform: scale(.4); opacity: 0; color: var(--comment) }
|
|
753
1008
|
.cm-copy.ok .a { transform: scale(.4); opacity: 0 }
|
|
754
1009
|
.cm-copy.ok .b { transform: scale(1); opacity: 1 }
|
|
755
|
-
/* composer row: my avatar (list size) +
|
|
756
|
-
.
|
|
1010
|
+
/* composer row: my avatar (list size) + an auto-growing textarea with a round send button inside.
|
|
1011
|
+
Multiline: Enter sends, Shift+Enter is a newline (Live Jam §6). The textarea grows 1-6 rows. */
|
|
1012
|
+
.cm-compose { display: flex; align-items: flex-end; gap: 8px }
|
|
1013
|
+
/* the row bottom-aligns (the avatar holds still while the textarea grows), so the avatar
|
|
1014
|
+
needs a lift to sit optically centered against a ONE-LINE pill: (38-24)/2 */
|
|
1015
|
+
.cm-compose > .cm-me, .cm-compose > .cm-avatar { margin-bottom: 7px }
|
|
757
1016
|
.cm-inputwrap { position: relative; flex: 1; min-width: 0 }
|
|
758
|
-
.cm-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
1017
|
+
/* the mirror layer (.cm-hl) and the textarea share an IDENTICAL box so the caret + styled text align
|
|
1018
|
+
exactly. The textarea sits on top with TRANSPARENT text + bg (caret visible); .cm-hl behind paints the
|
|
1019
|
+
field + the styled text, so @marver shows its trigger colour live as you type (Live Jam §1). */
|
|
1020
|
+
.cm-inputwrap textarea, .cm-hl { box-sizing: border-box; width: 100%; min-height: 38px; max-height: 132px;
|
|
1021
|
+
margin: 0; padding: 9px 42px 9px 15px; border: 1px solid transparent; border-radius: 19px;
|
|
1022
|
+
/* the math must CLOSE: 1 + 9 + 18 + 9 + 1 = 38 exactly, so a single line sits dead-center
|
|
1023
|
+
in the pill (a fractional line box + uncounted borders left the text riding off-center) */
|
|
1024
|
+
font: 400 13px/18px -apple-system, system-ui, sans-serif; letter-spacing: normal;
|
|
1025
|
+
white-space: pre-wrap; overflow-wrap: break-word; word-break: break-word }
|
|
1026
|
+
.cm-hl { position: absolute; inset: 0; z-index: 0; overflow: hidden; pointer-events: none;
|
|
1027
|
+
background: var(--cm-field); color: var(--glass-ink) }
|
|
1028
|
+
.cm-inputwrap textarea { position: relative; z-index: 1; display: block; resize: none; overflow-y: auto;
|
|
1029
|
+
background: transparent; color: transparent; caret-color: var(--glass-ink);
|
|
1030
|
+
border-color: var(--glass-brd); outline: none; transition: border-color .15s, box-shadow .15s }
|
|
1031
|
+
.cm-inputwrap textarea::placeholder { color: var(--glass-ink-3) }
|
|
1032
|
+
.cm-inputwrap textarea:focus { border-color: var(--cm-line, var(--comment)); box-shadow: 0 0 0 3px var(--cm-line-ring, var(--comment-ring)) }
|
|
1033
|
+
.cm-inputwrap textarea:disabled { opacity: .6; cursor: default }
|
|
1034
|
+
.cm-send { position: absolute; right: 5px; bottom: 5px; /* (38 - 28) / 2 - centered in the one-line pill */
|
|
1035
|
+
z-index: 2; /* above the textarea (z1, the @marver mirror stack) - or clicks land on the field, not the button */
|
|
765
1036
|
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 }
|
|
1037
|
+
background: var(--cm-solid, var(--comment)); color: #fff; cursor: pointer; transition: background .15s, opacity .15s }
|
|
767
1038
|
.cm-send:hover:not(:disabled) { filter: brightness(1.08) }
|
|
768
1039
|
.cm-send:disabled { background: var(--glass-ink-3); opacity: .5; cursor: default }
|
|
769
1040
|
/* signed-out viewer: one clear button in place of the composer */
|
|
770
1041
|
.cm-signin-cta { width: 100%; height: 40px; border: 0; border-radius: 999px;
|
|
771
|
-
background: var(--comment); color: #fff; cursor: pointer;
|
|
1042
|
+
background: var(--cm-solid, var(--comment)); color: #fff; cursor: pointer;
|
|
772
1043
|
font: 600 13px -apple-system, system-ui, sans-serif; transition: filter .15s }
|
|
773
1044
|
.cm-signin-cta:hover { filter: brightness(1.06) }
|
|
774
1045
|
/* the draft composer is JUST the pill - no card chrome around a single input */
|
|
775
1046
|
.cm-draft { padding: 0; width: 272px; background: none; border: none; box-shadow: none; outline: none;
|
|
776
1047
|
backdrop-filter: none; -webkit-backdrop-filter: none }
|
|
777
|
-
.cm-draft
|
|
778
|
-
.cm-draft
|
|
1048
|
+
.cm-draft textarea, .cm-draft .cm-hl { min-height: 40px; padding-top: 10px; padding-bottom: 10px } /* 1+10+18+10+1 = 40, same closed math */
|
|
1049
|
+
.cm-draft textarea { box-shadow: 0 2px 6px rgba(24,24,27,.15), 0 12px 32px -8px rgba(24,24,27,.35) }
|
|
1050
|
+
.cm-draft .cm-send { right: 6px; bottom: 6px } /* (40 - 28) / 2 for the taller draft pill */
|
|
1051
|
+
|
|
1052
|
+
/* Live Jam: @marver mention styling. Owner-authored = a live trigger (bold accent-blue);
|
|
1053
|
+
anyone else's = plain-weight, read like any comment, with a teaching tooltip (§1). */
|
|
1054
|
+
.cm-at { color: var(--glass-ink-2, var(--glass-ink)); font-weight: 500 }
|
|
1055
|
+
.cm-at.owner { color: var(--mv-blue); font-weight: 700 }
|
|
1056
|
+
/* IN THE COMPOSER MIRROR the mention may change COLOR + PAINT, never METRICS: real bold
|
|
1057
|
+
glyphs are WIDER than the invisible textarea's regular ones, so any font-weight change
|
|
1058
|
+
drifts the caret further off with every mention. The bold LOOK comes from a hairline
|
|
1059
|
+
text-stroke instead - it thickens the glyph paint without touching a single advance
|
|
1060
|
+
width, so the mirror stays pixel-locked to the textarea and the caret never lies. */
|
|
1061
|
+
.cm-hl .cm-at { font-weight: inherit; -webkit-text-stroke: .4px currentColor }
|
|
1062
|
+
.cm-at-tip { display: block; max-width: 220px; line-height: 1.4 }
|
|
1063
|
+
|
|
1064
|
+
/* Marver identity: the agent's avatar mark + provenance tooltip (§7). Always accent, never a hue. */
|
|
1065
|
+
.cm-avatar.cm-marver { background: var(--mv-blue); color: rgba(255,255,255,.9); cursor: default } /* white/90 mark, embedded in the accent */
|
|
1066
|
+
.cm-marver-wrap { display: inline-flex; border-radius: 50% }
|
|
1067
|
+
.cm-marver-name { color: var(--glass-ink) } /* black, not blue */
|
|
1068
|
+
/* provenance tooltip: matches the toolbar-button tooltip - a BOLD BRIGHT value (what you scan) + a
|
|
1069
|
+
muted qualifier label after it, one fact per row, left-aligned. Overrides the house single-row
|
|
1070
|
+
`.sh-tip span` dim/right-drift so nothing washes out. */
|
|
1071
|
+
/* two-row shortcut tooltip (toolbar): one action per row, key right-aligned - the same
|
|
1072
|
+
bold-value/muted-key language as the provenance tooltip below. opacity:1 overrides the
|
|
1073
|
+
house `.sh-tip span` dim so the wrapper rows stay full-bright; only keys mute. */
|
|
1074
|
+
.sh-tip:has(.sh-tip-rows) { padding: 7px 11px }
|
|
1075
|
+
.sh-tip .sh-tip-rows { display: flex; flex-direction: column; align-items: stretch; gap: 5px;
|
|
1076
|
+
opacity: 1; margin: 0; font-size: 12px; line-height: 1.2 }
|
|
1077
|
+
.sh-tip .sh-tip-rows .r { display: flex; align-items: baseline; justify-content: space-between;
|
|
1078
|
+
gap: 18px; opacity: 1; margin: 0; font-size: 12px; white-space: nowrap }
|
|
1079
|
+
.sh-tip .sh-tip-rows .r b { font-weight: 600 }
|
|
1080
|
+
.sh-tip .sh-tip-rows .r span { opacity: .55; font-size: 11px; font-weight: 600; margin-left: auto }
|
|
1081
|
+
|
|
1082
|
+
.sh-tip:has(.cm-meta-tip) { padding: 8px 12px }
|
|
1083
|
+
/* opacity:1 is the fix - .cm-meta-tip is a <span>, so the house `.sh-tip span{opacity:.55}` was
|
|
1084
|
+
dimming the ENTIRE tooltip, and a child's opacity:1 can't undo a parent's reduced opacity. */
|
|
1085
|
+
.sh-tip .cm-meta-tip { display: flex; flex-direction: column; align-items: flex-start; gap: 6px; line-height: 1.2; opacity: 1; font-size: 12.5px }
|
|
1086
|
+
.sh-tip .cm-meta-row { display: flex; gap: 8px; align-items: baseline; margin: 0; opacity: 1; font-size: 12.5px; white-space: nowrap }
|
|
1087
|
+
.sh-tip .cm-meta-row b { font-weight: 600; opacity: 1; margin: 0; color: var(--tip-ink) } /* value: bold, full-bright */
|
|
1088
|
+
.sh-tip .cm-meta-row .k { margin: 0; opacity: .5; font-weight: 500; color: var(--tip-ink) } /* label: muted qualifier */
|
|
779
1089
|
/* identity modal - same glass family */
|
|
780
1090
|
.cm-modal-wrap { position: fixed; inset: 0; z-index: 300; display: flex; align-items: center; justify-content: center;
|
|
781
1091
|
background: rgba(24,24,27,.45); backdrop-filter: blur(5px) }
|
|
@@ -801,11 +1111,14 @@ body.sh-kbd .sh-app :focus-visible { outline: 2px solid var(--accent); outline-o
|
|
|
801
1111
|
.cm-chip span { font-size: 10.5px; font-weight: 600; letter-spacing: .04em; color: var(--glass-ink-3); flex: none }
|
|
802
1112
|
.cm-idrow { display: flex; align-items: center; gap: 10px }
|
|
803
1113
|
.cm-pfp { width: 44px; height: 44px; border-radius: 50%; border: 1.5px dashed var(--glass-ink-3);
|
|
804
|
-
background: var(--cm-field)
|
|
1114
|
+
background: var(--cm-field); color: var(--glass-ink-3); flex: none; overflow: hidden;
|
|
805
1115
|
display: flex; align-items: center; justify-content: center; cursor: pointer; padding: 0;
|
|
806
1116
|
transition: border-color .15s, color .15s }
|
|
807
1117
|
.cm-pfp:hover { border-color: var(--comment); color: var(--comment) }
|
|
808
|
-
.cm-pfp.set { border-style: solid; border-color: var(--glass-brd)
|
|
1118
|
+
.cm-pfp.set { border-style: solid; border-color: var(--glass-brd) }
|
|
1119
|
+
/* the photo is a REAL img filling the clipped circle - background-image layering left
|
|
1120
|
+
uncovered edges */
|
|
1121
|
+
.cm-pfp img { width: 100%; height: 100%; object-fit: cover; border-radius: 50%; display: block }
|
|
809
1122
|
.cm-row { display: flex; gap: 8px; margin-top: 12px }
|
|
810
1123
|
.cm-row button:disabled, .cm-row button.cm-primary:disabled {
|
|
811
1124
|
background: var(--glass-ink-3); color: var(--glass); cursor: default; opacity: 1 }
|
|
@@ -818,6 +1131,19 @@ body.sh-kbd .sh-app :focus-visible { outline: 2px solid var(--accent); outline-o
|
|
|
818
1131
|
.cm-row button.cm-primary { background: var(--glass-ink); color: var(--glass); border-color: transparent; flex: 1 }
|
|
819
1132
|
.cm-row button.cm-primary:hover { opacity: .88 }
|
|
820
1133
|
|
|
1134
|
+
/* the dev identity door: your avatar in the composer opens the profile popover. Unset shows a
|
|
1135
|
+
quiet + badge - the whole first-comment nudge, no modal in the way. */
|
|
1136
|
+
.cm-me { position: relative; display: inline-flex; flex: none }
|
|
1137
|
+
.cm-mebtn { position: relative; display: inline-flex; padding: 0; border: 0; background: none;
|
|
1138
|
+
cursor: pointer; border-radius: 50% }
|
|
1139
|
+
/* hover morphs the avatar into a pen on quiet gray - "this is editable" without a badge */
|
|
1140
|
+
.cm-pen-face { position: absolute; inset: 0; border-radius: 50%; box-sizing: border-box;
|
|
1141
|
+
display: flex; align-items: center; justify-content: center;
|
|
1142
|
+
background: color-mix(in srgb, var(--glass-ink) 10%, var(--cm-field));
|
|
1143
|
+
border: 1px solid var(--glass-brd); color: var(--glass-ink-2);
|
|
1144
|
+
opacity: 0; transition: opacity .12s; pointer-events: none }
|
|
1145
|
+
.cm-mebtn:hover .cm-pen-face, .cm-mebtn:focus-visible .cm-pen-face { opacity: 1 }
|
|
1146
|
+
|
|
821
1147
|
/* the unclipped comment plane over each frame: itself transparent to the pointer,
|
|
822
1148
|
its children (pins, cards, stacks) fully interactive */
|
|
823
1149
|
.cm-layer { position: absolute; left: 0; width: 100%; pointer-events: none; z-index: 25 }
|
|
@@ -14,6 +14,7 @@ import { Component, createElement, useEffect, useRef, useState, type ComponentTy
|
|
|
14
14
|
import { flushSync } from 'react-dom'
|
|
15
15
|
import { createRoot } from 'react-dom/client'
|
|
16
16
|
import { frameFile, frames, layoutChain, layouts, providers } from '../frame-host/registry.ts'
|
|
17
|
+
import { createInspect } from '../frame-host/inspect.js'
|
|
17
18
|
|
|
18
19
|
const params = new URLSearchParams(location.search)
|
|
19
20
|
// Both signals, always - same pair as the frame host: [data-theme] for attribute-keyed
|
|
@@ -24,7 +25,9 @@ document.documentElement.dataset.theme = bootTheme
|
|
|
24
25
|
document.documentElement.classList.toggle('dark', bootTheme === 'dark')
|
|
25
26
|
const startId = params.get('at') ?? ''
|
|
26
27
|
|
|
27
|
-
|
|
28
|
+
// same-origin parent (the shell): fixed target origin so anchor bundles never leak if a
|
|
29
|
+
// data-goto ever navigates the stage cross-origin
|
|
30
|
+
const post = (msg: Record<string, unknown>) => { if (window.parent !== window) window.parent.postMessage(msg, location.origin) }
|
|
28
31
|
|
|
29
32
|
window.addEventListener('error', (e) => post({ type: 'sh:stage-error', message: String(e.message || e.error) }))
|
|
30
33
|
window.addEventListener('unhandledrejection', (e) => post({ type: 'sh:stage-error', message: `unhandled rejection: ${e.reason}` }))
|
|
@@ -77,6 +80,7 @@ function Stage() {
|
|
|
77
80
|
const [err, setErr] = useState<{ id: string; message: string } | null>(null)
|
|
78
81
|
const current = useRef(startId)
|
|
79
82
|
const swapSeq = useRef(0)
|
|
83
|
+
const readySent = useRef(false)
|
|
80
84
|
|
|
81
85
|
/** Swap to a frame. `announce` posts sh:stage-at (user-driven); history restores stay silent. */
|
|
82
86
|
const goto = async (id: string, announce: boolean) => {
|
|
@@ -85,10 +89,13 @@ function Stage() {
|
|
|
85
89
|
try {
|
|
86
90
|
const next = await resolve(id)
|
|
87
91
|
if (seq !== swapSeq.current) return // a newer swap superseded this one
|
|
88
|
-
current.current = id
|
|
89
92
|
// startViewTransition runs its callback async - recheck the seq there too, or an
|
|
90
|
-
// older pending transition could commit stale state over a newer navigation
|
|
91
|
-
|
|
93
|
+
// older pending transition could commit stale state over a newer navigation.
|
|
94
|
+
// current.current is advanced INSIDE apply (once the new DOM is committed), so
|
|
95
|
+
// inspect.getId() never reports the new frame while the old DOM is still live -
|
|
96
|
+
// a pick / anchor-resolve landing mid-swap then carries the old id and the shell
|
|
97
|
+
// guards drop it instead of stamping it onto the wrong frame.
|
|
98
|
+
const apply = () => { if (seq === swapSeq.current) { flushSync(() => { setErr(null); setMounted(next) }); current.current = id } }
|
|
92
99
|
if (document.startViewTransition) document.startViewTransition(apply)
|
|
93
100
|
else { apply(); document.getElementById('root')?.animate([{ opacity: 0.35 }, { opacity: 1 }], { duration: 180, easing: 'ease-out' }) }
|
|
94
101
|
if (announce) post({ type: 'sh:stage-at', at: id })
|
|
@@ -101,13 +108,27 @@ function Stage() {
|
|
|
101
108
|
}
|
|
102
109
|
}
|
|
103
110
|
|
|
111
|
+
// sh:stage-ready fires ONCE, after the first frame actually commits to the DOM (not when
|
|
112
|
+
// goto is merely scheduled) - the shell re-drives the review highlight on it, which must
|
|
113
|
+
// resolve against a live document
|
|
114
|
+
useEffect(() => {
|
|
115
|
+
if (mounted && !readySent.current) { readySent.current = true; post({ type: 'sh:stage-ready', at: current.current }) }
|
|
116
|
+
}, [mounted])
|
|
117
|
+
|
|
104
118
|
useEffect(() => {
|
|
105
119
|
goto(startId, false)
|
|
106
|
-
|
|
120
|
+
|
|
121
|
+
// laser / comment-pick / anchor / highlight - the SAME controller the canvas
|
|
122
|
+
// frames run, so review works identically in the prototype. getId reports the
|
|
123
|
+
// CURRENT frame (the stage swaps in place). It self-installs its own listeners;
|
|
124
|
+
// we only borrow modeActive() to keep a review click from ALSO navigating.
|
|
125
|
+
const inspect = createInspect({ post, getId: () => current.current })
|
|
107
126
|
|
|
108
127
|
// data-goto is handled HERE, in place - never posted up as sh:go (capture phase
|
|
109
|
-
// beats any frame handler; preventDefault stops real <a href> navigations)
|
|
128
|
+
// beats any frame handler; preventDefault stops real <a href> navigations). A
|
|
129
|
+
// laser/comment click owns the press instead - it must not navigate.
|
|
110
130
|
const onClick = (e: MouseEvent) => {
|
|
131
|
+
if (inspect.modeActive()) return
|
|
111
132
|
const el = e.target instanceof Element ? e.target.closest('[data-goto]') : null
|
|
112
133
|
if (!el) return
|
|
113
134
|
e.preventDefault()
|
|
@@ -118,19 +139,21 @@ function Stage() {
|
|
|
118
139
|
document.addEventListener('click', onClick, true)
|
|
119
140
|
|
|
120
141
|
const onKey = (e: KeyboardEvent) => {
|
|
121
|
-
// Escape always exits, even mid-typing (matches the canvas bridge)
|
|
122
|
-
if (e.key === 'Escape') { post({ type: 'sh:stage-exit' }); return }
|
|
123
142
|
if ((e.metaKey || e.ctrlKey) && e.key === '/') { e.preventDefault(); post({ type: 'sh:stage-key', key: '/', code: e.code, meta: true }); return }
|
|
124
143
|
if (e.metaKey || e.ctrlKey) return // ⌘D is the browser's bookmark, not our theme
|
|
144
|
+
// Escape always reaches the shell, even mid-typing: it decides (cancel a
|
|
145
|
+
// comment/laser first, else exit) - the stage can't see review mode
|
|
146
|
+
if (e.key === 'Escape') { post({ type: 'sh:stage-key', key: 'Escape', code: e.code }); return }
|
|
125
147
|
if (e.target instanceof HTMLInputElement || e.target instanceof HTMLTextAreaElement) return
|
|
126
|
-
// every play shortcut belongs to the shell (it owns walk order + chrome) - forward
|
|
127
|
-
|
|
148
|
+
// every play shortcut belongs to the shell (it owns walk order + chrome) - forward.
|
|
149
|
+
// l/c toggle laser/comment, C (shift+c) hides pins; the shell acts and broadcasts back.
|
|
150
|
+
if (/^Digit[0-9]$/.test(e.code) || ['d', 'h', 'r', 'l', 'c', 'C', '[', ']', 'ArrowRight', 'ArrowLeft'].includes(e.key))
|
|
128
151
|
post({ type: 'sh:stage-key', key: e.key, code: e.code })
|
|
129
152
|
}
|
|
130
153
|
window.addEventListener('keydown', onKey)
|
|
131
154
|
|
|
132
155
|
const onMsg = (e: MessageEvent) => {
|
|
133
|
-
if (e.source !== window.parent) return
|
|
156
|
+
if (e.source !== window.parent || (e.origin && e.origin !== location.origin)) return
|
|
134
157
|
const data = e.data
|
|
135
158
|
if (data?.type === 'sh:set-theme') {
|
|
136
159
|
document.documentElement.dataset.theme = data.theme
|
|
@@ -144,6 +167,7 @@ function Stage() {
|
|
|
144
167
|
document.removeEventListener('click', onClick, true)
|
|
145
168
|
window.removeEventListener('keydown', onKey)
|
|
146
169
|
window.removeEventListener('message', onMsg)
|
|
170
|
+
inspect.dispose()
|
|
147
171
|
}
|
|
148
172
|
}, [])
|
|
149
173
|
|