@marver-design/marver 0.7.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.
Files changed (34) hide show
  1. package/CHANGELOG.md +46 -0
  2. package/dist/{build-BZaPa2DS.mjs → build-B8z902TW.mjs} +2 -2
  3. package/dist/cli.mjs +5 -5
  4. package/dist/{collab-CXy8gqoz.mjs → collab-pLGzSvm5.mjs} +16 -5
  5. package/dist/{comments-odHzYdO3.mjs → comments-BrpC86Kf.mjs} +9 -15
  6. package/dist/{comments-Ba8mU600.mjs → comments-DSwMhdbZ.mjs} +2 -8
  7. package/dist/daemon-C-huGHAM.mjs +899 -0
  8. package/dist/{dev-DaPQ9xA5.mjs → dev-BYm9ebfN.mjs} +14 -3
  9. package/dist/events-BMtBvvgU.mjs +101 -0
  10. package/dist/{init-DsCUmlCW.mjs → init-DrLomSWq.mjs} +13 -1
  11. package/dist/ledger-wFvEIEGi.mjs +64 -0
  12. package/dist/{manifest-C8FODq2S.mjs → manifest-D3eaARf4.mjs} +16 -1
  13. package/dist/{plugin-wMY9lNf3.mjs → plugin-BVFuRfEo.mjs} +56 -40
  14. package/dist/profile-BkiWglVE.mjs +39 -0
  15. package/dist/{serve-D_KBK7Oy.mjs → serve-CwAfayJk.mjs} +3 -3
  16. package/dist/{sync-CkBk-tUk.mjs → sync-Ch4Bymb1.mjs} +4 -102
  17. package/package.json +1 -1
  18. package/src/client/frame-host/inspect.js +15 -2
  19. package/src/client/shell/App.tsx +91 -9
  20. package/src/client/shell/Comments.tsx +418 -68
  21. package/src/client/shell/Play.tsx +10 -6
  22. package/src/client/shell/Toolbar.tsx +14 -2
  23. package/src/client/shell/canvas/FrameNode.tsx +33 -6
  24. package/src/client/shell/comments-store.ts +47 -6
  25. package/src/client/shell/icons.tsx +4 -0
  26. package/src/client/shell/mentions.ts +18 -0
  27. package/src/client/shell/store.ts +30 -2
  28. package/src/client/shell/styles.css +294 -21
  29. package/src/shared/events.ts +22 -5
  30. package/templates/AGENTS-embedded.md +1 -0
  31. package/templates/AGENTS-studio.md +1 -0
  32. package/templates/instructions/configure.md +15 -0
  33. package/templates/instructions/jam.md +85 -0
  34. package/dist/rolldown-runtime-D7D4PA-g.mjs +0 -13
@@ -52,6 +52,9 @@ const DEPTH_HUE = (() => {
52
52
 
53
53
  export function createInspect({ post, getId, onModeChange }) {
54
54
  let laserOn = false, pickOn = false
55
+ // quiet pick (⇧L laser comment off): clicks still anchor comments, but NO hover outline,
56
+ // label, or lock lighting inside the artwork - the shell decides, the frame obeys
57
+ let pickQuiet = false
55
58
  let hoverEl = null, labelEl = null
56
59
  let lockedEl = null, highlightAnchor = null // the persistent outline (#4 lock / #5 open)
57
60
  let copiedTimer = null, copySeq = 0, copyEl = null
@@ -172,11 +175,19 @@ body [data-mv-hover] { outline: 2px solid hsl(var(--mv-hue) 95% 45%); outline-of
172
175
  label.style.top = y + 'px'
173
176
  }
174
177
 
178
+ // the pointer LEAVING this frame takes its hover lighting with it: each frame is its own
179
+ // document, so once the cursor is gone no mousemove ever fires here again - without this,
180
+ // the last hovered outline + label fossilize on every frame the cursor crossed.
181
+ // (relatedTarget null = left the window/iframe, not just moved between elements)
182
+ on(document, 'mouseout', (e) => { if (!e.relatedTarget) clearHover() })
183
+ on(window, 'blur', clearHover)
184
+
175
185
  // hover-follow: the label chases the cursor while a mode is on - BUT a locked
176
186
  // element (#4 composing / #5 thread open) freezes it: the lit element is the one
177
187
  // being discussed, not whatever the pointer grazes.
178
188
  on(document, 'mousemove', (e) => {
179
189
  if (!modeActive() || lockedEl) return
190
+ if (pickOn && pickQuiet) return // quiet pick: no hover visuals, clicks still land
180
191
  const el = e.target instanceof Element && e.target.closest('body *:not(#mv-laser-label)')
181
192
  if (!el || el === hoverEl) { if (labelEl && hoverEl) place(labelEl, e); return }
182
193
  clearHover()
@@ -245,7 +256,8 @@ body [data-mv-hover] { outline: 2px solid hsl(var(--mv-hue) 95% 45%); outline-of
245
256
  if (pickOn) {
246
257
  // #4: lock the chosen element the instant it is picked - no round-trip flicker.
247
258
  // The shell re-confirms via sh:highlight-anchor once the draft is staged.
248
- applyLock(el, hueOf(el))
259
+ // Quiet pick (⇧L off) anchors WITHOUT lighting the element.
260
+ if (!pickQuiet) applyLock(el, hueOf(el))
249
261
  clearHover()
250
262
  post({ type: 'sh:picked', id: getId(), anchor: anchorBundle(el, e) })
251
263
  } else {
@@ -299,7 +311,8 @@ body [data-mv-hover] { outline: 2px solid hsl(var(--mv-hue) 95% 45%); outline-of
299
311
  if (!m || typeof m !== 'object') return
300
312
  if (m.type === 'sh:laser') { laserOn = !!m.on; applyModes(); onModeChange?.(laserOn, pickOn) }
301
313
  else if (m.type === 'sh:pick') {
302
- pickOn = !!m.on; applyModes()
314
+ pickOn = !!m.on; pickQuiet = !!m.quiet; applyModes()
315
+ if (pickQuiet) clearHover() // a hover lit before the toggle landed must not linger
303
316
  // leaving comment mode drops a provisional self-lock the shell never confirmed;
304
317
  // a shell-owned highlight (highlightAnchor set, e.g. #5) survives
305
318
  if (!pickOn && !highlightAnchor) clearLock()
@@ -6,8 +6,8 @@ import { animateLayout, Canvas, canvasCtl } from './canvas/Canvas.tsx'
6
6
  import { frameByWindow } from './canvas/frame-registry.ts'
7
7
  import { enterPlay, playCtl, PlayOverlay } from './Play.tsx'
8
8
  import { bootHash, parseHash, writeHash } from './hash.ts'
9
- import { CardsIcon, CardsThreeIcon, CaretIcon, CheckIcon, ColumnsIcon, FrameRectIcon, IntentGlyph, MoonIcon, PanelFilledIcon, PanelHollowIcon, ParallelogramDuoIcon, PlayIcon, PlusIcon, SignpostIcon, SunIcon, VariantsIcon, XIcon, deviceIcon } from './icons.tsx'
10
- import { CommentsController } from './Comments.tsx'
9
+ import { CardsIcon, CardsThreeIcon, CaretIcon, CheckIcon, ColumnsIcon, FrameRectIcon, IntentGlyph, MoonIcon, PanelFilledIcon, PanelHollowIcon, ParallelogramDuoIcon, ParallelogramFillIcon, PlayIcon, PlusIcon, SignpostIcon, SunIcon, VariantsIcon, XIcon, deviceIcon } from './icons.tsx'
10
+ import { CommentsController, revealThread } from './Comments.tsx'
11
11
  import { useComments } from './comments-store.ts'
12
12
  import { CommentButton, DevicePicker, HideUIButton, LaserButton, Popover, ThemePicker, toggleHideUI, usePopover } from './Toolbar.tsx'
13
13
 
@@ -442,6 +442,12 @@ export function App() {
442
442
  useEffect(() => {
443
443
  if (!import.meta.hot) return
444
444
  import.meta.hot.on('sh:manifest', (m: any) => applyManifest(m))
445
+ // Live Jam presence (SPEC §10): the daemon broadcasts the set of frames Marver is editing.
446
+ // Camera-safe by construction - this only toggles a glow class, never moves the view.
447
+ import.meta.hot.on('sh:jam-activity', (m: any) => useStore.getState().setWorking(Array.isArray(m?.frames) ? m.frames.filter((x: unknown) => typeof x === 'string') : []))
448
+ // Live Jam reply delivery: the daemon just wrote to a board's log - fetch it NOW instead of
449
+ // waiting out the 30s comment poll, so the reply + notification land within a second.
450
+ import.meta.hot.on('sh:jam-comment', (m: any) => { if (typeof m?.board === 'string') useComments.getState().poke(m.board) })
445
451
  // A7 controlled HMR: a frame file changed. Reload exactly the affected frames through the
446
452
  // lease-aware path (idle frames now, leased ones deferred to a safe point) - never a shell
447
453
  // reload, never a React Fast Refresh yanking a frame the user is in.
@@ -593,6 +599,14 @@ export function App() {
593
599
  toast(c.commentMode ? 'comment mode off' : 'comment mode - click an element in a frame')
594
600
  }
595
601
  if (e.key === 'C' && e.shiftKey) { const c = commentsStore(); c.setShow(!c.show) }
602
+ // Shift+L = laser comment: the laser-sharp lighting on the element a comment tags
603
+ // (pick hover, compose lock, open-thread highlight). Pins and cards stay - this
604
+ // only dims the lighting inside the artwork.
605
+ if (e.key === 'L' && e.shiftKey) {
606
+ const c = commentsStore()
607
+ c.setShowAnchor(!c.showAnchor)
608
+ toast(c.showAnchor ? 'laser comment off' : 'laser comment on')
609
+ }
596
610
  if (e.key === 'P' && e.shiftKey && s.selection.length) {
597
611
  const files = s.selection
598
612
  .map((k) => { const n = s.nodes.find((x) => x.key === k); return n ? s.frameFor(n)?.file : undefined })
@@ -794,22 +808,22 @@ export function App() {
794
808
 
795
809
  {/* floating pill nav, top right; collapses to a chip (same ladder as panel/fab) */}
796
810
  <nav className={`sh-pill${pillOpen ? '' : ' closed'}`} aria-hidden={!pillOpen}>
797
- {/* far-left section: actions - comment mode, laser, tidy, prototype */}
811
+ {/* far-left section: actions - comment mode, laser, tidy */}
798
812
  <CommentButton />
799
813
  <LaserButton />
800
814
  <Tip side="bottom" label={<><b>Tidy layout</b><span>T</span></>}>
801
815
  <button className="sh-pill-btn" onClick={() => { animateLayout(); runTidy() }}><ColumnsIcon size={16} /></button>
802
816
  </Tip>
803
- <Tip side="bottom" label={<><b>Prototype view</b><span>P</span></>}>
804
- <button className="sh-pill-btn" onClick={() => enterPlay()}><PlayIcon size={15} /></button>
805
- </Tip>
806
817
  <i className="sep" />
807
818
  <DeviceMenu />
808
819
  <ThemeMenu />
809
820
  <i className="sep" />
810
821
  <ZoomMenu />
811
822
  <i className="sep" />
812
- {/* far right: UI management only - hide, collapse */}
823
+ {/* far right: view management - prototype, hide, collapse */}
824
+ <Tip side="bottom" label={<><b>Prototype view</b><span>P</span></>}>
825
+ <button className="sh-pill-btn" onClick={() => enterPlay()}><PlayIcon size={15} /></button>
826
+ </Tip>
813
827
  <HideUIButton />
814
828
  <Tip side="bottom" label={<><b>Collapse toolbar</b><span>⌘/</span></>}>
815
829
  <button className="sh-pill-btn" onClick={() => setPillOpen(false)} tabIndex={pillOpen ? 0 : -1}>
@@ -830,9 +844,77 @@ export function App() {
830
844
  : CONFIG.noTheme && <div className="sh-banner">no theme configured - frames render unstyled. Create design/theme.css importing your app's stylesheet (or set theme in design/config.ts)</div>}
831
845
  <UpdatePill />
832
846
 
833
- <div className="sh-toasts">
834
- {toasts.map((t) => <div key={t.id} className="sh-toast"><CheckIcon size={12} /> {t.text}</div>)}
847
+ <JamToasts toasts={toasts} />
848
+ </div>
849
+ )
850
+ }
851
+
852
+ /** The bottom-right notification corner (SPEC-live-jam §9). Plain toasts render as before.
853
+ * Jam pills are FRAME-FIRST (icon + frame title, then Marver · preview) and stack as a DECK:
854
+ * 1-2 show in full; 3+ collapse to the newest pill with two card edges peeking beneath and a
855
+ * +N badge - click to expand the full list (newest first, timestamps, Clear all). */
856
+ function JamToasts({ toasts }: { toasts: import('./store.ts').Toast[] }) {
857
+ const [expanded, setExpanded] = useState(false)
858
+ const plain = toasts.filter((t) => !t.jam)
859
+ const jams = toasts.filter((t) => t.jam)
860
+ useEffect(() => { if (jams.length <= 2) setExpanded(false) }, [jams.length])
861
+ const deck = jams.length > 2 && !expanded
862
+ const newest = jams[jams.length - 1]
863
+ return (
864
+ <div className="sh-toasts">
865
+ {plain.slice(-2).map((t) => <div key={t.id} className="sh-toast"><CheckIcon size={12} /> {t.text}</div>)}
866
+ {expanded && jams.length > 2 && (
867
+ <div className="sh-jam-list">
868
+ <div className="sh-jam-listhead">
869
+ <span>{jams.length} NOTIFICATIONS</span>
870
+ <button onClick={() => { useStore.getState().clearJamToasts(); setExpanded(false) }}>Clear all</button>
871
+ <button aria-label="Collapse" onClick={() => setExpanded(false)}><XIcon size={12} /></button>
872
+ </div>
873
+ {[...jams].reverse().map((t) => <JamToast key={t.id} id={t.id} note={t.jam!} />)}
874
+ </div>
875
+ )}
876
+ {!expanded && (deck
877
+ ? (
878
+ <div className="sh-jam-deck" onClick={() => setExpanded(true)} role="button" aria-label={`${jams.length} notifications - expand`}>
879
+ <div className="sh-jam-ghost g2" />
880
+ <div className="sh-jam-ghost g1" />
881
+ {newest?.jam && <JamToast id={newest.id} note={newest.jam} badge={jams.length - 1} inert />}
882
+ </div>
883
+ )
884
+ : jams.map((t) => <JamToast key={t.id} id={t.id} note={t.jam!} />))}
885
+ </div>
886
+ )
887
+ }
888
+
889
+ /** View from a notification: switch to the note's board first when needed (the thread only exists
890
+ * in that board's state), then reveal - retrying briefly while the board's comments load. Dismiss
891
+ * only on success: a cross-board note must never be a destructive no-op. */
892
+ async function viewNote(note: import('./store.ts').JamNote, dismiss: () => void) {
893
+ const s = useStore.getState()
894
+ if (note.board && s.board !== note.board) await s.switchBoard(note.board)
895
+ for (let i = 0; i < 12; i++) {
896
+ if (revealThread(note.threadId)) { dismiss(); return }
897
+ await new Promise((r) => setTimeout(r, 250))
898
+ }
899
+ }
900
+
901
+ /** One jam pill, frame-first: row 1 = intent icon + FRAME TITLE (blue) · age; row 2 = Marver · preview.
902
+ * `badge` shows the +N deck count (replacing View); `inert` disables inner clicks (the deck handles it). */
903
+ function JamToast({ id, note, badge, inert }: { id: number; note: import('./store.ts').JamNote; badge?: number; inert?: boolean }) {
904
+ const dismiss = () => useStore.getState().dismissToast(id)
905
+ return (
906
+ <div className="sh-toast jam">
907
+ <span className="sh-jam-mark"><ParallelogramFillIcon size={17} /></span>
908
+ <div className="sh-jam-txt">
909
+ <b className="sh-jam-frame"><span className="t">{note.frameTitle ?? note.board}</span></b>
910
+ <span className="sh-jam-prev">{note.preview}</span>
835
911
  </div>
912
+ {badge != null
913
+ ? <span className="sh-jam-badge">+{badge}</span>
914
+ : !inert && <>
915
+ <button className="sh-jam-view" onClick={() => void viewNote(note, dismiss)}>View</button>
916
+ <button className="sh-jam-x" aria-label="Dismiss" onClick={dismiss}><XIcon size={13} /></button>
917
+ </>}
836
918
  </div>
837
919
  )
838
920
  }