@marver-design/marver 0.19.1 → 0.19.2
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 +5 -4
- package/package.json +1 -1
- package/src/client/shell/canvas/Sticky.tsx +16 -1
- package/src/client/shell/notes.ts +43 -9
- package/src/client/shell/store.ts +14 -5
- package/src/client/shell/tidy.ts +12 -5
- package/templates/AGENTS-embedded.md +3 -2
- package/templates/AGENTS-studio.md +3 -2
- package/templates/instructions/shape.md +6 -4
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.2 - 2026-09-09
|
|
6
|
+
|
|
7
|
+
### Fixed
|
|
8
|
+
|
|
9
|
+
- **A note longer than its frame gets its room below.** 0.19.1 reserved a note's width beside its
|
|
10
|
+
frame and nothing under it: a long note ran on below the card and over the next row's notes and
|
|
11
|
+
frames. The column's height is now measured as it renders (markdown, fonts and diagrams decide
|
|
12
|
+
it) and fed to the layout: a composed board's next row starts under the note, the gutter still
|
|
13
|
+
the card's, and a board already saved re-applies its recipe when a note lands or grows past its
|
|
14
|
+
frame, as it did for width. Two columns running into each other count as cramped too; the empty
|
|
15
|
+
canvas under a card beside a long column is free. Leaving a device view re-checks the room the
|
|
16
|
+
restored rows have. Folding a note keeps its room, so a fold never moves the board.
|
|
17
|
+
|
|
5
18
|
## 0.19.1 - 2026-09-08
|
|
6
19
|
|
|
7
20
|
### Fixed
|
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`. The layout makes room for it; 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, beside the frame and below; 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
|
@@ -21,10 +21,11 @@ inert. A scene note is 380 wide, a frame note 260; when a frame has both, they s
|
|
|
21
21
|
scene first. An edit lands on the canvas as you save, without reloading the frame.
|
|
22
22
|
|
|
23
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
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
24
|
+
board, tidy, device views) reserves the note's width in front of its frame and its height below it:
|
|
25
|
+
a note longer than its frame runs on under the card, and the row beneath starts under the note, the
|
|
26
|
+
gutter unchanged. A note landing on a board already composed re-applies the recipe so the frames
|
|
27
|
+
make way - whether the board is open at the time or not - and so does a note that grows. Nobody
|
|
28
|
+
moves frames for a note. A board dragged by hand keeps its positions; `t` makes the room there.
|
|
28
29
|
|
|
29
30
|
## Diagrams
|
|
30
31
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@marver-design/marver",
|
|
3
|
-
"version": "0.19.
|
|
3
|
+
"version": "0.19.2",
|
|
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,
|
|
@@ -19,6 +19,7 @@ import { cleanSource, guardDiagramSource, sanitizeSvg } from '../../content/diag
|
|
|
19
19
|
import { useComments } from '../comments-store.ts'
|
|
20
20
|
import { goTo } from '../goto.ts'
|
|
21
21
|
import { NOTE_W, SCENE_NOTE_W, noteAnchor, noteVisible, useNotes, type NoteKind } from '../notes.ts'
|
|
22
|
+
import { useStore } from '../store.ts'
|
|
22
23
|
|
|
23
24
|
export interface NoteSpec { kind: NoteKind; id: string; text: string }
|
|
24
25
|
|
|
@@ -266,10 +267,24 @@ function StickyBody({ text, kind, nodeKey, frameId }: { text: string; kind: Note
|
|
|
266
267
|
export const Stickies = memo(function Stickies({ nodeKey, frameId, notes, underBadge }: { nodeKey: string; frameId: string; notes: NoteSpec[]; underBadge: boolean }) {
|
|
267
268
|
const ids = notes.map((n) => n.id)
|
|
268
269
|
const on = useNotes((s) => noteVisible(s, ids))
|
|
270
|
+
const col = useRef<HTMLDivElement>(null)
|
|
271
|
+
// the column's extent is measured, never computed: markdown, fonts and diagrams decide it. The
|
|
272
|
+
// layout gets it (and its changes) so a note longer than its frame has room below; a folded
|
|
273
|
+
// column keeps its height (the fold is a transform), so folding never reflows. Unmounting
|
|
274
|
+
// (notes gone, board switched) clears it.
|
|
275
|
+
useEffect(() => {
|
|
276
|
+
const el = col.current
|
|
277
|
+
if (!el) return
|
|
278
|
+
const report = () => useStore.getState().noteMeasured(nodeKey, el.offsetTop + el.offsetHeight)
|
|
279
|
+
const ro = new ResizeObserver(report)
|
|
280
|
+
ro.observe(el)
|
|
281
|
+
report()
|
|
282
|
+
return () => { ro.disconnect(); useStore.getState().noteMeasured(nodeKey, 0) }
|
|
283
|
+
}, [nodeKey, underBadge, notes.length])
|
|
269
284
|
if (!notes.length) return null
|
|
270
285
|
const width = Math.max(...notes.map((n) => (n.kind === 'scene' ? SCENE_NOTE_W : NOTE_W)))
|
|
271
286
|
return (
|
|
272
|
-
<div className={`sh-notes${on ? '' : ' off'}${underBadge ? ' below-vbadge' : ''}`} data-node-notes={nodeKey} style={{ width }}>
|
|
287
|
+
<div ref={col} className={`sh-notes${on ? '' : ' off'}${underBadge ? ' below-vbadge' : ''}`} data-node-notes={nodeKey} style={{ width }}>
|
|
273
288
|
<button className="sh-notes-fold sh-no-pan" type="button" aria-label={on ? 'hide notes' : 'show notes'} title={on ? 'hide notes (N: all)' : 'show notes (N: all)'}
|
|
274
289
|
onPointerDown={(e) => e.stopPropagation()}
|
|
275
290
|
onClick={(e) => { e.stopPropagation(); useNotes.getState().toggle(ids) }} />
|
|
@@ -44,14 +44,36 @@ export function sceneNoteHost(
|
|
|
44
44
|
* `h + NODE_HEADER` tall, and that is the height a note beside it has to clear. */
|
|
45
45
|
export const NODE_HEADER = 28
|
|
46
46
|
|
|
47
|
-
/**
|
|
48
|
-
*
|
|
49
|
-
*
|
|
50
|
-
*
|
|
51
|
-
|
|
47
|
+
/** The measured extent of a node's note column: world px from the node's top to the bottom of
|
|
48
|
+
* its last sticky (0 = no column, or not rendered yet). A note's height is its content's, known
|
|
49
|
+
* only once it is drawn - so, like a content frame's height, it is measured, transient (never
|
|
50
|
+
* serialized, a reload remeasures), and fed back to the layout when it lands. */
|
|
51
|
+
const noteHeights = new Map<string, number>()
|
|
52
|
+
export const noteHeight = (key: string): number => noteHeights.get(key) ?? 0
|
|
53
|
+
/** Record a column's extent; true when it changed (the caller asks the layout for room then). */
|
|
54
|
+
export function setNoteHeight(key: string, h: number): boolean {
|
|
55
|
+
const H = Number.isFinite(h) && h > 0 ? Math.round(h) : 0
|
|
56
|
+
if ((noteHeights.get(key) ?? 0) === H) return false
|
|
57
|
+
if (H) noteHeights.set(key, H)
|
|
58
|
+
else noteHeights.delete(key)
|
|
59
|
+
return true
|
|
60
|
+
}
|
|
61
|
+
/** A board load starts from no heights: node keys are per board file, and a key another board
|
|
62
|
+
* also uses must not bring its column's height along (the old board unmounts after the load). */
|
|
63
|
+
export const clearNoteHeights = (): void => { noteHeights.clear() }
|
|
64
|
+
|
|
65
|
+
/** True when a note has no room: another node stands inside the space in front of a noted node
|
|
66
|
+
* (its own note, or the scene's note it hosts) - beside its card, or below it where a note taller
|
|
67
|
+
* than its frame runs on. Room is the layout's job, never the author's - a board the shell
|
|
68
|
+
* composes re-applies its layout when this is true, so a note file can land on a saved board and
|
|
69
|
+
* the frames make way. An obstacle is another node's card, or its own note column (two columns
|
|
70
|
+
* running into each other cramp too) - the two rectangles, never their bounding box: the empty
|
|
71
|
+
* canvas under a card beside a long column is free. Missing nodes (a deleted frame's card, still
|
|
72
|
+
* drawn full size) block room but never host a note. `noteH` is the measured column extent. */
|
|
52
73
|
export function notesCramped(
|
|
53
74
|
nodes: readonly { key: string; frame: string; x: number; y: number; w: number; h: number; missing?: boolean }[],
|
|
54
75
|
manifest: { frames: { id: string; scene: string; note?: string }[]; scenes: { name: string; note?: string }[] } | null,
|
|
76
|
+
noteH: (key: string) => number = noteHeight,
|
|
55
77
|
): boolean {
|
|
56
78
|
if (!manifest) return false
|
|
57
79
|
const entry = (id: string) => manifest.frames.find((f) => f.id === id)
|
|
@@ -62,11 +84,23 @@ export function notesCramped(
|
|
|
62
84
|
const h = sceneNoteHost(live, (id) => entry(id)?.scene, s.name)
|
|
63
85
|
if (h) hosts.add(h)
|
|
64
86
|
}
|
|
87
|
+
type N = (typeof nodes)[number]
|
|
88
|
+
type R = { x0: number; x1: number; y0: number; y1: number }
|
|
89
|
+
const hit = (a: R, b: R) => a.x0 < b.x1 && a.x1 > b.x0 && a.y0 < b.y1 && a.y1 > b.y0
|
|
90
|
+
const card = (n: N): R => ({ x0: n.x, x1: n.x + n.w, y0: n.y, y1: n.y + n.h + NODE_HEADER })
|
|
91
|
+
// the room a note takes: the reserve beside the card, as tall as the card or the column, whichever runs further
|
|
92
|
+
const room = (n: N): R | null => {
|
|
93
|
+
const r = n.missing ? 0 : noteReserve(!!entry(n.frame)?.note, hosts.has(n.key))
|
|
94
|
+
return r ? { x0: n.x - r, x1: n.x, y0: n.y, y1: n.y + Math.max(n.h + NODE_HEADER, noteH(n.key)) } : null
|
|
95
|
+
}
|
|
65
96
|
return live.some((n) => {
|
|
66
|
-
const
|
|
67
|
-
if (!
|
|
68
|
-
|
|
69
|
-
|
|
97
|
+
const mine = room(n)
|
|
98
|
+
if (!mine) return false
|
|
99
|
+
return nodes.some((o) => {
|
|
100
|
+
if (o === n) return false
|
|
101
|
+
const theirs = room(o)
|
|
102
|
+
return hit(mine, card(o)) || (!!theirs && hit(mine, theirs))
|
|
103
|
+
})
|
|
70
104
|
})
|
|
71
105
|
}
|
|
72
106
|
|
|
@@ -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, notesCramped } from './notes.ts'
|
|
4
|
+
import { clearNoteHeights, noteHeight, noteReserve, notesCramped, setNoteHeight } 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'
|
|
@@ -208,9 +208,10 @@ export async function boardFrames(name: string): Promise<string[]> {
|
|
|
208
208
|
}
|
|
209
209
|
|
|
210
210
|
const HEADER = 28
|
|
211
|
-
/** What tidy sees: nodes with their scene, variant run, header-inclusive height,
|
|
211
|
+
/** What tidy sees: nodes with their scene, variant run, header-inclusive height, the sticky
|
|
212
212
|
* note width to reserve in front (spec 18: the frame's note, and the scene's on every member -
|
|
213
|
-
* tidy keeps the scene reserve on the first node it places)
|
|
213
|
+
* tidy keeps the scene reserve on the first node it places), and the measured height of the
|
|
214
|
+
* column standing there, so a note longer than its frame gets its room below as well. */
|
|
214
215
|
export function tidyInput(nodes: readonly Node[], manifest: Manifest | null): TidyNode[] {
|
|
215
216
|
const entryOf = (id: string) => manifest?.frames.find((f) => f.id === id)
|
|
216
217
|
const sceneNote = (scene: string) => !!manifest?.scenes.find((s) => s.name === scene)?.note
|
|
@@ -219,7 +220,7 @@ export function tidyInput(nodes: readonly Node[], manifest: Manifest | null): Ti
|
|
|
219
220
|
const scene = f?.scene ?? ''
|
|
220
221
|
return {
|
|
221
222
|
key: n.key, frame: n.frame, scene, group: f?.variantGroup, variant: f?.variant, w: n.w, h: n.h + HEADER,
|
|
222
|
-
noteW: noteReserve(!!f?.note, false), sceneNoteW: noteReserve(false, sceneNote(scene)),
|
|
223
|
+
noteW: noteReserve(!!f?.note, false), sceneNoteW: noteReserve(false, sceneNote(scene)), noteH: noteHeight(n.key),
|
|
223
224
|
}
|
|
224
225
|
})
|
|
225
226
|
}
|
|
@@ -327,6 +328,8 @@ interface State {
|
|
|
327
328
|
moveNode(key: string, x: number, y: number): void
|
|
328
329
|
resizeNode(key: string, w: number, h: number): void
|
|
329
330
|
measureNode(key: string, frameId: string, ownWidth: number, measuredWidth: number, height: number): void
|
|
331
|
+
/** A node's sticky column was drawn (or grew, or went): its extent from the node's top, world px. */
|
|
332
|
+
noteMeasured(key: string, height: number): void
|
|
330
333
|
setStatus(key: string, status: Node['status'], error?: string): void
|
|
331
334
|
bumpRev(key: string): void
|
|
332
335
|
setThemeOn(key: string, theme: string): void
|
|
@@ -483,6 +486,7 @@ export const useStore = create<State>((set, get) => {
|
|
|
483
486
|
* null = failure (transport, malformed manifest, non-404 board error) - the caller
|
|
484
487
|
* keeps whatever board is currently mounted. */
|
|
485
488
|
const loadBoardState = async (boardName: string): Promise<Partial<State> | null> => {
|
|
489
|
+
clearNoteHeights() // heights are per column drawn; a key shared by two board files carries none across
|
|
486
490
|
try {
|
|
487
491
|
let raw: any
|
|
488
492
|
if (DATA) raw = DATA.manifest
|
|
@@ -996,6 +1000,11 @@ export const useStore = create<State>((set, get) => {
|
|
|
996
1000
|
set((st) => ({ nodes: st.nodes.map((n) => (n.key === key ? { ...n, h: H } : n)) }))
|
|
997
1001
|
scheduleReflow()
|
|
998
1002
|
},
|
|
1003
|
+
noteMeasured(key, height) {
|
|
1004
|
+
// the column's height is its content's - the layout learns it here, and a composed board
|
|
1005
|
+
// whose next row now stands under a long note re-applies its recipe (0.19.2)
|
|
1006
|
+
if (setNoteHeight(key, height)) roomForNotes()
|
|
1007
|
+
},
|
|
999
1008
|
setDeviceView(name) {
|
|
1000
1009
|
const vp = name ? CONFIG.viewports[name] : null
|
|
1001
1010
|
if (name && !vp) return
|
|
@@ -1026,7 +1035,7 @@ export const useStore = create<State>((set, get) => {
|
|
|
1026
1035
|
return { deviceView: name, dirty: true, baseLayout, nodes }
|
|
1027
1036
|
})
|
|
1028
1037
|
if (name) get().runTidy() // restore must NOT tidy - it would destroy positions
|
|
1029
|
-
else scheduleSave()
|
|
1038
|
+
else { scheduleSave(); roomForNotes() } // ...unless a note grew meanwhile and the restored rows stand under it
|
|
1030
1039
|
},
|
|
1031
1040
|
bumpRev(key) { set((s) => ({ nodes: s.nodes.map((n) => (n.key === key ? { ...n, rev: (n.rev ?? 0) + 1 } : n)) })) },
|
|
1032
1041
|
setThemeOn(key, theme) { set((s) => ({ nodes: s.nodes.map((n) => (n.key === key ? { ...n, themeOn: theme } : n)) })) },
|
package/src/client/shell/tidy.ts
CHANGED
|
@@ -4,6 +4,10 @@ export interface TidyNode {
|
|
|
4
4
|
* (its own note), and in front of the scene's first placed node (the scene note - carried on
|
|
5
5
|
* every member, applied once). A column holds both, so the wider wins, never the sum. */
|
|
6
6
|
noteW?: number; sceneNoteW?: number
|
|
7
|
+
/** The note column's measured extent (world px from the node's top), when it is known. A column
|
|
8
|
+
* taller than its card runs below it: the node takes that height in its lane, so the next
|
|
9
|
+
* lane starts under the note - the gutter itself is still sized from the card. */
|
|
10
|
+
noteH?: number
|
|
7
11
|
}
|
|
8
12
|
export interface Placed { key: string; x: number; y: number }
|
|
9
13
|
|
|
@@ -36,20 +40,23 @@ interface Box {
|
|
|
36
40
|
charW: number; charH: number
|
|
37
41
|
}
|
|
38
42
|
|
|
39
|
-
|
|
43
|
+
/** `h` is the part's extent (card, or its note column when that runs further); `ch` the card's own
|
|
44
|
+
* height, the characteristic size gutters are measured from - a long note never inflates them. */
|
|
45
|
+
const box = (id: string, parts: Array<{ key: string; dx: number; dy: number; w: number; h: number; ch?: number }>): Box => ({
|
|
40
46
|
id,
|
|
41
47
|
parts: parts.map(({ key, dx, dy }) => ({ key, dx, dy })),
|
|
42
48
|
w: Math.max(0, ...parts.map((p) => p.dx + p.w)),
|
|
43
49
|
h: Math.max(0, ...parts.map((p) => p.dy + p.h)),
|
|
44
50
|
charW: Math.max(0, ...parts.map((p) => p.w)),
|
|
45
|
-
charH: Math.max(0, ...parts.map((p) => p.h)),
|
|
51
|
+
charH: Math.max(0, ...parts.map((p) => p.ch ?? p.h)),
|
|
46
52
|
})
|
|
53
|
+
const extent = (n: TidyNode) => Math.max(n.h, n.noteH ?? 0)
|
|
47
54
|
|
|
48
55
|
/** A run of nodes laid side by side (a frame's instances, or a variant run). */
|
|
49
56
|
const runBox = (id: string, run: TidyNode[]): Box => {
|
|
50
|
-
const parts: Array<{ key: string; dx: number; dy: number; w: number; h: number }> = []
|
|
57
|
+
const parts: Array<{ key: string; dx: number; dy: number; w: number; h: number; ch?: number }> = []
|
|
51
58
|
let dx = 0
|
|
52
|
-
for (const n of run) { dx += n.noteW ?? 0; parts.push({ key: n.key, dx, dy: 0, w: n.w, h: n.h }); dx += n.w + frameGapX(n.w) }
|
|
59
|
+
for (const n of run) { dx += n.noteW ?? 0; parts.push({ key: n.key, dx, dy: 0, w: n.w, h: extent(n), ch: n.h }); dx += n.w + frameGapX(n.w) }
|
|
53
60
|
return box(id, parts)
|
|
54
61
|
}
|
|
55
62
|
|
|
@@ -285,7 +292,7 @@ export function tidy(nodes: TidyNode[], layout?: BoardLayout, warn: Warn = () =>
|
|
|
285
292
|
sceneMaps.set(scene, m)
|
|
286
293
|
const parts = members
|
|
287
294
|
.filter((n) => m.has(n.key))
|
|
288
|
-
.map((n) => ({ key: n.key, dx: m.get(n.key)!.x, dy: m.get(n.key)!.y, w: n.w, h: n.h }))
|
|
295
|
+
.map((n) => ({ key: n.key, dx: m.get(n.key)!.x, dy: m.get(n.key)!.y, w: n.w, h: extent(n), ch: n.h }))
|
|
289
296
|
sceneBoxes.set(scene, box(scene, parts))
|
|
290
297
|
}
|
|
291
298
|
|
|
@@ -132,8 +132,9 @@ Report where the request came from: chat requests get chat replies; only comment
|
|
|
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
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
|
|
136
|
-
move frames or pad a lane for
|
|
135
|
+
reserves the note's room in front of its frame and below it (a long note pushes the next
|
|
136
|
+
row down) and re-applies when a note lands or grows - never move frames or pad a lane for
|
|
137
|
+
a note, sideways or down, just write the file.
|
|
137
138
|
Keep it an aside, a screen's worth at most: specs, flows and mood boards stay content frames.
|
|
138
139
|
Full guide: instructions/shape.md.
|
|
139
140
|
|
|
@@ -132,8 +132,9 @@ Report where the request came from: chat requests get chat replies; only comment
|
|
|
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
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
|
|
136
|
-
move frames or pad a lane for
|
|
135
|
+
reserves the note's room in front of its frame and below it (a long note pushes the next
|
|
136
|
+
row down) and re-applies when a note lands or grows - never move frames or pad a lane for
|
|
137
|
+
a note, sideways or down, just write the file.
|
|
137
138
|
Keep it an aside, a screen's worth at most: specs, flows and mood boards stay content frames.
|
|
138
139
|
Full guide: instructions/shape.md.
|
|
139
140
|
|
|
@@ -208,10 +208,12 @@ element of a note exactly as on a frame element.
|
|
|
208
208
|
|
|
209
209
|
Placement is not your job: the canvas keeps the room. Every layout the shell composes - a
|
|
210
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
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
211
|
+
gutter in front of its frame, and its height below it: a note longer than its frame runs on
|
|
212
|
+
under the card and the next row starts under the note. When a note lands on a board that is
|
|
213
|
+
already composed (open or not), or grows, the recipe re-applies and the frames make way. Never
|
|
214
|
+
move frames, pad a lane or measure a gap for a note, sideways or down: write the file and the
|
|
215
|
+
board takes care of it. Only a board the human dragged by hand keeps its positions as they
|
|
216
|
+
are; their `t` makes the room there.
|
|
215
217
|
|
|
216
218
|
````md
|
|
217
219
|
## Why the jobs list leads
|