@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 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`. Published canvases carry them.
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.
@@ -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.0",
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
- const scheduleReflow = () => {
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 (s.layout || s.sceneRows?.length) s.runTidy()
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
- if ((!boardHash || needTidy) && nodes.length) {
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; callers see dirty:true and schedule the save that persists the prune
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