@marver-design/marver 0.19.2 → 0.21.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 +90 -0
- package/README.md +8 -10
- package/dist/{bake-kaf5kGZ7.mjs → bake-BID6mo-N.mjs} +1 -1
- package/dist/{boards-BmxcT3Lc.mjs → boards-BwiDAmPf.mjs} +95 -48
- package/dist/{boards-PuVzw5Wp.mjs → boards-DnLewfj8.mjs} +20 -11
- package/dist/{build-7ed5H2vT.mjs → build-C7MqQ7hq.mjs} +18 -8
- package/dist/cli.mjs +19 -13
- package/dist/{daemon-DbHvLQUL.mjs → daemon-CRZFpl6K.mjs} +1 -1
- package/dist/{dev-D3mP2x27.mjs → dev-BNZF4Mup.mjs} +5 -5
- package/dist/{init-BQYCS3EU.mjs → init-C34BY3R4.mjs} +34 -33
- package/dist/{manifest-B01PSyDc.mjs → manifest-mMfUhPtL.mjs} +8 -5
- package/dist/{plugin-DI-7NAnx.mjs → plugin-omHLCn91.mjs} +34 -26
- package/dist/{poster-DNh6N27C.mjs → poster-BvxiAzy1.mjs} +1 -1
- package/dist/{publish-bakes-Dp-ZFk3d.mjs → publish-bakes-BqzAAa3w.mjs} +8 -2
- package/dist/{shot-DMDvDbeP.mjs → shot-DswS4iRK.mjs} +7 -7
- package/docs/live-jam.md +1 -1
- package/docs/slides.md +89 -89
- package/docs/sticky-notes.md +9 -0
- 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/BoardList.tsx +111 -52
- 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/store.ts +8 -8
- package/src/client/shell/styles.css +10 -9
- package/src/client/stage/main.tsx +56 -7
- package/src/shared/board-tree.ts +271 -140
- package/templates/AGENTS-embedded.md +3 -3
- package/templates/AGENTS-studio.md +3 -3
- package/templates/instructions/boards.md +47 -22
- package/templates/instructions/reference/deck-layouts.md +153 -199
- package/templates/instructions/reference/deck-story.md +6 -6
- package/templates/instructions/slides.md +275 -383
package/src/client/const.ts
CHANGED
|
@@ -9,15 +9,33 @@ export const ROUTE = '/__mv'
|
|
|
9
9
|
* manifest scan (defaultSize for content frames) - one source, no drift. */
|
|
10
10
|
export const CONTENT_WIDTH: Record<string, number> = { document: 760, wide: 1280 }
|
|
11
11
|
|
|
12
|
-
/** The slide stage
|
|
13
|
-
*
|
|
14
|
-
*
|
|
12
|
+
/** The default slide stage: 16:9 at 1280×720, deliberately NOT a config viewport - no
|
|
13
|
+
* migration for existing projects, no deck device in sweeps. Dependency-neutral so
|
|
14
|
+
* server (shot) and shell (store, play) share it. */
|
|
15
15
|
export const SLIDE_INTRINSIC = { width: 1280, height: 720 }
|
|
16
16
|
|
|
17
|
-
/** The one
|
|
18
|
-
* `slide: true`
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
-
|
|
22
|
-
|
|
17
|
+
/** The one sizing rule for slide frames, shared by canvas, shot and slides mode: a
|
|
18
|
+
* `slide: true` frame's stage is its declared viewport when the project defines one
|
|
19
|
+
* (a 16:10 deck authored at `laptop` stays 1280×800), else the 1280×720 default. A
|
|
20
|
+
* slide is an ordinary frame at that size - slides mode scales the whole stage to the
|
|
21
|
+
* screen, so the frame never has to. */
|
|
22
|
+
export function slideSize(
|
|
23
|
+
frame: { slide?: boolean; viewport?: string },
|
|
24
|
+
viewports: Record<string, { width: number; height: number }> = {},
|
|
25
|
+
): { width: number; height: number } | null {
|
|
26
|
+
if (!frame.slide) return null
|
|
27
|
+
const vp = frame.viewport ? viewports[frame.viewport] : undefined
|
|
28
|
+
return vp ? { width: vp.width, height: vp.height } : SLIDE_INTRINSIC
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/** The baseline every slide document shares, in the frame host and the stage alike: the
|
|
32
|
+
* document IS the stage, so the browser's body margin never frames it - whatever the theme
|
|
33
|
+
* or the content primitives a previous slide injected. */
|
|
34
|
+
export const SLIDE_DOC_CSS = 'html[data-mv-slide] body { margin: 0 }'
|
|
35
|
+
|
|
36
|
+
/** How a slide's stage sits in a box of any other size: scaled uniformly to fit and centred.
|
|
37
|
+
* A slide never reflows - the canvas node, the player and a shot all show the same stage. */
|
|
38
|
+
export function stageFit(stage: { width: number; height: number }, box: { w: number; h: number }): { k: number; ox: number; oy: number } {
|
|
39
|
+
const k = Math.max(0.01, Math.min(box.w / stage.width, box.h / stage.height))
|
|
40
|
+
return { k, ox: (box.w - stage.width * k) / 2, oy: (box.h - stage.height * k) / 2 }
|
|
23
41
|
}
|
|
@@ -1,11 +1,9 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Chart (v1.5) - Apache ECharts, the Diagram way: the author picks the FORM
|
|
3
3
|
* (the ECharts option surface, pointed at from instructions/slides.md);
|
|
4
|
-
* marver injects the house theme and
|
|
5
|
-
* breaks the deck (animation at rest, above all).
|
|
4
|
+
* marver injects the house theme and keeps the chart still at rest.
|
|
6
5
|
*
|
|
7
|
-
* SVG renderer ONLY -
|
|
8
|
-
* the board (the lean-DOM serializer keeps <canvas> frames degraded). At
|
|
6
|
+
* SVG renderer ONLY - crisp at any canvas zoom and any stage scale. At
|
|
9
7
|
* rest the chart renders its final state (animation force-disabled); in
|
|
10
8
|
* slides mode (useSlidePlay) it plays its entrance once on mount.
|
|
11
9
|
*
|
|
@@ -66,19 +64,21 @@ export function chartTheme(t: { ink: string; font: string; accent: string; groun
|
|
|
66
64
|
|
|
67
65
|
/** The house theme, read from the frame the chart sits in, at render time. Ink and font are
|
|
68
66
|
* the element's own COMPUTED color and font-family - so a chart inherits a UI screen's
|
|
69
|
-
* Tailwind text colour and typeface, a Doc's tokens, or a
|
|
70
|
-
* wiring. Accent and ground come from slide tokens, then Doc tokens, then the
|
|
67
|
+
* Tailwind text colour and typeface, a Doc's tokens, or a slide's own type, with no
|
|
68
|
+
* per-context wiring. Accent and ground come from slide tokens, then Doc tokens, then the
|
|
69
|
+
* mode palette. A slide frame (meta `slide: true`, stamped on <html> as data-mv-slide) or a
|
|
70
|
+
* <Slide> wrapper takes the stage label scale. */
|
|
71
71
|
function houseTheme(el: HTMLElement, dark: boolean) {
|
|
72
72
|
const css = getComputedStyle(el)
|
|
73
73
|
const v = (...names: string[]) => { for (const n of names) { const x = css.getPropertyValue(n).trim(); if (x) return x } return '' }
|
|
74
74
|
return chartTheme({
|
|
75
75
|
ink: css.color || (dark ? '#F2F2F7' : '#1C1C1E'),
|
|
76
76
|
font: v('--sl-font') || css.fontFamily || FONT_STACK,
|
|
77
|
-
accent: v('--sl-accent', '--mv-accent') || (dark ? '#0091FF' : '#0088FF'),
|
|
77
|
+
accent: v('--sl-accent', '--marver-slide-accent', '--mv-accent') || (dark ? '#0091FF' : '#0088FF'),
|
|
78
78
|
ground: v('--sl-ground', '--mv-surface', '--mv-bg') || (dark ? '#1C1C1E' : '#FFFFFF'),
|
|
79
79
|
grid: v('--sl-grid') || (dark ? 'rgba(242,242,247,.12)' : 'rgba(28,28,30,.1)'),
|
|
80
80
|
dark,
|
|
81
|
-
inSlide: !!el.closest('.sl-root'),
|
|
81
|
+
inSlide: !!el.closest('.sl-root') || document.documentElement.hasAttribute('data-mv-slide'),
|
|
82
82
|
})
|
|
83
83
|
}
|
|
84
84
|
|
|
@@ -22,7 +22,7 @@ export function Diagram(props: Parameters<typeof DiagramRoot>[0]) { ensureStyles
|
|
|
22
22
|
import { Slide as SlideRoot } from './slide.tsx'
|
|
23
23
|
import { Chart as ChartRoot } from './chart.tsx'
|
|
24
24
|
import { Video as VideoRoot } from './video.tsx'
|
|
25
|
-
export { SLIDE_W, SLIDE_H } from './slide.tsx'
|
|
25
|
+
export { SLIDE_W, SLIDE_H, useSlidePlay } from './slide.tsx'
|
|
26
26
|
// the shared stylesheet used to ride in with Doc alone; a slide composes Img,
|
|
27
27
|
// Chart, and Video straight inside <Slide> with no Doc, so every public
|
|
28
28
|
// primitive installs it - once per document, idempotent
|
|
@@ -98,9 +98,10 @@ export function Img({ src, caption, alt, h }: { src: string; caption?: string; a
|
|
|
98
98
|
const url = assetUrl(src)
|
|
99
99
|
const [err, setErr] = useState(false)
|
|
100
100
|
const canvasRef = useRef<HTMLCanvasElement>(null)
|
|
101
|
-
//
|
|
102
|
-
// the
|
|
103
|
-
|
|
101
|
+
// on a slide the LOD canvas is OFF: slides mode scales the whole stage up to the screen,
|
|
102
|
+
// and a bitmap decoded for the frame's own box would blur there - the plain <img> keeps
|
|
103
|
+
// its full resolution. A slide frame is marked on <html> (data-mv-slide) before it renders.
|
|
104
|
+
const inSlide = useInSlide() || (typeof document !== 'undefined' && document.documentElement.hasAttribute('data-mv-slide'))
|
|
104
105
|
// LOD: paint the image on a <canvas> decoded to its on-screen size (never the full 17MB bitmap), and
|
|
105
106
|
// re-pick resolution only when the canvas settles after a zoom. See img-lod.ts. Falls back to a plain
|
|
106
107
|
// <img> where createImageBitmap/bitmaprenderer isn't available (correctness over the optimization).
|
|
@@ -1,39 +1,32 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
3
|
-
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
2
|
+
* Slide - an OPTIONAL wrapper, nothing more. A slide is any frame with `slide: true` in its
|
|
3
|
+
* meta: it renders at its stage size (its declared viewport, else 1280×720) and slides mode
|
|
4
|
+
* scales the whole stage to the screen, so the frame never scales itself. Everything inside
|
|
5
|
+
* is the author's own code - layout, type, colour, imagery, motion.
|
|
6
6
|
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
* unthemed repo still gets a coherent deck.
|
|
7
|
+
* The wrapper fills the frame and tells the content primitives they sit on a stage (Img,
|
|
8
|
+
* Chart); decks written against the earlier Slide root import it, so it stays.
|
|
10
9
|
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
* arms the entrance presets (`data-animate`, run once after the swap
|
|
15
|
-
* settles - the stage adds `data-sl-entered`).
|
|
10
|
+
* The playback contract lives with the player (src/client/stage/main.tsx): while a deck
|
|
11
|
+
* plays, <html> carries `data-sl-play`, plus `data-sl-entered` once each slide has arrived -
|
|
12
|
+
* authors key their own CSS and JS motion off them. useSlidePlay() is the same flag for React.
|
|
16
13
|
*/
|
|
17
|
-
import { createContext, useContext,
|
|
18
|
-
import {
|
|
14
|
+
import { createContext, useContext, useSyncExternalStore, type CSSProperties, type ReactNode } from 'react'
|
|
15
|
+
import { SLIDE_INTRINSIC } from '../const.ts'
|
|
19
16
|
|
|
20
|
-
|
|
21
|
-
export const
|
|
22
|
-
|
|
23
|
-
const PAD_X = 88
|
|
24
|
-
const PAD_Y = 44
|
|
17
|
+
/** The default stage. A deck may declare another size through its frames' viewport. */
|
|
18
|
+
export const SLIDE_W = SLIDE_INTRINSIC.width
|
|
19
|
+
export const SLIDE_H = SLIDE_INTRINSIC.height
|
|
25
20
|
|
|
26
|
-
/** Img (and anything else that cares) asks: am I inside a
|
|
21
|
+
/** Img (and anything else that cares) asks: am I inside a <Slide>? */
|
|
27
22
|
export const SlideCtx = createContext(false)
|
|
28
23
|
export const useInSlide = () => useContext(SlideCtx)
|
|
29
24
|
|
|
30
25
|
/**
|
|
31
|
-
* Is
|
|
32
|
-
*
|
|
33
|
-
*
|
|
34
|
-
*
|
|
35
|
-
* documentElement attribute, so the contract is one attribute, one owner,
|
|
36
|
-
* observable by anyone.
|
|
26
|
+
* Is a deck playing? The STAGE owns the answer: it stamps `data-sl-play` on <html> (boot:
|
|
27
|
+
* the `slides` URL param). CSS reacts to the attribute natively; React components (Chart's
|
|
28
|
+
* entrance, Video's player mount) subscribe here - a MutationObserver over the
|
|
29
|
+
* documentElement attribute, so the contract is one attribute, one owner, observable by anyone.
|
|
37
30
|
*/
|
|
38
31
|
const subscribePlay = (cb: () => void) => {
|
|
39
32
|
if (typeof document === 'undefined') return () => {}
|
|
@@ -44,195 +37,27 @@ const subscribePlay = (cb: () => void) => {
|
|
|
44
37
|
const readPlay = () => typeof document !== 'undefined' && document.documentElement.hasAttribute('data-sl-play')
|
|
45
38
|
export const useSlidePlay = (): boolean => useSyncExternalStore(subscribePlay, readPlay, () => false)
|
|
46
39
|
|
|
40
|
+
/* the frame document must not pad the stage: a stage-wide root in a margined body overflows */
|
|
47
41
|
const SLIDE_CSS = `
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
also paints the slide ground, so the letterbox around a scaled stage is
|
|
51
|
-
seamless instead of a default-white halo. */
|
|
52
|
-
body:has(.sl-root) { margin: 0; background: var(--marver-slide-ground, #ffffff) }
|
|
53
|
-
.dark body:has(.sl-root), [data-theme="dark"] body:has(.sl-root) { background: var(--marver-slide-ground-dark, #101014) }
|
|
54
|
-
.sl-root, .sl-root * { box-sizing: border-box }
|
|
55
|
-
.sl-root {
|
|
56
|
-
--sl-ink: var(--marver-slide-ink, #18181b);
|
|
57
|
-
--sl-ground: var(--marver-slide-ground, #ffffff);
|
|
58
|
-
--sl-accent: var(--marver-slide-accent, #0088ff);
|
|
59
|
-
--sl-muted: var(--marver-slide-muted, rgba(24, 24, 27, .55));
|
|
60
|
-
--sl-tempo: var(--marver-slide-tempo, 350ms);
|
|
61
|
-
--sl-font: var(--marver-slide-font, ${FONT_STACK});
|
|
62
|
-
/* THE STAGE MARGINS, in px and ASYMMETRIC - the shape every well-made deck
|
|
63
|
-
uses. Generous at the sides, tighter top and bottom, so the title sits
|
|
64
|
-
high, the footnote sits low, and the middle band is the tallest thing on
|
|
65
|
-
the slide. Percentages are wrong here twice over: they would resolve
|
|
66
|
-
against this absolutely positioned box's containing block (the viewport,
|
|
67
|
-
not the stage), and one value for all four sides squeezes the middle. */
|
|
68
|
-
--sl-pad-x: var(--marver-slide-pad-x, ${PAD_X}px);
|
|
69
|
-
--sl-pad-y: var(--marver-slide-pad-y, ${PAD_Y}px);
|
|
70
|
-
--sl-margin: var(--sl-pad-x); /* the side margin, for author math */
|
|
71
|
-
/* THE FIT: authored at exactly ${SLIDE_W}x${SLIDE_H}, then scaled and
|
|
72
|
-
centered to the largest box the viewport gives it - fill window, any
|
|
73
|
-
device, a canvas node resized to a phone, any published viewer's screen.
|
|
74
|
-
One coordinate system, so the author's px, Tailwind classes, and charts
|
|
75
|
-
all scale together. Pure CSS (tan(atan2(a, b)) is the unitless ratio
|
|
76
|
-
a / b), so a lean cover - which runs no JS - reflows to the right scale
|
|
77
|
-
the moment its node is resized. 1 wherever the viewport IS the stage.
|
|
78
|
-
Declared under @supports below: an engine without CSS trig would keep
|
|
79
|
-
the invalid tokens, defeat the var() fallback, and invalidate the whole
|
|
80
|
-
transform - unscaled AND uncentered. */
|
|
81
|
-
--sl-fit: 1;
|
|
82
|
-
/* translate-center, not inset+margin:auto - an overconstrained absolute
|
|
83
|
-
box (1280px stage in a 390px viewport) resolves margins to 0 and the
|
|
84
|
-
scaled slide drifts off-center; translate(-50%,-50%) centers at ANY size */
|
|
85
|
-
position: absolute; left: 50%; top: 50%;
|
|
86
|
-
width: ${SLIDE_W}px; height: ${SLIDE_H}px; overflow: hidden;
|
|
87
|
-
transform: translate(-50%, -50%) scale(var(--sl-fit, 1)); transform-origin: center center;
|
|
88
|
-
background: var(--sl-ground); color: var(--sl-ink);
|
|
89
|
-
font-family: var(--sl-font);
|
|
90
|
-
padding: var(--sl-pad-y) var(--sl-pad-x); box-sizing: border-box;
|
|
91
|
-
display: flex; flex-direction: column; justify-content: center; gap: 28px;
|
|
92
|
-
}
|
|
93
|
-
@supports (width: calc(1px * tan(atan2(1px, 1px)))) {
|
|
94
|
-
.sl-root { --sl-fit: min(tan(atan2(100vw, ${SLIDE_W}px)), tan(atan2(100vh, ${SLIDE_H}px))) }
|
|
95
|
-
}
|
|
96
|
-
.dark .sl-root, [data-theme="dark"] .sl-root {
|
|
97
|
-
--sl-ink: var(--marver-slide-ink-dark, #f5f5f7);
|
|
98
|
-
--sl-ground: var(--marver-slide-ground-dark, #101014);
|
|
99
|
-
--sl-muted: var(--marver-slide-muted-dark, rgba(245, 245, 247, .55));
|
|
100
|
-
}
|
|
101
|
-
/* type roles - fixed values, one coordinate system with the 1280x720 stage.
|
|
102
|
-
sl-display is the ONE sanctioned oversize: the big-number stat, a section
|
|
103
|
-
numeral, the manifesto line - never running text. */
|
|
104
|
-
.sl-display { font-size: 160px; line-height: 1; font-weight: 800; letter-spacing: -.03em; margin: 0; font-variant-numeric: tabular-nums }
|
|
105
|
-
/* sl-stat is the ROW size: three or four figures side by side, where one
|
|
106
|
-
sl-display would not fit and sl-assertion would not read as a number. */
|
|
107
|
-
.sl-stat { font-size: 88px; line-height: 1.02; font-weight: 700; letter-spacing: -.03em; margin: 0; font-variant-numeric: tabular-nums }
|
|
108
|
-
.sl-assertion { font-size: 56px; line-height: 1.08; font-weight: 750; letter-spacing: -.02em; margin: 0 }
|
|
109
|
-
.sl-support { font-size: 30px; line-height: 1.25; font-weight: 500; margin: 0 }
|
|
110
|
-
.sl-body { font-size: 24px; line-height: 1.45; margin: 0 }
|
|
111
|
-
.sl-caption { font-size: 18px; line-height: 1.4; color: var(--sl-muted); margin: 0 }
|
|
112
|
-
|
|
113
|
-
/* THE MOTION RESET - at rest, a slide is still: animation NONE (not paused -
|
|
114
|
-
none is deterministic; paused can freeze mid-keyframe) and no transitions.
|
|
115
|
-
Slides mode (the stage sets data-sl-play on <html>) lifts it. This governs
|
|
116
|
-
EVERY descendant - spinners and loaders included - and that is the Slide
|
|
117
|
-
contract, documented in instructions/slides.md. */
|
|
118
|
-
:root:not([data-sl-play]) .sl-root, :root:not([data-sl-play]) .sl-root *,
|
|
119
|
-
:root:not([data-sl-play]) .sl-root *::before, :root:not([data-sl-play]) .sl-root *::after {
|
|
120
|
-
animation: none !important;
|
|
121
|
-
transition: none !important;
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
/* Entrance presets: inert until the stage marks the swap settled. An element
|
|
125
|
-
carrying a morph name must not carry data-animate (one transform owner) -
|
|
126
|
-
the doctrine says so; the selector below cannot check it, the review gate does. */
|
|
127
|
-
[data-sl-play] .sl-root [data-animate] { opacity: 0 }
|
|
128
|
-
/* ONE tempo per deck: the token times the entrances AND the view-transition
|
|
129
|
-
morphs between slides (the stage document is this document). */
|
|
130
|
-
::view-transition-group(*), ::view-transition-old(root), ::view-transition-new(root) {
|
|
131
|
-
animation-duration: var(--marver-slide-tempo, 350ms);
|
|
132
|
-
}
|
|
133
|
-
[data-sl-play][data-sl-entered] .sl-root [data-animate] {
|
|
134
|
-
opacity: 1; animation-duration: var(--sl-tempo); animation-timing-function: cubic-bezier(.2, .7, .2, 1);
|
|
135
|
-
animation-fill-mode: both;
|
|
136
|
-
}
|
|
137
|
-
[data-sl-play][data-sl-entered] .sl-root [data-animate="fade-up"] { animation-name: sl-fade-up }
|
|
138
|
-
[data-sl-play][data-sl-entered] .sl-root [data-animate="fade"] { animation-name: sl-fade }
|
|
139
|
-
[data-sl-play][data-sl-entered] .sl-root [data-animate="scale-in"] { animation-name: sl-scale-in }
|
|
140
|
-
[data-sl-play][data-sl-entered] .sl-root [data-animate-delay="1"] { animation-delay: 80ms }
|
|
141
|
-
[data-sl-play][data-sl-entered] .sl-root [data-animate-delay="2"] { animation-delay: 160ms }
|
|
142
|
-
[data-sl-play][data-sl-entered] .sl-root [data-animate-delay="3"] { animation-delay: 240ms }
|
|
143
|
-
@keyframes sl-fade-up { from { opacity: 0; transform: translateY(18px) } to { opacity: 1; transform: none } }
|
|
144
|
-
@keyframes sl-fade { from { opacity: 0 } to { opacity: 1 } }
|
|
145
|
-
@keyframes sl-scale-in { from { opacity: 0; transform: scale(.94) } to { opacity: 1; transform: none } }
|
|
146
|
-
@media (prefers-reduced-motion: reduce) {
|
|
147
|
-
[data-sl-play] .sl-root [data-animate] { opacity: 1; animation: none !important }
|
|
148
|
-
}
|
|
42
|
+
body:has(.sl-root) { margin: 0 }
|
|
43
|
+
.sl-root { position: relative; box-sizing: border-box; width: 100%; min-height: 100vh }
|
|
149
44
|
`
|
|
150
45
|
|
|
151
46
|
function ensureSlideStyles() {
|
|
152
47
|
// keyed by the DOCUMENT, not a module boolean - HMR reloads and multiple
|
|
153
48
|
// roots must not double- or under-inject
|
|
154
|
-
if (typeof document === 'undefined' || document.querySelector('style[data-mv-slide]')) return
|
|
49
|
+
if (typeof document === 'undefined' || document.querySelector('style[data-mv-slide-css]')) return
|
|
155
50
|
const el = document.createElement('style')
|
|
156
|
-
el.setAttribute('data-mv-slide', '')
|
|
51
|
+
el.setAttribute('data-mv-slide-css', '')
|
|
157
52
|
el.textContent = SLIDE_CSS
|
|
158
53
|
document.head.appendChild(el)
|
|
159
54
|
}
|
|
160
55
|
|
|
161
|
-
export function Slide({ children, style }: { children?: ReactNode; style?: CSSProperties }) {
|
|
56
|
+
export function Slide({ children, className, style }: { children?: ReactNode; className?: string; style?: CSSProperties }) {
|
|
162
57
|
ensureSlideStyles()
|
|
163
|
-
const ref = useRef<HTMLDivElement>(null)
|
|
164
|
-
// DEV overflow marker: a slide that outgrows its stage is a VISIBLE defect
|
|
165
|
-
// (outline + console + data-sl-over), never a silent clip decision left to
|
|
166
|
-
// chance. Development only - a published deck paints no diagnostics.
|
|
167
|
-
// Two tests, both in the stage's LAYOUT space (rects are post-transform,
|
|
168
|
-
// the fit scales the stage, so rect offsets are divided by the scale):
|
|
169
|
-
// 1. escape - a box leaving the 1280x720 stage. Measured against the
|
|
170
|
-
// STAGE, not the padded content box: full-bleed images and clipped
|
|
171
|
-
// photos break the margins on purpose.
|
|
172
|
-
// 2. collision - a flex/grid child outgrowing its parent's content box.
|
|
173
|
-
// Inside a flex column a body at flex:1 keeps its own box while ITS
|
|
174
|
-
// children spill over the neighbouring bands, all still inside the
|
|
175
|
-
// stage, which test 1 never sees.
|
|
176
|
-
useEffect(() => {
|
|
177
|
-
if (!import.meta.env.DEV) return
|
|
178
|
-
const el = ref.current
|
|
179
|
-
if (!el || typeof ResizeObserver === 'undefined') return
|
|
180
|
-
let last: boolean | null = null
|
|
181
|
-
let raf = 0
|
|
182
|
-
const check = () => {
|
|
183
|
-
raf = 0
|
|
184
|
-
const r = el.getBoundingClientRect()
|
|
185
|
-
const k = el.offsetWidth ? r.width / el.offsetWidth : 1
|
|
186
|
-
const pad = (v: string) => parseFloat(v) || 0
|
|
187
|
-
const W = el.offsetWidth, H = el.offsetHeight
|
|
188
|
-
let over = el.scrollHeight > el.clientHeight + 1 || el.scrollWidth > el.clientWidth + 1
|
|
189
|
-
let culprit: Element | null = null
|
|
190
|
-
const all = el.querySelectorAll('*')
|
|
191
|
-
const n = Math.min(all.length, 600)
|
|
192
|
-
for (let i = 0; !over && i < n; i++) {
|
|
193
|
-
const c = all[i]
|
|
194
|
-
const b = c.getBoundingClientRect()
|
|
195
|
-
if (!b.width && !b.height) continue
|
|
196
|
-
const y0 = (b.top - r.top) / k, y1 = (b.bottom - r.top) / k
|
|
197
|
-
const x0 = (b.left - r.left) / k, x1 = (b.right - r.left) / k
|
|
198
|
-
if (y1 > H + 1.5 || y0 < -1.5 || x1 > W + 1.5 || x0 < -1.5) { over = true; culprit = c; break }
|
|
199
|
-
const par = c.parentElement
|
|
200
|
-
if (!par || !par.clientHeight) continue
|
|
201
|
-
const ps = getComputedStyle(par)
|
|
202
|
-
if (!/flex|grid/.test(ps.display) || ps.overflowY !== 'visible' || ps.overflowX !== 'visible') continue
|
|
203
|
-
if (/absolute|fixed/.test(getComputedStyle(c).position)) continue
|
|
204
|
-
const pr = par.getBoundingClientRect()
|
|
205
|
-
const lo = (pr.top - b.top) / k + pad(ps.paddingTop)
|
|
206
|
-
const hi = (b.bottom - pr.bottom) / k + pad(ps.paddingBottom)
|
|
207
|
-
if (lo > 2 || hi > 2) { over = true; culprit = c; break }
|
|
208
|
-
}
|
|
209
|
-
if (over === last) return // idempotent: never write the same state twice
|
|
210
|
-
last = over
|
|
211
|
-
el.style.outline = over ? '3px solid #ff4d4f' : ''
|
|
212
|
-
el.dataset.slOver = over ? '1' : ''
|
|
213
|
-
if (over) console.warn('[marver slide] content overflows the 1280×720 stage - split the slide, never shrink the type', culprit ?? el)
|
|
214
|
-
}
|
|
215
|
-
const schedule = () => { if (!raf) raf = requestAnimationFrame(check) }
|
|
216
|
-
// observe every descendant (bounded): a flex child keeps its box while its
|
|
217
|
-
// content grows, and a root-only observer never hears about that. Nodes
|
|
218
|
-
// added later are picked up by the childList observer - childList ONLY,
|
|
219
|
-
// so our own outline/data writes (attribute mutations) cannot re-trigger.
|
|
220
|
-
const ro = new ResizeObserver(schedule)
|
|
221
|
-
const seen = new WeakSet<Element>()
|
|
222
|
-
const observeAll = () => {
|
|
223
|
-
const all = el.querySelectorAll('*')
|
|
224
|
-
for (let i = 0; i < all.length && i < 600; i++) { if (!seen.has(all[i])) { seen.add(all[i]); ro.observe(all[i]) } }
|
|
225
|
-
}
|
|
226
|
-
ro.observe(el)
|
|
227
|
-
observeAll()
|
|
228
|
-
const mo = new MutationObserver(() => { observeAll(); schedule() })
|
|
229
|
-
mo.observe(el, { childList: true, subtree: true })
|
|
230
|
-
schedule()
|
|
231
|
-
return () => { cancelAnimationFrame(raf); ro.disconnect(); mo.disconnect() }
|
|
232
|
-
}, [])
|
|
233
58
|
return (
|
|
234
59
|
<SlideCtx.Provider value={true}>
|
|
235
|
-
<div
|
|
60
|
+
<div className={className ? `sl-root ${className}` : 'sl-root'} style={style}>{children}</div>
|
|
236
61
|
</SlideCtx.Provider>
|
|
237
62
|
)
|
|
238
63
|
}
|
|
@@ -8,6 +8,7 @@ import { createRoot } from 'react-dom/client'
|
|
|
8
8
|
|
|
9
9
|
import './bridge.js'
|
|
10
10
|
import { frameFile, frames, layoutChain, layouts, providers } from './registry.ts'
|
|
11
|
+
import { SLIDE_DOC_CSS } from '../const.ts'
|
|
11
12
|
|
|
12
13
|
const params = new URLSearchParams(location.search)
|
|
13
14
|
const id = params.get('id') ?? ''
|
|
@@ -77,6 +78,15 @@ async function boot() {
|
|
|
77
78
|
// No typeof gate: memo()/forwardRef() components are objects, not functions.
|
|
78
79
|
// React + the ErrorBoundary validate the element type better than we can.
|
|
79
80
|
if (Frame == null) return fail(`${fileKey} has no default export`)
|
|
81
|
+
// a slide frame says so on its document - content primitives size for a stage by it, and
|
|
82
|
+
// the slide baseline (SLIDE_DOC_CSS) applies, the same one the stage installs
|
|
83
|
+
if (frameMod.meta?.slide === true) {
|
|
84
|
+
document.documentElement.setAttribute('data-mv-slide', '')
|
|
85
|
+
const base = document.createElement('style')
|
|
86
|
+
base.setAttribute('data-mv-slide-doc', '')
|
|
87
|
+
base.textContent = SLIDE_DOC_CSS
|
|
88
|
+
document.head.appendChild(base)
|
|
89
|
+
}
|
|
80
90
|
|
|
81
91
|
const wrappers: any[] = []
|
|
82
92
|
const providerKey = Object.keys(providers)[0]
|