@gradeui/ui 2.0.0 → 2.1.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/components/ui/avatar.md +28 -3
- package/components/ui/grade-loader.md +31 -0
- package/components/ui/input.md +2 -1
- package/components/ui/logo.md +39 -13
- package/components/ui/map.md +19 -10
- package/components/ui/motion.md +109 -0
- package/dist/contracts.js +5 -5
- package/dist/contracts.js.map +1 -1
- package/dist/contracts.mjs +5 -5
- package/dist/contracts.mjs.map +1 -1
- package/dist/index.d.mts +309 -21
- package/dist/index.d.ts +309 -21
- package/dist/index.js +222 -45
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +222 -45
- package/dist/index.mjs.map +1 -1
- package/dist/map/leaflet.d.mts +6 -0
- package/dist/map/leaflet.d.ts +6 -0
- package/dist/map/leaflet.js +4 -0
- package/dist/map/leaflet.js.map +1 -0
- package/dist/map/leaflet.mjs +4 -0
- package/dist/map/leaflet.mjs.map +1 -0
- package/dist/styles.css +1 -1
- package/package.json +7 -2
package/components/ui/avatar.md
CHANGED
|
@@ -2,16 +2,23 @@
|
|
|
2
2
|
name: Avatar
|
|
3
3
|
import: "@gradeui/ui"
|
|
4
4
|
subcomponents: [AvatarImage, AvatarFallback]
|
|
5
|
+
sizes: [2xs, xs, sm, md, lg, xl]
|
|
5
6
|
props:
|
|
6
|
-
-
|
|
7
|
+
- size? (2xs | xs | sm | md | lg | xl) — t-shirt scale, 20px → 80px; default md (40px). xs for chat message rows, sm for comments/dense threads, lg/xl for profile headers. Prefer this over h-*/w-* className utilities.
|
|
7
8
|
- AvatarImage: src, alt
|
|
8
|
-
- AvatarFallback:
|
|
9
|
+
- AvatarFallback: tone? (muted | primary | violet | amber | emerald | sky | rose | plum | lime) — tinted bg/text pair. Reach for explicit tones when each author needs a stable colour mapping (chat avatars, comment threads, member lists); default muted.
|
|
10
|
+
- AvatarFallback: children — initials (or a small icon), rendered while the image loads or when it fails
|
|
9
11
|
when_to_use: User/entity identity for PEOPLE — profile pictures, author rows, member lists, account headers. Circular by default; the AvatarFallback initials read as a person's name. Always include AvatarFallback so load failure doesn't leave a gap.
|
|
10
|
-
composes_with: [Card (in CardHeader), Table cells, Badge (placed next to for status), Skeleton (loading state)]
|
|
12
|
+
composes_with: [Card (in CardHeader), Table cells, Badge (placed next to for status), Skeleton (loading state), Message (in the avatar slot)]
|
|
11
13
|
aliases: [profile picture, user image, account image, avatar, person glyph, user avatar, profile image, react native avatar]
|
|
12
14
|
notes: |
|
|
13
15
|
Anti-patterns to avoid:
|
|
14
16
|
|
|
17
|
+
- DO NOT pass `initials` as a prop on <Avatar> — that prop does not
|
|
18
|
+
exist. Initials are the CHILDREN of <AvatarFallback>:
|
|
19
|
+
`<Avatar><AvatarFallback>AL</AvatarFallback></Avatar>`.
|
|
20
|
+
- DO NOT size with className utilities (h-7 w-7) — use the `size`
|
|
21
|
+
prop so the scale stays on the t-shirt tokens.
|
|
15
22
|
- DO NOT use Avatar for album art, posters, product photos, landscape
|
|
16
23
|
images, or anything that isn't a person. Use <MediaSurface> with the
|
|
17
24
|
appropriate `hint` ("album", "poster", "product", "landscape", etc.) —
|
|
@@ -27,3 +34,21 @@ notes: |
|
|
|
27
34
|
<AvatarFallback>AL</AvatarFallback>
|
|
28
35
|
</Avatar>
|
|
29
36
|
```
|
|
37
|
+
|
|
38
|
+
```jsx
|
|
39
|
+
// Chat / comment rows — small size + stable per-author tone.
|
|
40
|
+
<Avatar size="xs">
|
|
41
|
+
<AvatarFallback tone="violet">A</AvatarFallback>
|
|
42
|
+
</Avatar>
|
|
43
|
+
<Avatar size="sm">
|
|
44
|
+
<AvatarFallback tone="amber">B</AvatarFallback>
|
|
45
|
+
</Avatar>
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
```jsx
|
|
49
|
+
// Profile header — large, with image + initials fallback.
|
|
50
|
+
<Avatar size="lg">
|
|
51
|
+
<AvatarImage src="/ali.jpg" alt="Ali Driver" />
|
|
52
|
+
<AvatarFallback tone="primary">AD</AvatarFallback>
|
|
53
|
+
</Avatar>
|
|
54
|
+
```
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
# GradeLoader
|
|
2
|
+
|
|
3
|
+
The branded indeterminate loader — the Grade G-arrow mark with a diagonal
|
|
4
|
+
shimmer sweeping through it. Use it for EVERY "working, unknown duration"
|
|
5
|
+
moment instead of a generic spinner: fetching, compiling, warming a shader,
|
|
6
|
+
waiting on AI.
|
|
7
|
+
|
|
8
|
+
props:
|
|
9
|
+
- size?: "sm" | "md" | "lg" | "xl" | number — mark size (16/24/32/48px). Default "md".
|
|
10
|
+
- label?: string — accessible status text; shown visually with showLabel. Default "Loading…"; pass "" to silence.
|
|
11
|
+
- showLabel?: boolean — render the label as a caption under the mark. Default false.
|
|
12
|
+
|
|
13
|
+
when_to_use:
|
|
14
|
+
- Indeterminate waits: data fetching, AI generation in flight, preview compiling, media/shader warm-up, route transitions.
|
|
15
|
+
- Centered in an empty panel/card region while its content loads (pair with size="lg" + showLabel).
|
|
16
|
+
- NOT for determinate progress — use Progress when you can show a fraction.
|
|
17
|
+
- NOT a skeleton — use Skeleton when the layout shape is known and content is imminent.
|
|
18
|
+
|
|
19
|
+
composes_with:
|
|
20
|
+
- Card / panel bodies (centered placeholder state)
|
|
21
|
+
- Button (size="sm" inline while an action is pending)
|
|
22
|
+
- Motion scene boundaries / media surfaces while heavy content warms
|
|
23
|
+
- EmptyState (loading precursor before empty/error variants)
|
|
24
|
+
|
|
25
|
+
aliases: loader, spinner, loading indicator, busy, indeterminate, grade mark loader, branded spinner
|
|
26
|
+
|
|
27
|
+
notes:
|
|
28
|
+
- Paints with currentColor — set text colour on a parent (`text-muted-foreground`, `text-white` over footage).
|
|
29
|
+
- The shimmer highlights with oklch(var(--brand-1)) when brand pops are present; degrades to currentColor.
|
|
30
|
+
- prefers-reduced-motion swaps the sweep for a gentle opacity pulse.
|
|
31
|
+
- Announces via role="status"; the label is always available to screen readers.
|
package/components/ui/input.md
CHANGED
|
@@ -3,7 +3,8 @@ name: Input
|
|
|
3
3
|
import: "@gradeui/ui"
|
|
4
4
|
props:
|
|
5
5
|
- type?: string (text | email | password | number | search | url | tel | date)
|
|
6
|
-
-
|
|
6
|
+
- placeholder?: string — hint text shown while the input is empty. Model it explicitly (not just a native passthrough) so generated screens carry placeholders and the validator accepts them.
|
|
7
|
+
- size?: "default" | "sm" | "xs" — control density. `default` (h-9) for forms; `sm` (h-8) and `xs` (h-7) for dense tool panels like the inspector. NOTE: pre-unification scale — see Figma parity audit; due to migrate to the t-shirt scale (xs 24 | sm 28 | md 32 | lg 40, default→md).
|
|
7
8
|
- startSlot?: ReactNode — adornment rendered inside the leading edge (icon, prefix, currency symbol). Non-interactive by default so clicks focus the input.
|
|
8
9
|
- endSlot?: ReactNode — adornment rendered inside the trailing edge (unit like "px", a clear button, a stepper). Same pointer rules as startSlot.
|
|
9
10
|
- All native input HTML attrs (value, onChange, placeholder, disabled, required)
|
package/components/ui/logo.md
CHANGED
|
@@ -5,9 +5,10 @@ subcomponents: []
|
|
|
5
5
|
props:
|
|
6
6
|
- sources?: LogoSources — artwork keyed by lockup then appearance:
|
|
7
7
|
{ square?: { light?, dark?, mono? }, horizontal?: {...}, icon?: {...} }.
|
|
8
|
-
Each slot is any node (inline <svg>, <img>, component).
|
|
9
|
-
and
|
|
10
|
-
|
|
8
|
+
Each slot is any node (inline <svg>, <img>, component). OMIT ENTIRELY
|
|
9
|
+
and the GRADE MARK renders (the square G-arrow, painted with
|
|
10
|
+
currentColor so it sits correctly on any surface). A bare <Logo /> is
|
|
11
|
+
always correct branding.
|
|
11
12
|
- lockup?: "square" | "horizontal" | "icon" (default "horizontal")
|
|
12
13
|
- mode?: "light" | "dark" (default "light") — the background the logo sits on
|
|
13
14
|
- mono?: boolean (default false) — use the single-colour artwork (inherits currentColor)
|
|
@@ -16,19 +17,31 @@ props:
|
|
|
16
17
|
- decorative?: boolean — aria-hidden when the name is already nearby
|
|
17
18
|
- href?: string — renders the logo as a link (logo-links-home)
|
|
18
19
|
- className?: string
|
|
19
|
-
when_to_use:
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
20
|
+
when_to_use: ALWAYS use <Logo> wherever a screen carries a brand mark —
|
|
21
|
+
app-shell headers, sidenav headers, toolbars, footers, sign-in pages, hero
|
|
22
|
+
navs, splash states, Motion network-bug overlays. NEVER fake a brand with
|
|
23
|
+
placeholder text, initials in a circle, a generic lucide icon, or a bare
|
|
24
|
+
<img>. When the user hasn't named a brand or supplied artwork, render a
|
|
25
|
+
bare <Logo /> — it defaults to the GRADE mark (the square G-arrow), which
|
|
26
|
+
is the intended branding for unbranded screens. When the user names their
|
|
27
|
+
own brand, pass their artwork via `sources` (with a `label`). Built-in
|
|
28
|
+
variations — square for tight spaces, horizontal lockup for headers,
|
|
29
|
+
monochrome for busy/inverted surfaces — all switchable by prop.
|
|
30
|
+
composes_with: [AppShell, AppShellHeader, Sidebar, SidebarHeader, Toolbar, MotionOverlay, Row, Stack]
|
|
31
|
+
aliases: [logo, brand, brandmark, wordmark, lockup, brand logo, app logo, logotype, grade mark, g arrow]
|
|
27
32
|
---
|
|
28
33
|
|
|
29
34
|
```jsx
|
|
30
|
-
//
|
|
31
|
-
//
|
|
35
|
+
// THE DEFAULT — no brand named, no artwork supplied: the Grade mark.
|
|
36
|
+
// Size it and set the surrounding text colour; nothing else needed.
|
|
37
|
+
<Row gap="sm" align="center" className="text-foreground">
|
|
38
|
+
<Logo lockup="square" size="sm" decorative />
|
|
39
|
+
<span className="text-sm font-semibold">Grade</span>
|
|
40
|
+
</Row>
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
```jsx
|
|
44
|
+
// A branded screen: supply the brand's own artwork per slot.
|
|
32
45
|
<Logo
|
|
33
46
|
lockup="horizontal"
|
|
34
47
|
mode="dark"
|
|
@@ -42,8 +55,21 @@ aliases: [logo, brand, brandmark, wordmark, lockup, brand logo, app logo, logoty
|
|
|
42
55
|
/>
|
|
43
56
|
```
|
|
44
57
|
|
|
58
|
+
```jsx
|
|
59
|
+
// In a Motion's broadcast layer — the network bug is a Logo, not a div.
|
|
60
|
+
<MotionOverlay zone="top-right">
|
|
61
|
+
<span className="text-white">
|
|
62
|
+
<Logo lockup="square" size="sm" label="Grade" />
|
|
63
|
+
</span>
|
|
64
|
+
</MotionOverlay>
|
|
65
|
+
```
|
|
66
|
+
|
|
45
67
|
### Anti-patterns
|
|
46
68
|
|
|
69
|
+
DO NOT fake a brand mark with initials in a circle, placeholder text like
|
|
70
|
+
"LOGO", or a generic lucide icon — `<Logo />` with no props IS the correct
|
|
71
|
+
unbranded default (it renders the Grade mark).
|
|
72
|
+
|
|
47
73
|
DO NOT drop a bare `<img src="logo.png">` in a toolbar/sidenav/footer when you
|
|
48
74
|
want light/dark or square/horizontal switching — use `<Logo>` so the variant
|
|
49
75
|
is a prop.
|
package/components/ui/map.md
CHANGED
|
@@ -4,16 +4,22 @@ import: "@gradeui/ui"
|
|
|
4
4
|
subcomponents: [MapMarker]
|
|
5
5
|
aliases: [map, maps, mapbox, maplibre, google maps, geo, location, latlng, coordinates, marker, pin, airbnb, listings, fleet, real estate, logistics, map view, mapkit, mapview, react native maps, rn maps]
|
|
6
6
|
props:
|
|
7
|
-
- provider — "maplibre" (default, free, no key) | "mapbox" (needs accessToken) | "google" (needs apiKey). Switching is one prop change.
|
|
8
|
-
- center — `[lng, lat]` tuple. ALWAYS lng first. Required.
|
|
9
|
-
- zoom — number, 0–22. Required.
|
|
10
|
-
- bounds — `[[swLng, swLat], [neLng, neLat]]`. When set, takes precedence over center+zoom.
|
|
11
|
-
- appearance — "light" | "dark" | "satellite" | "auto" (default "auto", follows GradeThemeProvider mode).
|
|
12
|
-
- hoveredId — controlled string id, pairs with onHoveredIdChange. The matching MapMarker gets `data-gds-state="hovered"` automatically. This is how you build list ↔ map two-way sync.
|
|
13
|
-
- interactive — false freezes pan/zoom, useful for static cards.
|
|
14
|
-
- onLoad(handle) / onError(error) — handle exposes flyTo, panTo, fitBounds, getCenter, getZoom, getBounds, instance.
|
|
15
|
-
- tilerKey (maplibre)
|
|
16
|
-
- accessToken (mapbox),
|
|
7
|
+
- Map: provider — "maplibre" (default, free, no key) | "mapbox" (needs accessToken) | "google" (needs apiKey). Switching is one prop change.
|
|
8
|
+
- Map: center — `[lng, lat]` tuple. ALWAYS lng first. Required.
|
|
9
|
+
- Map: zoom — number, 0–22. Required.
|
|
10
|
+
- Map: bounds — `[[swLng, swLat], [neLng, neLat]]`. When set, takes precedence over center+zoom.
|
|
11
|
+
- Map: appearance — "light" | "dark" | "satellite" | "auto" (default "auto", follows GradeThemeProvider mode).
|
|
12
|
+
- Map: hoveredId — controlled string id, pairs with onHoveredIdChange. The matching MapMarker gets `data-gds-state="hovered"` automatically. This is how you build list ↔ map two-way sync.
|
|
13
|
+
- Map: interactive — false freezes pan/zoom, useful for static cards.
|
|
14
|
+
- Map: onLoad(handle) / onError(error) — handle exposes flyTo, panTo, fitBounds, getCenter, getZoom, getBounds, instance.
|
|
15
|
+
- Map: tilerKey? — MapLibre only (provider="maplibre"). Optional everywhere: omit on `gradeui.com`/`localhost` and the referrer-locked demo key is used; set it only when embedding off-domain. The contract never requires it.
|
|
16
|
+
- Map: accessToken? — Mapbox only. Pass it whenever provider="mapbox" — the component itself enforces this at runtime (throws a clear `provider="mapbox" requires an accessToken prop` error via onError if missing). It is OPTIONAL in the contract on purpose, so the validator never demands it from maplibre/google maps.
|
|
17
|
+
- Map: apiKey? — Google only. Pass it whenever provider="google" — the component enforces it at runtime (throws `provider="google" requires an apiKey prop` via onError if missing). OPTIONAL in the contract on purpose, so it's never demanded from maplibre/mapbox.
|
|
18
|
+
- MapMarker: id — string. Required. Stable marker id; pair with Map's `hoveredId` for list↔map hover sync.
|
|
19
|
+
- MapMarker: at — `[lng, lat]` tuple. Required. THE coordinate prop. ALWAYS lng first. The prop is literally named `at` — it is NOT `lngLat`, `coordinates`, `position`, `latLng`, `center`, or separate `lng`/`lat` props. Passing any other name leaves the marker coord `undefined`, and MapLibre throws on mount, crashing the WHOLE screen in every renderer. When in doubt, copy the `airbnb-listings` scaffold: `<MapMarker id={l.id} at={l.coords}>`.
|
|
20
|
+
- MapMarker: anchor — "center" | "bottom" (default "bottom", pin tip sits on the coord). Only these two values.
|
|
21
|
+
- MapMarker: onClick — handler called with `({ id, coords, native })` on marker click.
|
|
22
|
+
- MapMarker: children — DOM rendered as the marker (Badge, Card, Avatar, or any element). Inherits `--gds-*` tokens.
|
|
17
23
|
when_to_use: Any layout that needs a real map — listings (real estate, Airbnb-style), fleet/logistics dashboards, store locators, anywhere a user picks a location from a viewport. Reach for the controlled `hoveredId` prop when a sibling list and the map need to highlight each other.
|
|
18
24
|
composes_with: [Card (as marker content), Badge, Avatar, Button, Row, Stack, Skeleton]
|
|
19
25
|
---
|
|
@@ -69,8 +75,11 @@ Provider swap — one line:
|
|
|
69
75
|
<Map provider="google" apiKey={env.GOOGLE_MAPS_KEY} center={[-0.1, 51.5]} zoom={11} />
|
|
70
76
|
```
|
|
71
77
|
|
|
78
|
+
The contract is deliberately provider-AGNOSTIC. `tilerKey`, `accessToken`, and `apiKey` are all OPTIONAL in the contract so any valid provider config validates — a maplibre map needs no key on-domain, a mapbox map carries `accessToken`, a google map carries `apiKey`, and none of them trip a `missing-required` error for a key another provider uses. The knowledge of which key a provider needs lives in the component/adapter at runtime, not the static contract: maplibre falls back to the referrer-locked demo key; mapbox throws `provider="mapbox" requires an accessToken prop`; google throws `provider="google" requires an apiKey prop` — each surfaced via `onError({ code: "api-key-missing" })`. Maintainers: do NOT re-mark these credentials required in the contract — that's the bug that blanket-required all three and broke on-domain maplibre maps.
|
|
79
|
+
|
|
72
80
|
ANTI-PATTERNS — don't do these:
|
|
73
81
|
|
|
82
|
+
- DO NOT name the marker coordinate prop anything other than `at`. It is `<MapMarker id="…" at={[lng, lat]} />` — NOT `lngLat`, `coordinates`, `position`, `latLng`, or `center`. A wrong name passes JSX validation (the validator only checks `<Map>`'s contract, not subcomponent prop names) but registers an `undefined` coord, so MapLibre throws on mount and the whole screen fails to render.
|
|
74
83
|
- DO NOT pass `{ lat, lng }` objects. Coordinates are ALWAYS `[lng, lat]` tuples. Google's adapter handles the object conversion internally.
|
|
75
84
|
- DO NOT hand-roll an iframe with a Google Maps embed URL. Use `<Map provider="google" apiKey={...}>`.
|
|
76
85
|
- DO NOT use `useRef` + `mapRef.current.flyTo(id)` on list-hover when `hoveredId` already does it controlled.
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: Motion
|
|
3
|
+
import: "@gradeui/ui"
|
|
4
|
+
subcomponents: [MotionScene, MotionScreen, MotionText]
|
|
5
|
+
props:
|
|
6
|
+
- view?: "play" | "strip" (default "play") — play runs the film; strip lays
|
|
7
|
+
scenes out left-to-right as labelled cards (the arrangement view).
|
|
8
|
+
- aspect?: "auto" | "16/9" | "9/16" | "1/1" (default "auto") — fixed artboard
|
|
9
|
+
aspect, letterboxed into the container. "9/16" is the TikTok / Reels /
|
|
10
|
+
Shorts format; "auto" fills responsively. Strip cards adopt the ratio.
|
|
11
|
+
- stage?: string — CSS background of the persistent stage behind every scene.
|
|
12
|
+
- backdrop?: React.ReactNode — live layer behind all scenes (image, gradient, <ThreeScene>).
|
|
13
|
+
- autoplay?: boolean (default true)
|
|
14
|
+
- loop?: boolean (default false — a motion is a movie, it ends)
|
|
15
|
+
- controls?: boolean (default true) — play/pause, restart, scene dots (random access)
|
|
16
|
+
- children: <MotionScene> elements, in order.
|
|
17
|
+
- "MotionScene: label?, durationMs? (MINIMUM runtime; the whole clock when
|
|
18
|
+
nothing inside keeps time, default 4000), fill? (scene background over
|
|
19
|
+
the stage — e.g. a white title card), transition? ('fade' | 'slide-up' |
|
|
20
|
+
'slide-down' | 'slide-left' | 'slide-right' | 'pop' | 'zoom' |
|
|
21
|
+
'wipe-circle' (a circular mask wipe) | 'none' — how the scene ARRIVES.
|
|
22
|
+
The OUTGOING scene stays visible as a frozen layer UNDERNEATH for the
|
|
23
|
+
transition window, so slides reveal it and wipes cut through it),
|
|
24
|
+
transitionMs? (timing override; each transition has a sensible
|
|
25
|
+
default), children (ANY JSX)."
|
|
26
|
+
- "MotionScreen: device? ('desktop' | 'mobile'), shots? (its OWN ScreenAnimator
|
|
27
|
+
camera), virtualWidth?, spotlight?, cursor?, enter? (default FALSE —
|
|
28
|
+
the offscreen fly-in reads badly inside a small frame; use scene
|
|
29
|
+
transition / animate for entrances), animate? ('rise' |
|
|
30
|
+
'tilt-settle' — entrances; 'float' | 'drift' — ambient loops; 'none'
|
|
31
|
+
default — animates the FRAME in place within the scene, composable with
|
|
32
|
+
the camera inside; pair entrances with enter={false}), screenId?
|
|
33
|
+
(provenance, ignored at render), children (the screen content, copied in)."
|
|
34
|
+
- "MotionText: template? ('title' | 'lower-third' | 'section-break' |
|
|
35
|
+
'broadcast' — the TV-style full-width brand-blue band that sits over
|
|
36
|
+
the screen | 'ticker' — a news-style marquee bar pinned to the very
|
|
37
|
+
bottom: heading is the uppercase label chip, text scrolls in an
|
|
38
|
+
infinite loop | 'stat' — an oversized statistic slate: heading is the
|
|
39
|
+
number slamming in at up to 180px, text is the label fading up below |
|
|
40
|
+
'quote' — an editorial pull-quote with a decorative oversized opening
|
|
41
|
+
mark: heading is the quote, text the em-dash attribution), heading,
|
|
42
|
+
text?, durationMs?, tone? ('light' | 'dark'). 'ticker' pairs well
|
|
43
|
+
inside MotionOverlay zone='bottom' for a film-level ticker that runs
|
|
44
|
+
across every scene."
|
|
45
|
+
- "MotionOverlay: the BROADCAST layer — a peer of MotionScene inside
|
|
46
|
+
<Motion> that renders above every scene for the film's runtime:
|
|
47
|
+
network-bug logo, live wall clock (which keeps ticking when playback
|
|
48
|
+
pauses — better-than-video proof it's live), ticker, persistent
|
|
49
|
+
video. zone? ('top-left' | 'top' | 'top-right' | 'center' |
|
|
50
|
+
'bottom-left' | 'bottom' | 'bottom-right' | 'lower-third'),
|
|
51
|
+
fromScene?/toScene? (scene-range visibility — overlays are a second
|
|
52
|
+
timeline; defaults = always on), interactive? (re-enable pointer
|
|
53
|
+
events), children (any JSX)."
|
|
54
|
+
when_to_use: A directed sequence of scenes on one persistent stage — the
|
|
55
|
+
text → demo → video → text grammar of a modern product demo. A scene is a
|
|
56
|
+
stage MOMENT holding arbitrary JSX; screens go inside scenes via
|
|
57
|
+
<MotionScreen> (each with its own camera — two side by side shows
|
|
58
|
+
mobile + desktop), templated text via <MotionText>, video/images as plain
|
|
59
|
+
children. A scene advances when all its timed children finish (camera tours,
|
|
60
|
+
text templates), or after durationMs when nothing keeps time. Use
|
|
61
|
+
<ScreenAnimator> alone for a single directed screen; use <Motion> the moment
|
|
62
|
+
there's a sequence.
|
|
63
|
+
composes_with: [ScreenAnimator, ThreeScene, VideoPlayer, AppShell, the whole component set (scenes hold screens)]
|
|
64
|
+
aliases: [motion, grade motion, scenes, sequence, demo reel, product video, launch video, title card, lower third, section break, multi-scene, storyboard]
|
|
65
|
+
---
|
|
66
|
+
|
|
67
|
+
```jsx
|
|
68
|
+
// Title card → dashboard at two viewports → video clip. One stage throughout.
|
|
69
|
+
<Motion>
|
|
70
|
+
<MotionScene label="Hook">
|
|
71
|
+
<MotionText template="title" heading="Meet the new pipeline" text="From prompt to product" />
|
|
72
|
+
</MotionScene>
|
|
73
|
+
<MotionScene label="Dashboard">
|
|
74
|
+
<MotionScreen device="mobile" shots={[{ zoom: 1, hold: 2000 }, { zoom: 2, cx: 0.5, cy: 0.25, hold: 2400, label: "Live on mobile" }]}>
|
|
75
|
+
<DashboardMobile />
|
|
76
|
+
</MotionScreen>
|
|
77
|
+
<MotionScreen shots={[{ zoom: 1, hold: 2400 }, { zoom: 2.2, cx: 0.22, cy: 0.3, hold: 2600, label: "Revenue up 24%" }]}>
|
|
78
|
+
<Dashboard />
|
|
79
|
+
</MotionScreen>
|
|
80
|
+
</MotionScene>
|
|
81
|
+
<MotionScene label="Proof">
|
|
82
|
+
<MotionText template="stat" heading="4.2x" text="Faster from prompt to product" />
|
|
83
|
+
</MotionScene>
|
|
84
|
+
<MotionScene label="Word">
|
|
85
|
+
<MotionText template="quote" heading="It feels like the demo directs itself." text="Head of Design, Acme" />
|
|
86
|
+
</MotionScene>
|
|
87
|
+
<MotionScene label="Clip" durationMs={6000}>
|
|
88
|
+
<video src="/clip.mp4" autoPlay muted style={{ borderRadius: 12, maxWidth: "70%" }} />
|
|
89
|
+
<MotionText template="ticker" heading="Live" text="Grade Motion ships scene transitions, broadcast overlays and a directed camera" />
|
|
90
|
+
</MotionScene>
|
|
91
|
+
</Motion>
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
### Anti-patterns
|
|
95
|
+
|
|
96
|
+
DO NOT wrap a whole scene in <ScreenAnimator> — the camera belongs to each
|
|
97
|
+
<MotionScreen> inside the scene, not to the scene. A scene with two screens
|
|
98
|
+
has two cameras.
|
|
99
|
+
|
|
100
|
+
durationMs is a MINIMUM runtime, not just a fallback: a scene with a 3s
|
|
101
|
+
lower-third and `durationMs={16000}` runs the full 16s (the caption ending
|
|
102
|
+
early never cuts a long visual mid-flight). Timed children can extend a
|
|
103
|
+
scene PAST the floor; with no timed children, durationMs is the whole clock.
|
|
104
|
+
|
|
105
|
+
DO NOT use it as a layout wrapper — like ScreenAnimator it positions
|
|
106
|
+
`absolute inset-0` and takes over the frame.
|
|
107
|
+
|
|
108
|
+
DO NOT worry about reduced motion — the play view falls back to the strip
|
|
109
|
+
(see everything, move nothing).
|