@marver-design/marver 0.19.0 → 0.19.1
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 +13 -0
- package/README.md +1 -1
- package/docs/sticky-notes.md +6 -0
- package/package.json +1 -1
- package/src/client/shell/notes.ts +30 -0
- package/src/client/shell/store.ts +31 -8
- package/templates/AGENTS-embedded.md +3 -0
- package/templates/AGENTS-studio.md +3 -0
- package/templates/instructions/shape.md +7 -0
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,19 @@
|
|
|
2
2
|
|
|
3
3
|
Notable changes to `@marver-design/marver`. Format follows [Keep a Changelog](https://keepachangelog.com); versions follow semver.
|
|
4
4
|
|
|
5
|
+
## 0.19.1 - 2026-09-08
|
|
6
|
+
|
|
7
|
+
### Fixed
|
|
8
|
+
|
|
9
|
+
- **A note landing on a composed board makes its own room.** 0.19.0 reserved a note's width only
|
|
10
|
+
when tidy ran; a note file added to a board that already had saved positions stood on its left
|
|
11
|
+
neighbour until someone pressed `t`. A board with a recipe (or the auto board) now re-applies
|
|
12
|
+
its layout when a note has no room - as the file lands while the board is open, and at load
|
|
13
|
+
when it landed while the board was closed - and saves the result. Boards dragged by hand keep
|
|
14
|
+
their positions, as before. A deleted frame's card counts as an obstacle; a drag inside the
|
|
15
|
+
reflow's debounce is never overwritten. The agent instructions say it plainly: placement is
|
|
16
|
+
the canvas's job, write the file.
|
|
17
|
+
|
|
5
18
|
## 0.19.0 - 2026-09-08
|
|
6
19
|
|
|
7
20
|
### Added
|
package/README.md
CHANGED
|
@@ -45,7 +45,7 @@ Frames appear on the canvas the moment the files land. That's the loop.
|
|
|
45
45
|
- **Prototype links.** `data-goto="scene/frame"` on any element links frames into a walkable prototype - across boards, too.
|
|
46
46
|
- **Five ways to view a board.** The canvas (frames on a plane), the board (the same, tidy), **present** (`p`: a full-screen clickable walkthrough - `data-goto` navigates, arrows step, `[` / `]` cycle variants, laser, comments, theme and device pickers in the toolbar), **focus** (one frame as a document - the reading preset for specs), and **slides** (a deck). A published board names its landing view; a frame deep link opens straight into it.
|
|
47
47
|
- **Content frames.** Specs, Mermaid diagrams, mood boards, and slides live on the same canvas as the screens - import `Doc`, `Md`, `Diagram`, `Img`, `Slide`, `Chart`, `Video` from `@marver-design/marver/content` and think a feature through before any pixels exist. Works in a repo with no app at all: idea first, design second.
|
|
48
|
-
- **Sticky notes.** A markdown file beside a frame (`cart.note.md`) or a scene (`_note.md`) becomes a yellow note left of the frame on the canvas - what it is for, how two variations differ, an open question. Markdown, `goto:` links to frames, hand-drawn Mermaid; readers comment on a note's text like on a frame's, fold it to its corner, hide them all with `n`.
|
|
48
|
+
- **Sticky notes.** A markdown file beside a frame (`cart.note.md`) or a scene (`_note.md`) becomes a yellow note left of the frame on the canvas - what it is for, how two variations differ, an open question. Markdown, `goto:` links to frames, hand-drawn Mermaid; readers comment on a note's text like on a frame's, fold it to its corner, hide them all with `n`. The layout makes room for it; published canvases carry them.
|
|
49
49
|
- **Hi-fi at rest.** A frame at rest is its own live document, asleep: animations paused and every `backdrop-filter` element painted with a certified texture of its filtered backdrop (compiled by the dev server's headless Chrome), so a board of 30 glass screens pans like 30 statics and wakes pixel-identical on interact. Glass inside glass, blend modes and frames whose paint depends on random data or the clock stay live; `?awake=1` keeps every frame live for comparison.
|
|
50
50
|
- **Charts and video in any frame.** `Chart` (Apache ECharts, SVG, still at rest) inherits the ink, typeface and accent of whatever frame it sits in - a Tailwind dashboard, a dark spec, a slide - sizes its type to the context and follows the layout on resize. `Video` is poster-first everywhere: click to play wherever the frame is live, `autoplay` for an ambient loop, `ratio` for vertical clips; omit the poster and marver renders one from the clip. A screen with a chart or a clip is still a screen.
|
|
51
51
|
- **Copy as image.** Select a frame, press `i` - a 2x PNG of it lands on the clipboard, rendered by the same headless Chrome that serves `marver shot`; `⇧i` for 4x (a slide is 5120×2880). Paste into Slack, a doc, or a chat with your agent.
|
package/docs/sticky-notes.md
CHANGED
|
@@ -20,6 +20,12 @@ links jump to a frame; `http(s)` links open a new tab; images come from `design/
|
|
|
20
20
|
inert. A scene note is 380 wide, a frame note 260; when a frame has both, they stack in one column,
|
|
21
21
|
scene first. An edit lands on the canvas as you save, without reloading the frame.
|
|
22
22
|
|
|
23
|
+
The room is the layout's job. Every layout the shell composes (a board's `layout` recipe, the auto
|
|
24
|
+
board, tidy, device views) reserves the note's width in front of its frame, and a note landing on a
|
|
25
|
+
board already composed re-applies the recipe so the frames make way - whether the board is open at
|
|
26
|
+
the time or not. Nobody moves frames for a note. A board dragged by hand keeps its positions; `t`
|
|
27
|
+
makes the room there.
|
|
28
|
+
|
|
23
29
|
## Diagrams
|
|
24
30
|
|
|
25
31
|
A ```mermaid fence renders hand-sketched on the paper: rough boxes, hatched fills, handwriting
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@marver-design/marver",
|
|
3
|
-
"version": "0.19.
|
|
3
|
+
"version": "0.19.1",
|
|
4
4
|
"description": "The agent-native design canvas. A design/ folder, one command, a canvas of live frames built from your repo's real components - comment @marver and your own coding agent does the work. The tool ships no AI.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"private": false,
|
|
@@ -40,6 +40,36 @@ export function sceneNoteHost(
|
|
|
40
40
|
return best ? (best as { key: string }).key : null
|
|
41
41
|
}
|
|
42
42
|
|
|
43
|
+
/** The node header the canvas draws above a frame body (FrameNode's HEADER) - a node's card is
|
|
44
|
+
* `h + NODE_HEADER` tall, and that is the height a note beside it has to clear. */
|
|
45
|
+
export const NODE_HEADER = 28
|
|
46
|
+
|
|
47
|
+
/** True when a note has no room: another node's card stands inside the reserve in front of a
|
|
48
|
+
* noted node (its own note, or the scene's note it hosts). Room is the layout's job, never the
|
|
49
|
+
* author's - a board the shell composes re-applies its layout when this is true, so a note file
|
|
50
|
+
* can land on a saved board and the frames make way. Missing nodes (a deleted frame's card, still
|
|
51
|
+
* drawn full size) block room but never host a note. */
|
|
52
|
+
export function notesCramped(
|
|
53
|
+
nodes: readonly { key: string; frame: string; x: number; y: number; w: number; h: number; missing?: boolean }[],
|
|
54
|
+
manifest: { frames: { id: string; scene: string; note?: string }[]; scenes: { name: string; note?: string }[] } | null,
|
|
55
|
+
): boolean {
|
|
56
|
+
if (!manifest) return false
|
|
57
|
+
const entry = (id: string) => manifest.frames.find((f) => f.id === id)
|
|
58
|
+
const live = nodes.filter((n) => !n.missing)
|
|
59
|
+
const hosts = new Set<string>()
|
|
60
|
+
for (const s of manifest.scenes) {
|
|
61
|
+
if (!s.note) continue
|
|
62
|
+
const h = sceneNoteHost(live, (id) => entry(id)?.scene, s.name)
|
|
63
|
+
if (h) hosts.add(h)
|
|
64
|
+
}
|
|
65
|
+
return live.some((n) => {
|
|
66
|
+
const r = noteReserve(!!entry(n.frame)?.note, hosts.has(n.key))
|
|
67
|
+
if (!r) return false
|
|
68
|
+
const x0 = n.x - r, y1 = n.y + n.h + NODE_HEADER
|
|
69
|
+
return nodes.some((o) => o !== n && o.x < n.x && o.x + o.w > x0 && o.y < y1 && o.y + o.h + NODE_HEADER > n.y)
|
|
70
|
+
})
|
|
71
|
+
}
|
|
72
|
+
|
|
43
73
|
// ---- per-viewer visibility -------------------------------------------------------------
|
|
44
74
|
|
|
45
75
|
const STORAGE = 'mv-notes'
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { create } from 'zustand'
|
|
2
2
|
import { ROUTE, slideSize } from '../const.ts'
|
|
3
3
|
import { tidy, parseLayout, type BoardLayout, type TidyNode } from './tidy.ts'
|
|
4
|
-
import { noteReserve } from './notes.ts'
|
|
4
|
+
import { noteReserve, notesCramped } from './notes.ts'
|
|
5
5
|
import { stableNodeKey } from './keys.ts'
|
|
6
6
|
// @ts-expect-error virtual module provided by the plugin
|
|
7
7
|
import shConfig from 'virtual:sh-config'
|
|
@@ -411,17 +411,32 @@ export const useStore = create<State>((set, get) => {
|
|
|
411
411
|
// One cancelable, BOARD-SCOPED reflow after content measurements settle.
|
|
412
412
|
// The captured board name is the generation guard - a debounce surviving a board
|
|
413
413
|
// switch fires into a name check and dies, never touching the new board.
|
|
414
|
+
// `onlyIf` (a note asking for room) is re-judged when the timer fires - a drag or a restore
|
|
415
|
+
// in the meantime may have settled it - and never downgrades an unconditional reflow pending.
|
|
414
416
|
let reflowTimer: ReturnType<typeof setTimeout> | undefined
|
|
415
|
-
|
|
417
|
+
let reflowCheck: (() => boolean) | null = null
|
|
418
|
+
const scheduleReflow = (onlyIf?: () => boolean) => {
|
|
416
419
|
const boardAt = get().board
|
|
420
|
+
reflowCheck = reflowTimer !== undefined && reflowCheck === null ? null : (onlyIf ?? null)
|
|
417
421
|
clearTimeout(reflowTimer)
|
|
418
422
|
reflowTimer = setTimeout(() => {
|
|
423
|
+
reflowTimer = undefined
|
|
424
|
+
const check = reflowCheck
|
|
425
|
+
reflowCheck = null
|
|
419
426
|
const s = get()
|
|
420
427
|
if (s.board !== boardAt) return
|
|
421
|
-
if (s.gesture) { scheduleReflow(); return } // defer, never drop - retries after the drag
|
|
422
|
-
if (
|
|
428
|
+
if (s.gesture) { scheduleReflow(check ?? undefined); return } // defer, never drop - retries after the drag
|
|
429
|
+
if (check && !check()) return
|
|
430
|
+
if (composed(s)) s.runTidy()
|
|
423
431
|
}, 400)
|
|
424
432
|
}
|
|
433
|
+
/** Boards whose layout the shell owns: a recipe, scene rows, or the auto board. Room for a
|
|
434
|
+
* note is made here; a board the human placed by hand is never moved (spec 18, 0.19.1). */
|
|
435
|
+
const composed = (s: { layout: BoardLayout | null; sceneRows: string[][] | null; boardAuto: boolean }) => !!(s.layout || s.sceneRows?.length || s.boardAuto)
|
|
436
|
+
const cramped = () => { const s = get(); return !!s.manifest && notesCramped(s.nodes, s.manifest) }
|
|
437
|
+
/** A note may have landed with no room (a frame note via the manifest, a scene note via
|
|
438
|
+
* sh:scenes, either merged late by boot/switch): a composed board re-applies its layout. */
|
|
439
|
+
const roomForNotes = () => { if (composed(get()) && cramped()) scheduleReflow(cramped) }
|
|
425
440
|
|
|
426
441
|
/** Theme resolution ladder: user pin > the frame's declared meta.theme > viewTheme. */
|
|
427
442
|
const resolveTheme = (frame?: FrameEntry, user?: string) => user ?? frame?.theme ?? get().viewTheme
|
|
@@ -628,18 +643,22 @@ export const useStore = create<State>((set, get) => {
|
|
|
628
643
|
}
|
|
629
644
|
}
|
|
630
645
|
}
|
|
631
|
-
|
|
646
|
+
// a note that landed while this board was closed has no room in the saved positions:
|
|
647
|
+
// a board with a recipe re-applies it (spec 18 - room is the layout's job)
|
|
648
|
+
const cramped = !!boardHash && !needTidy && !!(layout || sceneRows?.length || boardAuto) && notesCramped(nodes, manifest)
|
|
649
|
+
if ((!boardHash || needTidy || cramped) && nodes.length) {
|
|
632
650
|
const placedAll = tidy(tidyInput(nodes, manifest), effectiveLayout(layout, sceneRows), layoutWarn)
|
|
633
651
|
for (const pl of placedAll) { const n = nodes.find((x) => x.key === pl.key)!; n.x = pl.x; n.y = pl.y }
|
|
634
652
|
}
|
|
635
653
|
// dirty matches disk by construction - except when load-time pruning changed the
|
|
636
|
-
// node set
|
|
654
|
+
// node set (or a cramped note re-ran the recipe); callers see dirty:true and
|
|
655
|
+
// schedule the save that persists it
|
|
637
656
|
// surface recipe problems at load (dry-run): materialized boards otherwise
|
|
638
657
|
// never run tidy, so a broken agent-authored layout would fail silently
|
|
639
|
-
if (layout && boardHash && !needTidy && nodes.length) {
|
|
658
|
+
if (layout && boardHash && !needTidy && !cramped && nodes.length) {
|
|
640
659
|
tidy(tidyInput(nodes, manifest), layout, layoutWarn)
|
|
641
660
|
}
|
|
642
|
-
return { manifest, nodes, boardHash, boardAuto, deviceView, sceneRows, layout, layoutRaw, baseLayout, selection: [], dirty: prunedAtLoad }
|
|
661
|
+
return { manifest, nodes, boardHash, boardAuto, deviceView, sceneRows, layout, layoutRaw, baseLayout, selection: [], dirty: prunedAtLoad || cramped }
|
|
643
662
|
} catch { return null }
|
|
644
663
|
}
|
|
645
664
|
|
|
@@ -663,6 +682,7 @@ export const useStore = create<State>((set, get) => {
|
|
|
663
682
|
if (next.dirty) scheduleSave() // load-time prune must reach the disk
|
|
664
683
|
if (live && manifestKey(live) !== manifestKey(next.manifest as Manifest)) get().applyManifest(live)
|
|
665
684
|
else if (scenesRev !== scenesAtStart && liveScenes) set({ manifest: { ...get().manifest!, scenes: liveScenes } }) // an sh:scenes that landed mid-fetch outranks the file we read
|
|
685
|
+
roomForNotes() // whichever way the notes arrived, they get their room
|
|
666
686
|
return true
|
|
667
687
|
},
|
|
668
688
|
|
|
@@ -697,6 +717,7 @@ export const useStore = create<State>((set, get) => {
|
|
|
697
717
|
if (next.dirty) scheduleSave() // load-time prune must reach the disk
|
|
698
718
|
if (live && manifestKey(live) !== manifestKey(next.manifest as Manifest)) get().applyManifest(live)
|
|
699
719
|
else if (scenesRev !== scenesAtStart && liveScenes) set({ manifest: { ...get().manifest!, scenes: liveScenes } })
|
|
720
|
+
roomForNotes()
|
|
700
721
|
},
|
|
701
722
|
|
|
702
723
|
renameBoard(name, title, baseHash) {
|
|
@@ -749,6 +770,7 @@ export const useStore = create<State>((set, get) => {
|
|
|
749
770
|
liveScenes = scenes
|
|
750
771
|
const m = get().manifest
|
|
751
772
|
if (m) set({ manifest: { ...m, scenes } }) // before the first manifest: kept for the boot's commit
|
|
773
|
+
roomForNotes() // a scene note arrived: room beside the scene's first frame
|
|
752
774
|
},
|
|
753
775
|
|
|
754
776
|
// The whole sidebar tree in one write: order, membership, the folders themselves. The
|
|
@@ -871,6 +893,7 @@ export const useStore = create<State>((set, get) => {
|
|
|
871
893
|
...(changed ? { dirty: true, baseLayout: nextBase } : {}),
|
|
872
894
|
}))
|
|
873
895
|
if (changed) scheduleSave()
|
|
896
|
+
roomForNotes() // a frame note file arrived: room in front of its frame
|
|
874
897
|
},
|
|
875
898
|
|
|
876
899
|
removeNode(key) {
|
|
@@ -131,6 +131,9 @@ Report where the request came from: chat requests get chat replies; only comment
|
|
|
131
131
|
width; gantt, journey and timeline are wide by nature - few items, or a content frame) -
|
|
132
132
|
raw HTML is inert. Readers comment on a note's text like on a frame element,
|
|
133
133
|
fold it to its corner, hide all with N. An edit lands live without reloading the frame.
|
|
134
|
+
Placement is not your job: every composed layout (recipe, auto board, tidy, device views)
|
|
135
|
+
reserves the note's room in front of its frame and re-applies when a note lands - never
|
|
136
|
+
move frames or pad a lane for a note, just write the file.
|
|
134
137
|
Keep it an aside, a screen's worth at most: specs, flows and mood boards stay content frames.
|
|
135
138
|
Full guide: instructions/shape.md.
|
|
136
139
|
|
|
@@ -131,6 +131,9 @@ Report where the request came from: chat requests get chat replies; only comment
|
|
|
131
131
|
width; gantt, journey and timeline are wide by nature - few items, or a content frame) -
|
|
132
132
|
raw HTML is inert. Readers comment on a note's text like on a frame element,
|
|
133
133
|
fold it to its corner, hide all with N. An edit lands live without reloading the frame.
|
|
134
|
+
Placement is not your job: every composed layout (recipe, auto board, tidy, device views)
|
|
135
|
+
reserves the note's room in front of its frame and re-applies when a note lands - never
|
|
136
|
+
move frames or pad a lane for a note, just write the file.
|
|
134
137
|
Keep it an aside, a screen's worth at most: specs, flows and mood boards stay content frames.
|
|
135
138
|
Full guide: instructions/shape.md.
|
|
136
139
|
|
|
@@ -206,6 +206,13 @@ links open a new tab, images are `design/assets/` paths, raw HTML is inert, and
|
|
|
206
206
|
` ```mermaid ` fence renders hand-drawn, in the note's own yellow. Readers can comment on any
|
|
207
207
|
element of a note exactly as on a frame element.
|
|
208
208
|
|
|
209
|
+
Placement is not your job: the canvas keeps the room. Every layout the shell composes - a
|
|
210
|
+
board's `layout` recipe, the auto board, tidy, device views - reserves the note's width and
|
|
211
|
+
gutter in front of its frame, and when a note lands on a board that is already composed (open
|
|
212
|
+
or not) the recipe re-applies and the frames make way. Never move frames, pad a lane or
|
|
213
|
+
measure a gap for a note: write the file and the board takes care of it. Only a board the
|
|
214
|
+
human dragged by hand keeps its positions as they are; their `t` makes the room there.
|
|
215
|
+
|
|
209
216
|
````md
|
|
210
217
|
## Why the jobs list leads
|
|
211
218
|
|