@marver-design/marver 0.19.1 → 0.20.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 +77 -0
- package/README.md +9 -11
- package/dist/{bake-kaf5kGZ7.mjs → bake-tAb0D6Rc.mjs} +1 -1
- package/dist/{build-7ed5H2vT.mjs → build-DwpNPl6Z.mjs} +3 -3
- package/dist/cli.mjs +18 -12
- package/dist/{dev-D3mP2x27.mjs → dev-BjdDP69b.mjs} +3 -3
- package/dist/{init-BQYCS3EU.mjs → init-DKxuRxBr.mjs} +33 -32
- package/dist/{plugin-DI-7NAnx.mjs → plugin-y4Ch7o_A.mjs} +7 -7
- package/dist/{poster-DNh6N27C.mjs → poster-FCv_nXyP.mjs} +1 -1
- package/dist/{publish-bakes-Dp-ZFk3d.mjs → publish-bakes-XH6Bac58.mjs} +8 -2
- package/dist/{shot-DMDvDbeP.mjs → shot-iw3SEcpn.mjs} +7 -7
- package/docs/live-jam.md +1 -1
- package/docs/slides.md +89 -89
- package/docs/sticky-notes.md +14 -4
- package/package.json +1 -1
- package/src/client/const.ts +27 -9
- package/src/client/content/chart.tsx +8 -8
- package/src/client/content/index.tsx +5 -4
- package/src/client/content/slide.tsx +26 -201
- package/src/client/frame-host/main.tsx +10 -0
- package/src/client/shell/Comments.tsx +12 -4
- package/src/client/shell/Play.tsx +26 -14
- package/src/client/shell/Toolbar.tsx +7 -5
- package/src/client/shell/canvas/FrameNode.tsx +16 -2
- package/src/client/shell/canvas/Sticky.tsx +16 -1
- package/src/client/shell/notes.ts +43 -9
- package/src/client/shell/store.ts +17 -8
- package/src/client/shell/tidy.ts +12 -5
- package/src/client/stage/main.tsx +56 -7
- package/templates/AGENTS-embedded.md +3 -2
- package/templates/AGENTS-studio.md +3 -2
- package/templates/instructions/reference/deck-layouts.md +153 -199
- package/templates/instructions/reference/deck-story.md +6 -6
- package/templates/instructions/shape.md +6 -4
- package/templates/instructions/slides.md +275 -383
|
@@ -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
|
}
|
|
@@ -256,10 +257,10 @@ let liveScenes: Manifest['scenes'] | null = null // the last sh:
|
|
|
256
257
|
const measuredHeights = new Map<string, number>()
|
|
257
258
|
|
|
258
259
|
function defaultSize(frame: FrameEntry) {
|
|
259
|
-
// the precedence chain (spec 09 slice 1): slide
|
|
260
|
+
// the precedence chain (spec 09 slice 1): slide stage → authored
|
|
260
261
|
// viewport → content sizing → default (one helper, shared with shot) -
|
|
261
|
-
//
|
|
262
|
-
const sl = slideSize(frame)
|
|
262
|
+
// a slide's stage is its declared viewport, else 1280×720
|
|
263
|
+
const sl = slideSize(frame, CONFIG.viewports)
|
|
263
264
|
if (sl) return { w: sl.width, h: sl.height }
|
|
264
265
|
// content frames: own width from Doc layout; height from the latest
|
|
265
266
|
// measurement at that width, or a placeholder until sh:measure lands.
|
|
@@ -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
|
|
|
@@ -14,6 +14,7 @@ import { Component, createElement, useEffect, useRef, useState, type ComponentTy
|
|
|
14
14
|
import { flushSync } from 'react-dom'
|
|
15
15
|
import { createRoot } from 'react-dom/client'
|
|
16
16
|
import { frameFile, frames, layoutChain, layouts, providers } from '../frame-host/registry.ts'
|
|
17
|
+
import { SLIDE_DOC_CSS } from '../const.ts'
|
|
17
18
|
import { createInspect } from '../frame-host/inspect.js'
|
|
18
19
|
|
|
19
20
|
const params = new URLSearchParams(location.search)
|
|
@@ -21,13 +22,54 @@ const params = new URLSearchParams(location.search)
|
|
|
21
22
|
// token systems, .dark for class-keyed ones (Tailwind/shadcn). Missing the class made
|
|
22
23
|
// play render class-keyed apps light while the canvas showed them dark.
|
|
23
24
|
const bootTheme = params.get('theme') ?? 'light'
|
|
24
|
-
// slides mode
|
|
25
|
-
//
|
|
26
|
-
//
|
|
27
|
-
//
|
|
25
|
+
// slides mode: the shell says so in the URL; the stage stamps the attributes every slide
|
|
26
|
+
// can key its own motion off - data-sl-play (the deck is playing) on <html> for the whole
|
|
27
|
+
// show, data-sl-entered once each swap settles, data-mv-slide while the mounted frame is a
|
|
28
|
+
// `slide: true` frame. `tr=none` and prefers-reduced-motion both skip view transitions.
|
|
28
29
|
const slidesMode = params.get('slides') === '1'
|
|
29
30
|
const deckTransition = params.get('tr') ?? 'fade'
|
|
30
31
|
if (slidesMode) document.documentElement.setAttribute('data-sl-play', '')
|
|
32
|
+
|
|
33
|
+
/** The deck's playback CSS - player behaviour, so it lives with the player and serves every
|
|
34
|
+
* slide, whatever its markup: one tempo (--marver-slide-tempo) for the morphs between slides
|
|
35
|
+
* and the optional entrance presets (data-animate + data-animate-delay), which stay inert
|
|
36
|
+
* until the slide has arrived. Anything beyond these is the author's own CSS or JS. */
|
|
37
|
+
const DECK_CSS = `
|
|
38
|
+
::view-transition-group(*), ::view-transition-old(root), ::view-transition-new(root) {
|
|
39
|
+
animation-duration: var(--marver-slide-tempo, 350ms);
|
|
40
|
+
}
|
|
41
|
+
[data-sl-play] [data-animate] { opacity: 0 }
|
|
42
|
+
[data-sl-play][data-sl-entered] [data-animate] {
|
|
43
|
+
opacity: 1; animation-duration: var(--marver-slide-tempo, 350ms); animation-timing-function: cubic-bezier(.2, .7, .2, 1);
|
|
44
|
+
animation-fill-mode: both;
|
|
45
|
+
}
|
|
46
|
+
[data-sl-play][data-sl-entered] [data-animate="fade-up"] { animation-name: sl-fade-up }
|
|
47
|
+
[data-sl-play][data-sl-entered] [data-animate="fade"] { animation-name: sl-fade }
|
|
48
|
+
[data-sl-play][data-sl-entered] [data-animate="scale-in"] { animation-name: sl-scale-in }
|
|
49
|
+
[data-sl-play][data-sl-entered] [data-animate-delay="1"] { animation-delay: 80ms }
|
|
50
|
+
[data-sl-play][data-sl-entered] [data-animate-delay="2"] { animation-delay: 160ms }
|
|
51
|
+
[data-sl-play][data-sl-entered] [data-animate-delay="3"] { animation-delay: 240ms }
|
|
52
|
+
@keyframes sl-fade-up { from { opacity: 0; transform: translateY(18px) } to { opacity: 1; transform: none } }
|
|
53
|
+
@keyframes sl-fade { from { opacity: 0 } to { opacity: 1 } }
|
|
54
|
+
@keyframes sl-scale-in { from { opacity: 0; transform: scale(.94) } to { opacity: 1; transform: none } }
|
|
55
|
+
@media (prefers-reduced-motion: reduce) {
|
|
56
|
+
[data-sl-play] [data-animate] { opacity: 1; animation: none !important }
|
|
57
|
+
}
|
|
58
|
+
`
|
|
59
|
+
if (slidesMode) {
|
|
60
|
+
const el = document.createElement('style')
|
|
61
|
+
el.setAttribute('data-mv-deck', '')
|
|
62
|
+
el.textContent = DECK_CSS
|
|
63
|
+
document.head.appendChild(el)
|
|
64
|
+
}
|
|
65
|
+
// the slide document baseline, keyed on data-mv-slide (set per mounted frame): any mode can
|
|
66
|
+
// show a slide, and its geometry must not depend on what the stage showed before
|
|
67
|
+
{
|
|
68
|
+
const base = document.createElement('style')
|
|
69
|
+
base.setAttribute('data-mv-slide-doc', '')
|
|
70
|
+
base.textContent = SLIDE_DOC_CSS
|
|
71
|
+
document.head.appendChild(base)
|
|
72
|
+
}
|
|
31
73
|
const reducedMotion = typeof matchMedia !== 'undefined' && matchMedia('(prefers-reduced-motion: reduce)').matches
|
|
32
74
|
document.documentElement.dataset.theme = bootTheme
|
|
33
75
|
document.documentElement.classList.toggle('dark', bootTheme === 'dark')
|
|
@@ -45,7 +87,7 @@ document.addEventListener('gesturestart', (e) => e.preventDefault())
|
|
|
45
87
|
// B0.2: a nested scroll container hitting its boundary must not chain into the shell page
|
|
46
88
|
document.documentElement.style.overscrollBehavior = 'contain'
|
|
47
89
|
|
|
48
|
-
interface Mounted { id: string; Frame: ComponentType; wrappers: ComponentType[] }
|
|
90
|
+
interface Mounted { id: string; Frame: ComponentType; wrappers: ComponentType[]; slide: boolean }
|
|
49
91
|
|
|
50
92
|
/** Resolve a frame id to its component + wrapper chain. Modules are import()-cached by
|
|
51
93
|
* Vite, so re-resolving a chain yields the SAME component references - React keeps
|
|
@@ -59,7 +101,7 @@ async function resolve(id: string): Promise<Mounted> {
|
|
|
59
101
|
const providerKey = Object.keys(providers)[0]
|
|
60
102
|
if (providerKey) wrappers.push((await providers[providerKey]() as any).default)
|
|
61
103
|
for (const lk of layoutChain(fileKey)) wrappers.push((await layouts[lk]() as any).default)
|
|
62
|
-
return { id, Frame: mod.default, wrappers: wrappers.filter((w) => w != null) }
|
|
104
|
+
return { id, Frame: mod.default, wrappers: wrappers.filter((w) => w != null), slide: mod.meta?.slide === true }
|
|
63
105
|
}
|
|
64
106
|
|
|
65
107
|
class Boundary extends Component<{ resetKey: string; children?: ReactNode }, { err: Error | null }> {
|
|
@@ -119,7 +161,14 @@ function Stage() {
|
|
|
119
161
|
for (const el of document.querySelectorAll('[data-animate]'))
|
|
120
162
|
if (getComputedStyle(el).viewTransitionName !== 'none') el.removeAttribute('data-animate')
|
|
121
163
|
}
|
|
122
|
-
const apply = () => {
|
|
164
|
+
const apply = () => {
|
|
165
|
+
if (seq !== swapSeq.current) return
|
|
166
|
+
// the slide mark first: content primitives read it while they render
|
|
167
|
+
document.documentElement.toggleAttribute('data-mv-slide', next.slide)
|
|
168
|
+
flushSync(() => { setErr(null); setMounted(next) })
|
|
169
|
+
current.current = id
|
|
170
|
+
disarm()
|
|
171
|
+
}
|
|
123
172
|
const entered = () => {
|
|
124
173
|
if (!slidesMode || seq !== swapSeq.current) return
|
|
125
174
|
document.documentElement.setAttribute('data-sl-entered', '')
|
|
@@ -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
|
|