@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.
- package/CHANGELOG.md +46 -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/inspect.js +15 -2
- package/src/client/shell/App.tsx +91 -9
- package/src/client/shell/Comments.tsx +418 -68
- package/src/client/shell/Play.tsx +10 -6
- package/src/client/shell/Toolbar.tsx +14 -2
- package/src/client/shell/canvas/FrameNode.tsx +33 -6
- package/src/client/shell/comments-store.ts +47 -6
- package/src/client/shell/icons.tsx +4 -0
- package/src/client/shell/mentions.ts +18 -0
- package/src/client/shell/store.ts +30 -2
- package/src/client/shell/styles.css +294 -21
- 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
|
@@ -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
|
-
|
|
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()
|
package/src/client/shell/App.tsx
CHANGED
|
@@ -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
|
|
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:
|
|
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
|
-
<
|
|
834
|
-
|
|
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
|
}
|