@syncedco/motion 0.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/CHANGELOG.md +106 -0
- package/CODE_OF_CONDUCT.md +26 -0
- package/CONTRIBUTING.md +100 -0
- package/LICENSE +22 -0
- package/README.md +278 -0
- package/SECURITY.md +36 -0
- package/SUPPORT.md +38 -0
- package/TRADEMARKS.md +14 -0
- package/bin/synced-motion.mjs +5 -0
- package/dist/astro.d.ts +15 -0
- package/dist/astro.js +51 -0
- package/dist/astro.js.map +1 -0
- package/dist/chunk-24KKECMK.js +281 -0
- package/dist/chunk-24KKECMK.js.map +1 -0
- package/dist/chunk-3WUVBPYX.js +162 -0
- package/dist/chunk-3WUVBPYX.js.map +1 -0
- package/dist/chunk-HCWQV65E.js +20 -0
- package/dist/chunk-HCWQV65E.js.map +1 -0
- package/dist/chunk-JET2MYEG.js +2287 -0
- package/dist/chunk-JET2MYEG.js.map +1 -0
- package/dist/chunk-KA3OPI4F.js +40 -0
- package/dist/chunk-KA3OPI4F.js.map +1 -0
- package/dist/chunk-ML7HTCZT.js +642 -0
- package/dist/chunk-ML7HTCZT.js.map +1 -0
- package/dist/chunk-NIXAEIHN.js +23 -0
- package/dist/chunk-NIXAEIHN.js.map +1 -0
- package/dist/chunk-VOIQPPQZ.js +299 -0
- package/dist/chunk-VOIQPPQZ.js.map +1 -0
- package/dist/chunk-WCG7TQSH.js +31 -0
- package/dist/chunk-WCG7TQSH.js.map +1 -0
- package/dist/cli.js +382 -0
- package/dist/cli.js.map +1 -0
- package/dist/full.d.ts +13 -0
- package/dist/full.js +201 -0
- package/dist/full.js.map +1 -0
- package/dist/index.d.ts +249 -0
- package/dist/index.js +183 -0
- package/dist/index.js.map +1 -0
- package/dist/lenis.d.ts +19 -0
- package/dist/lenis.js +7 -0
- package/dist/lenis.js.map +1 -0
- package/dist/mcp.d.ts +16 -0
- package/dist/mcp.js +82 -0
- package/dist/mcp.js.map +1 -0
- package/dist/plugins.d.ts +14 -0
- package/dist/plugins.js +17 -0
- package/dist/plugins.js.map +1 -0
- package/dist/react.d.ts +7 -0
- package/dist/react.js +32 -0
- package/dist/react.js.map +1 -0
- package/dist/recipes.d.ts +76 -0
- package/dist/recipes.js +129 -0
- package/dist/recipes.js.map +1 -0
- package/dist/styles.css +57 -0
- package/dist/styles.css.map +1 -0
- package/dist/svelte.d.ts +10 -0
- package/dist/svelte.js +30 -0
- package/dist/svelte.js.map +1 -0
- package/dist/vue.d.ts +13 -0
- package/dist/vue.js +35 -0
- package/dist/vue.js.map +1 -0
- package/dist/wordpress.d.ts +15 -0
- package/dist/wordpress.js +49 -0
- package/dist/wordpress.js.map +1 -0
- package/docs/API.md +176 -0
- package/docs/ATTRIBUTE-API.md +358 -0
- package/docs/CAPABILITIES.md +82 -0
- package/docs/INTEGRATIONS.md +92 -0
- package/docs/PERFORMANCE.md +150 -0
- package/docs/README.md +35 -0
- package/docs/RECIPE-REFERENCE.md +1358 -0
- package/docs/RECIPES.md +115 -0
- package/docs/TOOLING.md +65 -0
- package/docs/WEBFLOW-MIGRATION.md +39 -0
- package/package.json +167 -0
|
@@ -0,0 +1,642 @@
|
|
|
1
|
+
import {
|
|
2
|
+
createMotionRegistry,
|
|
3
|
+
validateMotionRecipe
|
|
4
|
+
} from "./chunk-24KKECMK.js";
|
|
5
|
+
import {
|
|
6
|
+
builtinMotionSpecs
|
|
7
|
+
} from "./chunk-JET2MYEG.js";
|
|
8
|
+
|
|
9
|
+
// src/recipes/authoring.js
|
|
10
|
+
var SHARED_NO_JS = /* @__PURE__ */ Object.freeze({
|
|
11
|
+
behavior: "Content and controls remain available in their authored final state."
|
|
12
|
+
});
|
|
13
|
+
var SHARED_ACCESSIBILITY = /* @__PURE__ */ Object.freeze({
|
|
14
|
+
notes: "Motion does not change document order; interactive semantics remain native."
|
|
15
|
+
});
|
|
16
|
+
var SHARED_PREVIEW = /* @__PURE__ */ Object.freeze({ fixture: "default", viewport: "standard", activation: "auto" });
|
|
17
|
+
function attributesForSelector(selector) {
|
|
18
|
+
return [...selector.matchAll(/\[([\w-]+)(?:=["']?([^\]"']+)["']?)?\]/g)].map(([, name, value]) => value === void 0 ? name : `${name}="${value}"`).join(" ");
|
|
19
|
+
}
|
|
20
|
+
function fixtureElement(selector, label) {
|
|
21
|
+
const attributes = attributesForSelector(selector);
|
|
22
|
+
if (/range/.test(selector)) return `<input ${attributes} type="range" min="0" max="100" value="50" aria-label="${label}">`;
|
|
23
|
+
if (/video/.test(selector)) return `<video ${attributes} controls aria-label="${label}"></video>`;
|
|
24
|
+
if (/(?:trigger|control|button)/.test(selector)) return `<button ${attributes} type="button">${label}</button>`;
|
|
25
|
+
if (/link/.test(selector)) return `<a ${attributes} href="#">${label}</a>`;
|
|
26
|
+
if (/(?:heading|title)/.test(selector)) return `<h2 ${attributes}>${label}</h2>`;
|
|
27
|
+
if (/(?:card|panel|item|chapter|step|detail)/.test(selector)) return `<article ${attributes}><h3>${label}</h3><p>Readable supporting content.</p></article>`;
|
|
28
|
+
if (/(?:image|media)/.test(selector)) return `<figure ${attributes}><div role="img" aria-label="${label}"></div></figure>`;
|
|
29
|
+
return `<div ${attributes}>${label}</div>`;
|
|
30
|
+
}
|
|
31
|
+
function defaultFixtureMarkup(id, selector, slots) {
|
|
32
|
+
const root = attributesForSelector(selector);
|
|
33
|
+
if (id === "split-lines-rise") return `<h2 ${root}>Responsive lines reveal without changing the reading order.</h2>`;
|
|
34
|
+
if (id === "split-words-cascade") return `<h2 ${root}>Each readable word enters in a measured sequence.</h2>`;
|
|
35
|
+
if (id === "split-chars-shimmer") return `<h2 ${root}>Character rhythm with one accessible heading.</h2>`;
|
|
36
|
+
if (id === "typewriter-announce") return `<h2 ${root}>The complete announcement remains available to assistive technology.</h2>`;
|
|
37
|
+
if (id === "reveal-stagger-cascade") return `<ul ${root}><li data-motion-stagger-item>Discover</li><li data-motion-stagger-item>Compose</li><li data-motion-stagger-item>Ship</li></ul>`;
|
|
38
|
+
if (id === "pinned-steps") return `<section ${root}><div data-motion-pin-target><nav aria-label="Story steps"><a href="#step-one" data-motion-step-link>Context</a><a href="#step-two" data-motion-step-link>Outcome</a></nav><article id="step-one" data-motion-step-panel><h2>Start with context</h2><p>The first state remains readable.</p></article><article id="step-two" data-motion-step-panel><h2>Show the outcome</h2><p>The second state follows in source order.</p></article></div></section>`;
|
|
39
|
+
if (id === "pinned-statement") return `<section ${root}><div data-motion-statement-pin><p data-motion-statement-label>Our approach</p><h2 data-motion-statement-heading><span data-motion-statement-hero-accent aria-hidden="true">Build</span><span data-motion-statement-inline-accent>Build</span> for lasting clarity.</h2><p data-motion-statement-lead>One strong idea resolves into useful detail.</p><div data-motion-statement-details><p data-motion-statement-detail>Semantic structure stays intact.</p><p data-motion-statement-detail>Motion only guides attention.</p></div></div></section>`;
|
|
40
|
+
if (id === "pinned-founder-story") return `<section ${root}><div data-motion-founder-statement-bg aria-hidden="true"></div><div data-motion-founder-metrics-bg aria-hidden="true"></div><div data-motion-founder-content><h2 data-motion-founder-heading>Built from first-hand experience.</h2></div><div data-motion-founder-metrics><strong data-motion-founder-stat>12</strong><span data-motion-founder-stat-label>years learning in public</span><p data-motion-founder-detail>One clear principle.</p><p data-motion-founder-detail>Many measured iterations.</p></div></section>`;
|
|
41
|
+
if (id === "horizontal-comparison-slider") return `<figure ${root}><div aria-label="Before view">Before</div><div data-motion-comparison-after aria-label="After view">After</div><figcaption><label>Reveal after view <input data-motion-comparison-range type="range" min="0" max="100" value="50"></label></figcaption></figure>`;
|
|
42
|
+
if (id === "media-expand") return `<figure ${root}><p data-motion-media-expand-prompt>Scroll to explore the full image</p><span data-motion-media-expand-label="start" aria-hidden="true">Detail</span><div data-motion-media-expand-frame><img alt="A workshop table prepared for a design review"></div><span data-motion-media-expand-label="end" aria-hidden="true">Context</span><figcaption data-motion-media-expand-caption>The full scene adds context without replacing the caption.</figcaption></figure>`;
|
|
43
|
+
if (id === "image-focus-pan") return `<figure ${root}><img data-motion-focus-image alt="A landscape with a clear authored focal point"><figcaption>Subtle movement preserves the subject.</figcaption></figure>`;
|
|
44
|
+
if (id === "video-poster-play") return `<figure ${root}><img data-motion-video-poster-image alt="Video poster showing the product workspace"><video data-motion-video-element aria-label="Product workspace walkthrough"></video><figcaption><button data-motion-video-trigger type="button" aria-pressed="false">Play walkthrough</button></figcaption></figure>`;
|
|
45
|
+
if (id === "hover-media-switch") return `<section ${root}><div><button data-motion-hover-key="system" type="button">Motion system</button><button data-motion-hover-key="runtime" type="button">Runtime</button></div><figure data-motion-hover-media="system"><div role="img" aria-label="Motion system diagram"></div><figcaption>Compose named recipes.</figcaption></figure><figure data-motion-hover-media="runtime"><div role="img" aria-label="Runtime lifecycle diagram"></div><figcaption>Mount and clean up safely.</figcaption></figure></section>`;
|
|
46
|
+
if (id === "expand-panels") return `<section ${root}><article data-motion-expand-panel><h2><a href="#contracts">Contracts</a></h2><p>Understand every semantic hook.</p></article><article data-motion-expand-panel><h2><a href="#cleanup">Cleanup</a></h2><p>Return to the authored state.</p></article></section>`;
|
|
47
|
+
if (id === "hover-lift") return `<a ${root} href="#case-study"><strong>Read the case study</strong><span> See how the system behaves.</span></a>`;
|
|
48
|
+
if (id === "magnetic-action") return `<div ${root}><button data-motion-magnetic-action type="button">Start composing</button></div>`;
|
|
49
|
+
if (id === "accessible-menu") return `<nav ${root} aria-label="Preview navigation"><button data-motion-menu-trigger type="button" aria-expanded="false" aria-controls="preview-menu">Menu</button><div id="preview-menu" data-motion-menu-panel hidden><a data-motion-menu-item href="#work">Work</a><a data-motion-menu-item href="#about">About</a><button data-motion-menu-close type="button">Close menu</button></div></nav>`;
|
|
50
|
+
if (id === "dialog-overlay") return `<div ${root}><button data-motion-overlay-trigger type="button" aria-expanded="false" aria-controls="preview-dialog">Open details</button><dialog id="preview-dialog" data-motion-overlay-dialog aria-labelledby="preview-dialog-title"><h2 id="preview-dialog-title">Project details</h2><p>The native dialog retains its focus and escape semantics.</p><button data-motion-overlay-close type="button">Close</button></dialog></div>`;
|
|
51
|
+
if (id === "accordion-disclosure") return `<details ${root}><summary>What does motion own?</summary><div data-motion-accordion-panel><p>Animation orchestration, cleanup, and reduced-motion behavior.</p></div></details>`;
|
|
52
|
+
if (id === "command-palette") return `<div ${root}><button data-motion-overlay-trigger type="button" aria-expanded="false" aria-controls="preview-palette">Open commands</button><dialog id="preview-palette" data-motion-overlay-dialog aria-labelledby="preview-palette-title"><h2 id="preview-palette-title">Commands</h2><label>Search commands <input data-motion-command-input type="search"></label><button data-motion-overlay-close type="button">Close</button></dialog></div>`;
|
|
53
|
+
if (id === "nav-active-indicator") return `<nav ${root} aria-label="Section navigation"><a data-motion-nav-item aria-current="page" href="#overview">Overview</a><a data-motion-nav-item href="#recipes">Recipes</a><span data-motion-nav-active-indicator aria-hidden="true"></span></nav>`;
|
|
54
|
+
if (id === "marquee") return `<section ${root} aria-label="Capabilities"><div data-motion-marquee-track><span>Semantic hooks \xB7 Reduced motion \xB7 Cleanup \xB7 </span><span aria-hidden="true">Semantic hooks \xB7 Reduced motion \xB7 Cleanup \xB7 </span></div></section>`;
|
|
55
|
+
if (id === "counter-value") return `<output ${root} data-motion-counter-from="0" data-motion-counter-to="60"><span data-motion-counter-value>60</span> production recipes</output>`;
|
|
56
|
+
if (id === "looping-logo-belt") return `<section ${root} aria-label="Compatible platforms"><div data-motion-logo-belt-track><span>React</span><span>Vue</span><span>Svelte</span><span>Astro</span><span aria-hidden="true">React</span><span aria-hidden="true">Vue</span></div></section>`;
|
|
57
|
+
if (id === "flip-list-reorder") return `<ol ${root}><li data-motion-flip-item>Define the motion intent</li><li data-motion-flip-item>Choose semantic hooks</li><li data-motion-flip-item>Validate cleanup</li></ol>`;
|
|
58
|
+
if (id === "flip-card-to-detail") return `<article ${root}><h2>Recipe contract</h2><button data-motion-flip-card-trigger type="button" aria-expanded="false" aria-controls="recipe-detail">Show details</button><div id="recipe-detail" data-motion-flip-card-detail><p>Slots, fallbacks, dependencies, and performance are explicit.</p></div></article>`;
|
|
59
|
+
if (id === "flip-filter-grid") return `<section ${root}><div aria-label="Filter examples"><button data-motion-filter-control="all" type="button">All</button><button data-motion-filter-control="editorial" type="button">Editorial</button></div><article data-motion-filter-item="editorial"><h2>Founder story</h2></article><article data-motion-filter-item="product"><h2>Product explainer</h2></article></section>`;
|
|
60
|
+
if (id === "flip-nav-indicator") return `<nav ${root} aria-label="Recipe families"><a data-motion-flip-nav-item aria-current="page" href="#reveals">Reveals<span data-motion-flip-nav-indicator aria-hidden="true"></span></a><a data-motion-flip-nav-item href="#overlays">Overlays</a></nav>`;
|
|
61
|
+
if (id === "layout-accordion-grid") return `<section ${root} aria-label="Feature details"><article data-motion-layout-item><h2><button data-motion-layout-trigger type="button" aria-expanded="false">Motion contracts</button></h2><p>Typed recipes make intent explicit.</p></article><article data-motion-layout-item><h2><button data-motion-layout-trigger type="button" aria-expanded="false">Lifecycle safety</button></h2><p>Every mounted pattern has deterministic cleanup.</p></article></section>`;
|
|
62
|
+
if (id === "progress-ring") return `<section ${root}><svg viewBox="0 0 120 120" role="img" aria-label="Progress"><circle data-motion-progress-ring-value cx="60" cy="60" r="48" fill="none" stroke="currentColor" stroke-width="8" /></svg><output data-motion-progress-ring-label>100%</output></section>`;
|
|
63
|
+
if (id === "svg-line-draw") return `<section ${root}><svg viewBox="0 0 320 180" role="img" aria-label="Rising line"><path data-motion-svg-path d="M20 150 C90 30 210 150 300 30" fill="none" stroke="currentColor" stroke-width="6" /></svg></section>`;
|
|
64
|
+
if (id === "svg-path-morph") return `<section ${root}><svg viewBox="0 0 200 200" role="img" aria-label="Morphing shape"><path data-motion-svg-morph-source d="M100 20 L180 180 L20 180 Z" /><path data-motion-svg-morph-target d="M100 15 A85 85 0 1 1 99.9 15" aria-hidden="true" hidden /></svg><button data-motion-svg-trigger type="button" aria-pressed="false">Change shape</button></section>`;
|
|
65
|
+
if (id === "svg-icon-state") return `<section ${root}><svg viewBox="0 0 100 100" role="img" aria-label="Controlled icon"><path data-motion-svg-icon-source d="M20 50 L42 72 L82 25" fill="none" stroke="currentColor" stroke-width="10" /><path data-motion-svg-icon-target d="M22 22 L78 78 M78 22 L22 78" fill="none" stroke="currentColor" stroke-width="10" aria-hidden="true" hidden /></svg><button data-motion-svg-trigger type="button" aria-pressed="false">Toggle state</button></section>`;
|
|
66
|
+
if (id === "svg-orbit") return `<section ${root}><svg viewBox="0 0 320 200" role="img" aria-label="Orbital path"><path data-motion-svg-orbit-path d="M30 100 C90 10 230 10 290 100 C230 190 90 190 30 100 Z" fill="none" stroke="currentColor" /><circle data-motion-svg-orbit-subject cx="30" cy="100" r="10" /></svg></section>`;
|
|
67
|
+
if (id === "svg-signature-reveal") return `<section ${root}><svg viewBox="0 0 360 160" role="img" aria-label="Signature"><path d="M20 120 C60 10 70 150 120 70 S180 120 220 50" fill="none" stroke="currentColor" stroke-width="5" /><path d="M130 125 C200 105 270 110 340 90" fill="none" stroke="currentColor" stroke-width="4" /></svg></section>`;
|
|
68
|
+
const children = slots.filter((slot) => slot.name !== "root" && slot.required !== false).flatMap((slot) => Array.from({ length: slot.multiple ? 2 : 1 }, (_, index) => fixtureElement(slot.selector, `${slot.name} ${index + 1}`))).join("");
|
|
69
|
+
return `<section ${root}><header><h2>${id.split("-").map((word) => word[0].toUpperCase() + word.slice(1)).join(" ")}</h2><p>A semantic, readable authored state for this motion pattern.</p></header>${children}</section>`;
|
|
70
|
+
}
|
|
71
|
+
var motionRecipeAuthoring = /* @__PURE__ */ Object.freeze({
|
|
72
|
+
"reveal-rise": {
|
|
73
|
+
title: "Reveal rise",
|
|
74
|
+
description: "Reveal content upward with opacity.",
|
|
75
|
+
intent: "Introduce an element as it enters the viewport.",
|
|
76
|
+
family: "reveals-entrances",
|
|
77
|
+
tags: ["reveal", "entrance", "viewport"]
|
|
78
|
+
},
|
|
79
|
+
"reveal-stagger-cascade": {
|
|
80
|
+
title: "Stagger cascade",
|
|
81
|
+
description: "Cascade a local group into view.",
|
|
82
|
+
intent: "Introduce related items in a readable sequence.",
|
|
83
|
+
family: "reveals-entrances",
|
|
84
|
+
tags: ["stagger", "cascade", "list"]
|
|
85
|
+
},
|
|
86
|
+
"reveal-directional": {
|
|
87
|
+
title: "Directional reveal",
|
|
88
|
+
description: "Reveal content from a semantic direction.",
|
|
89
|
+
intent: "Introduce content with a restrained directional offset.",
|
|
90
|
+
family: "reveals-entrances",
|
|
91
|
+
tags: ["reveal", "direction", "entrance"]
|
|
92
|
+
},
|
|
93
|
+
"reveal-scale-in": {
|
|
94
|
+
title: "Scale reveal",
|
|
95
|
+
description: "Gently scale and fade a focal element into view.",
|
|
96
|
+
intent: "Introduce a focal card or media element without changing layout.",
|
|
97
|
+
family: "reveals-entrances",
|
|
98
|
+
tags: ["reveal", "scale", "entrance"]
|
|
99
|
+
},
|
|
100
|
+
"reveal-clip-wipe": {
|
|
101
|
+
title: "Clip wipe reveal",
|
|
102
|
+
description: "Reveal content through a bounded inline clip.",
|
|
103
|
+
intent: "Uncover media or editorial content with a directional wipe.",
|
|
104
|
+
family: "reveals-entrances",
|
|
105
|
+
tags: ["reveal", "clip", "wipe"]
|
|
106
|
+
},
|
|
107
|
+
"split-lines-rise": {
|
|
108
|
+
title: "Split lines rise",
|
|
109
|
+
description: "Split text into responsive lines and reveal it.",
|
|
110
|
+
intent: "Give editorial headings a line-by-line entrance.",
|
|
111
|
+
family: "split-text-typography",
|
|
112
|
+
tags: ["text", "lines", "split"]
|
|
113
|
+
},
|
|
114
|
+
"split-words-cascade": {
|
|
115
|
+
title: "Split words cascade",
|
|
116
|
+
description: "Split text into words and stagger its entrance.",
|
|
117
|
+
intent: "Emphasize a statement word by word.",
|
|
118
|
+
family: "split-text-typography",
|
|
119
|
+
tags: ["text", "words", "split"]
|
|
120
|
+
},
|
|
121
|
+
"split-chars-shimmer": {
|
|
122
|
+
title: "Character shimmer",
|
|
123
|
+
description: "Reveal a statement through a restrained character cascade.",
|
|
124
|
+
intent: "Add character-level rhythm to short display text.",
|
|
125
|
+
family: "split-text-typography",
|
|
126
|
+
tags: ["text", "characters", "shimmer"]
|
|
127
|
+
},
|
|
128
|
+
"typewriter-announce": {
|
|
129
|
+
title: "Typewriter announcement",
|
|
130
|
+
description: "Progressively reveal a short announcement while retaining complete accessible text.",
|
|
131
|
+
intent: "Give a short label or announcement a typed entrance.",
|
|
132
|
+
family: "split-text-typography",
|
|
133
|
+
tags: ["text", "typewriter", "announcement"]
|
|
134
|
+
},
|
|
135
|
+
"text-highlight-sweep": {
|
|
136
|
+
title: "Text highlight sweep",
|
|
137
|
+
description: "Sweep a visual highlight behind readable text.",
|
|
138
|
+
intent: "Emphasize a phrase without replacing its semantic text.",
|
|
139
|
+
family: "split-text-typography",
|
|
140
|
+
tags: ["text", "highlight", "sweep"]
|
|
141
|
+
},
|
|
142
|
+
"scroll-parallax": {
|
|
143
|
+
title: "Scroll parallax",
|
|
144
|
+
description: "Scrub a decorative layer against page movement.",
|
|
145
|
+
intent: "Add depth to a scene without changing its reading order.",
|
|
146
|
+
family: "scroll-parallax",
|
|
147
|
+
tags: ["scroll", "parallax", "depth"]
|
|
148
|
+
},
|
|
149
|
+
"scroll-exit": {
|
|
150
|
+
title: "Scroll exit",
|
|
151
|
+
description: "Move and fade content as its scene leaves.",
|
|
152
|
+
intent: "Resolve a section as the next one takes focus.",
|
|
153
|
+
family: "scroll-parallax",
|
|
154
|
+
tags: ["scroll", "exit", "fade"]
|
|
155
|
+
},
|
|
156
|
+
"scroll-drift": {
|
|
157
|
+
title: "Scroll drift",
|
|
158
|
+
description: "Drift a decorative layer through a bounded scene.",
|
|
159
|
+
intent: "Create subtle scroll-linked atmosphere.",
|
|
160
|
+
family: "scroll-parallax",
|
|
161
|
+
tags: ["scroll", "drift", "ambient"]
|
|
162
|
+
},
|
|
163
|
+
"scroll-progress-meter": {
|
|
164
|
+
title: "Scroll progress meter",
|
|
165
|
+
description: "Map local reading progress to a nonessential indicator.",
|
|
166
|
+
intent: "Show progress through a long section without controlling navigation.",
|
|
167
|
+
family: "scroll-parallax",
|
|
168
|
+
tags: ["scroll", "progress", "meter"]
|
|
169
|
+
},
|
|
170
|
+
"scroll-depth-stack": {
|
|
171
|
+
title: "Scroll depth stack",
|
|
172
|
+
description: "Introduce stacked cards with restrained depth.",
|
|
173
|
+
intent: "Give a grouped card stack depth as it enters the viewport.",
|
|
174
|
+
family: "scroll-parallax",
|
|
175
|
+
tags: ["scroll", "cards", "depth"]
|
|
176
|
+
},
|
|
177
|
+
"pinned-steps": {
|
|
178
|
+
title: "Pinned steps",
|
|
179
|
+
description: "Pin a narrative and activate linked panels by scroll progress.",
|
|
180
|
+
intent: "Turn sequential content into a controlled scroll story.",
|
|
181
|
+
family: "pinned-storytelling",
|
|
182
|
+
tags: ["pin", "steps", "story"]
|
|
183
|
+
},
|
|
184
|
+
"pinned-statement": {
|
|
185
|
+
title: "Pinned statement",
|
|
186
|
+
description: "Transform an oversized accent into a complete statement.",
|
|
187
|
+
intent: "Reveal a large editorial message through scroll.",
|
|
188
|
+
family: "pinned-storytelling",
|
|
189
|
+
tags: ["pin", "statement", "editorial"]
|
|
190
|
+
},
|
|
191
|
+
"pinned-founder-story": {
|
|
192
|
+
title: "Pinned founder story",
|
|
193
|
+
description: "Sequence an editorial story into a metrics scene.",
|
|
194
|
+
intent: "Tell a long-form story through coordinated pinned states.",
|
|
195
|
+
family: "pinned-storytelling",
|
|
196
|
+
tags: ["pin", "story", "metrics"]
|
|
197
|
+
},
|
|
198
|
+
"pinned-chapter-crossfade": {
|
|
199
|
+
title: "Pinned chapter crossfade",
|
|
200
|
+
description: "Advance narrative chapters around a stable visual anchor.",
|
|
201
|
+
intent: "Tell a chaptered story while a visual remains pinned.",
|
|
202
|
+
family: "pinned-storytelling",
|
|
203
|
+
tags: ["pin", "chapters", "crossfade"]
|
|
204
|
+
},
|
|
205
|
+
"pinned-product-explainer": {
|
|
206
|
+
title: "Pinned product explainer",
|
|
207
|
+
description: "Coordinate product steps with a stable media region.",
|
|
208
|
+
intent: "Explain product states in a controlled scroll sequence.",
|
|
209
|
+
family: "pinned-storytelling",
|
|
210
|
+
tags: ["pin", "product", "steps"]
|
|
211
|
+
},
|
|
212
|
+
"horizontal-gallery-scrub": {
|
|
213
|
+
title: "Horizontal gallery scrub",
|
|
214
|
+
description: "Translate a native row through a pinned viewport.",
|
|
215
|
+
intent: "Explore an editorial card gallery through vertical scroll.",
|
|
216
|
+
family: "horizontal-galleries",
|
|
217
|
+
tags: ["horizontal", "gallery", "scrub"]
|
|
218
|
+
},
|
|
219
|
+
"horizontal-gallery-snap": {
|
|
220
|
+
title: "Horizontal gallery snap",
|
|
221
|
+
description: "Scrub and snap a horizontal card gallery.",
|
|
222
|
+
intent: "Create clear chapter stops inside a horizontal scroll scene.",
|
|
223
|
+
family: "horizontal-galleries",
|
|
224
|
+
tags: ["horizontal", "gallery", "snap"]
|
|
225
|
+
},
|
|
226
|
+
"horizontal-feature-rail": {
|
|
227
|
+
title: "Horizontal feature rail",
|
|
228
|
+
description: "Move a feature rail and synchronize semantic active states.",
|
|
229
|
+
intent: "Present features as an active horizontal editorial sequence.",
|
|
230
|
+
family: "horizontal-galleries",
|
|
231
|
+
tags: ["horizontal", "features", "rail"]
|
|
232
|
+
},
|
|
233
|
+
"horizontal-logo-reel": {
|
|
234
|
+
title: "Horizontal logo reel",
|
|
235
|
+
description: "Move partner or case-study marks through a bounded region.",
|
|
236
|
+
intent: "Add scroll-linked movement to a readable logo list.",
|
|
237
|
+
family: "horizontal-galleries",
|
|
238
|
+
tags: ["horizontal", "logos", "reel"]
|
|
239
|
+
},
|
|
240
|
+
"horizontal-comparison-slider": {
|
|
241
|
+
title: "Horizontal comparison slider",
|
|
242
|
+
description: "Reveal equivalent media states with a native range input.",
|
|
243
|
+
intent: "Compare before and after media with pointer and keyboard control.",
|
|
244
|
+
family: "horizontal-galleries",
|
|
245
|
+
tags: ["horizontal", "comparison", "range"]
|
|
246
|
+
},
|
|
247
|
+
"media-expand": {
|
|
248
|
+
title: "Media expand",
|
|
249
|
+
description: "Expand cropped media while preserving its caption and labels.",
|
|
250
|
+
intent: "Turn a compact media preview into an immersive scene.",
|
|
251
|
+
family: "media-mask-clip",
|
|
252
|
+
tags: ["media", "expand", "scroll"]
|
|
253
|
+
},
|
|
254
|
+
"media-clip-reveal": {
|
|
255
|
+
title: "Media clip reveal",
|
|
256
|
+
description: "Reveal media through a bounded inline clip.",
|
|
257
|
+
intent: "Uncover media without changing its layout box.",
|
|
258
|
+
family: "media-mask-clip",
|
|
259
|
+
tags: ["media", "clip", "reveal"]
|
|
260
|
+
},
|
|
261
|
+
"media-curtain-split": {
|
|
262
|
+
title: "Media curtain split",
|
|
263
|
+
description: "Open two decorative curtains around media.",
|
|
264
|
+
intent: "Stage an editorial media reveal with paired curtains.",
|
|
265
|
+
family: "media-mask-clip",
|
|
266
|
+
tags: ["media", "curtain", "reveal"]
|
|
267
|
+
},
|
|
268
|
+
"image-focus-pan": {
|
|
269
|
+
title: "Image focus pan",
|
|
270
|
+
description: "Scrub a restrained image focal movement through a scene.",
|
|
271
|
+
intent: "Add depth to decorative media while retaining its authored focal point.",
|
|
272
|
+
family: "media-mask-clip",
|
|
273
|
+
tags: ["image", "pan", "scroll"]
|
|
274
|
+
},
|
|
275
|
+
"video-poster-play": {
|
|
276
|
+
title: "Video poster play",
|
|
277
|
+
description: "Transition a poster into native video playback.",
|
|
278
|
+
intent: "Start an accessible controlled video from a poster action.",
|
|
279
|
+
family: "media-mask-clip",
|
|
280
|
+
tags: ["video", "poster", "play"]
|
|
281
|
+
},
|
|
282
|
+
"hover-media-switch": {
|
|
283
|
+
title: "Hover media switch",
|
|
284
|
+
description: "Synchronize pointer and keyboard focus with media state.",
|
|
285
|
+
intent: "Preview related media from a semantic list of triggers.",
|
|
286
|
+
family: "hover-focus-pointer",
|
|
287
|
+
tags: ["hover", "focus", "media"]
|
|
288
|
+
},
|
|
289
|
+
"expand-panels": {
|
|
290
|
+
title: "Expand panels",
|
|
291
|
+
description: "Expand one panel from pointer or keyboard focus.",
|
|
292
|
+
intent: "Explore a compact group while keeping every panel accessible.",
|
|
293
|
+
family: "hover-focus-pointer",
|
|
294
|
+
tags: ["hover", "focus", "panels"]
|
|
295
|
+
},
|
|
296
|
+
"hover-lift": {
|
|
297
|
+
title: "Hover lift",
|
|
298
|
+
description: "Lift a self-contained item on pointer hover or keyboard focus.",
|
|
299
|
+
intent: "Give a card or action restrained responsive feedback.",
|
|
300
|
+
family: "hover-focus-pointer",
|
|
301
|
+
tags: ["hover", "focus", "lift"]
|
|
302
|
+
},
|
|
303
|
+
"magnetic-action": {
|
|
304
|
+
title: "Magnetic action",
|
|
305
|
+
description: "Draw an action gently toward a fine pointer.",
|
|
306
|
+
intent: "Add fine-pointer feedback without changing button semantics.",
|
|
307
|
+
family: "hover-focus-pointer",
|
|
308
|
+
tags: ["pointer", "magnetic", "action"]
|
|
309
|
+
},
|
|
310
|
+
"pointer-spotlight": {
|
|
311
|
+
title: "Pointer spotlight",
|
|
312
|
+
description: "Move a decorative spotlight inside a bounded region.",
|
|
313
|
+
intent: "Add local pointer atmosphere without affecting interaction.",
|
|
314
|
+
family: "hover-focus-pointer",
|
|
315
|
+
tags: ["pointer", "spotlight", "ambient"]
|
|
316
|
+
},
|
|
317
|
+
"accessible-menu": {
|
|
318
|
+
title: "Accessible menu",
|
|
319
|
+
description: "Animate a controlled menu without weakening its keyboard behavior.",
|
|
320
|
+
intent: "Open and close a navigation overlay with managed focus.",
|
|
321
|
+
family: "navigation-overlay",
|
|
322
|
+
tags: ["menu", "overlay", "keyboard"]
|
|
323
|
+
},
|
|
324
|
+
"dialog-overlay": {
|
|
325
|
+
title: "Dialog overlay",
|
|
326
|
+
description: "Present a native dialog with a restrained entrance and focus return.",
|
|
327
|
+
intent: "Open modal content without replacing native dialog semantics.",
|
|
328
|
+
family: "navigation-overlay",
|
|
329
|
+
tags: ["dialog", "overlay", "focus"]
|
|
330
|
+
},
|
|
331
|
+
"nav-active-indicator": {
|
|
332
|
+
title: "Navigation active indicator",
|
|
333
|
+
description: "Move a visual indicator between semantic navigation items.",
|
|
334
|
+
intent: "Reinforce current and focused navigation state.",
|
|
335
|
+
family: "navigation-overlay",
|
|
336
|
+
tags: ["navigation", "indicator", "focus"]
|
|
337
|
+
},
|
|
338
|
+
"accordion-disclosure": {
|
|
339
|
+
title: "Accordion disclosure",
|
|
340
|
+
description: "Animate native disclosure content with transform and opacity.",
|
|
341
|
+
intent: "Give a native accordion restrained open-state feedback.",
|
|
342
|
+
family: "navigation-overlay",
|
|
343
|
+
tags: ["accordion", "disclosure", "toggle"]
|
|
344
|
+
},
|
|
345
|
+
"command-palette": {
|
|
346
|
+
title: "Command palette",
|
|
347
|
+
description: "Open a searchable native dialog from a trigger or keyboard shortcut.",
|
|
348
|
+
intent: "Provide a fast keyboard-accessible command surface.",
|
|
349
|
+
family: "navigation-overlay",
|
|
350
|
+
tags: ["command", "dialog", "keyboard"]
|
|
351
|
+
},
|
|
352
|
+
"marquee": {
|
|
353
|
+
title: "Marquee",
|
|
354
|
+
description: "Run a seamless transform driven content loop.",
|
|
355
|
+
intent: "Create a pauseable, reduced-motion-safe horizontal loop.",
|
|
356
|
+
family: "loops-progress-ambient",
|
|
357
|
+
tags: ["marquee", "loop", "logos"]
|
|
358
|
+
},
|
|
359
|
+
"counter-value": {
|
|
360
|
+
title: "Value counter",
|
|
361
|
+
description: "Count a readable value toward its authored result.",
|
|
362
|
+
intent: "Emphasize a metric without replacing its accessible output.",
|
|
363
|
+
family: "loops-progress-ambient",
|
|
364
|
+
tags: ["counter", "metric", "progress"]
|
|
365
|
+
},
|
|
366
|
+
"progress-ring": {
|
|
367
|
+
title: "Progress ring",
|
|
368
|
+
description: "Draw an SVG progress ring to a bounded value.",
|
|
369
|
+
intent: "Show nonessential progress with a readable text equivalent.",
|
|
370
|
+
family: "loops-progress-ambient",
|
|
371
|
+
tags: ["progress", "ring", "svg"]
|
|
372
|
+
},
|
|
373
|
+
"ambient-float": {
|
|
374
|
+
title: "Ambient float",
|
|
375
|
+
description: "Float a decorative element while it is in view.",
|
|
376
|
+
intent: "Add restrained ambient movement without changing layout.",
|
|
377
|
+
family: "loops-progress-ambient",
|
|
378
|
+
tags: ["ambient", "float", "loop"]
|
|
379
|
+
},
|
|
380
|
+
"looping-logo-belt": {
|
|
381
|
+
title: "Looping logo belt",
|
|
382
|
+
description: "Loop a duplicated logo track with viewport pausing.",
|
|
383
|
+
intent: "Present partner marks in a continuous reduced-motion-safe belt.",
|
|
384
|
+
family: "loops-progress-ambient",
|
|
385
|
+
tags: ["logos", "belt", "loop"]
|
|
386
|
+
},
|
|
387
|
+
"flip-list-reorder": {
|
|
388
|
+
title: "FLIP list reorder",
|
|
389
|
+
description: "Animate application-owned list reordering through FLIP.",
|
|
390
|
+
intent: "Preserve spatial context when a semantic list changes order.",
|
|
391
|
+
family: "flip-layout",
|
|
392
|
+
tags: ["flip", "list", "reorder"]
|
|
393
|
+
},
|
|
394
|
+
"flip-card-to-detail": {
|
|
395
|
+
title: "FLIP card to detail",
|
|
396
|
+
description: "Expand a card detail while retaining spatial continuity.",
|
|
397
|
+
intent: "Reveal card detail from a native action without disorienting layout jumps.",
|
|
398
|
+
family: "flip-layout",
|
|
399
|
+
tags: ["flip", "card", "detail"]
|
|
400
|
+
},
|
|
401
|
+
"flip-filter-grid": {
|
|
402
|
+
title: "FLIP filter grid",
|
|
403
|
+
description: "Animate a semantic filtered grid between result sets.",
|
|
404
|
+
intent: "Keep items spatially understandable while native filter controls update results.",
|
|
405
|
+
family: "flip-layout",
|
|
406
|
+
tags: ["flip", "filter", "grid"]
|
|
407
|
+
},
|
|
408
|
+
"flip-nav-indicator": {
|
|
409
|
+
title: "FLIP navigation indicator",
|
|
410
|
+
description: "Move an inert indicator between semantic navigation items.",
|
|
411
|
+
intent: "Reinforce navigation focus and current state without changing semantics.",
|
|
412
|
+
family: "flip-layout",
|
|
413
|
+
tags: ["flip", "navigation", "indicator"]
|
|
414
|
+
},
|
|
415
|
+
"layout-accordion-grid": {
|
|
416
|
+
title: "Layout accordion grid",
|
|
417
|
+
description: "Animate one expanded grid item while controls retain native semantics.",
|
|
418
|
+
intent: "Explore dense content with clear spatial continuity.",
|
|
419
|
+
family: "flip-layout",
|
|
420
|
+
tags: ["flip", "accordion", "grid"]
|
|
421
|
+
},
|
|
422
|
+
"svg-line-draw": {
|
|
423
|
+
title: "SVG line draw",
|
|
424
|
+
description: "Draw stroked SVG geometry as it enters the viewport.",
|
|
425
|
+
intent: "Introduce explanatory line art without hiding its meaning.",
|
|
426
|
+
family: "svg-path-morph",
|
|
427
|
+
tags: ["svg", "draw", "line"]
|
|
428
|
+
},
|
|
429
|
+
"svg-path-morph": {
|
|
430
|
+
title: "SVG path morph",
|
|
431
|
+
description: "Morph between compatible SVG path states from a native action.",
|
|
432
|
+
intent: "Show a meaningful visual state change in a compact illustration.",
|
|
433
|
+
family: "svg-path-morph",
|
|
434
|
+
tags: ["svg", "path", "morph"]
|
|
435
|
+
},
|
|
436
|
+
"svg-icon-state": {
|
|
437
|
+
title: "SVG icon state",
|
|
438
|
+
description: "Morph a controlled icon between two pressed states.",
|
|
439
|
+
intent: "Reinforce a button state without replacing its accessible name.",
|
|
440
|
+
family: "svg-path-morph",
|
|
441
|
+
tags: ["svg", "icon", "state"]
|
|
442
|
+
},
|
|
443
|
+
"svg-orbit": {
|
|
444
|
+
title: "SVG orbit",
|
|
445
|
+
description: "Move a decorative subject along an authored SVG path.",
|
|
446
|
+
intent: "Connect scroll progress to a bounded explanatory path.",
|
|
447
|
+
family: "svg-path-morph",
|
|
448
|
+
tags: ["svg", "orbit", "path"]
|
|
449
|
+
},
|
|
450
|
+
"svg-signature-reveal": {
|
|
451
|
+
title: "SVG signature reveal",
|
|
452
|
+
description: "Draw a multi-stroke signature in authored order.",
|
|
453
|
+
intent: "Reveal a decorative signature while leaving the complete mark available without motion.",
|
|
454
|
+
family: "svg-path-morph",
|
|
455
|
+
tags: ["svg", "signature", "draw"]
|
|
456
|
+
},
|
|
457
|
+
"page-load-hero": {
|
|
458
|
+
title: "Page-load hero",
|
|
459
|
+
description: "Sequence local hero content after the page becomes interactive.",
|
|
460
|
+
intent: "Establish hierarchy with a restrained initial page entrance.",
|
|
461
|
+
family: "page-load-route",
|
|
462
|
+
tags: ["page", "load", "hero"]
|
|
463
|
+
},
|
|
464
|
+
"page-load-brand-mark": {
|
|
465
|
+
title: "Page-load brand mark",
|
|
466
|
+
description: "Introduce a local brand mark without blocking content.",
|
|
467
|
+
intent: "Give a brand signature a short, nonblocking entrance.",
|
|
468
|
+
family: "page-load-route",
|
|
469
|
+
tags: ["page", "load", "brand"]
|
|
470
|
+
},
|
|
471
|
+
"route-fade": {
|
|
472
|
+
title: "Route fade",
|
|
473
|
+
description: "Coordinate outgoing and incoming route regions through a scoped event.",
|
|
474
|
+
intent: "Soften application-owned route replacement without owning navigation.",
|
|
475
|
+
family: "page-load-route",
|
|
476
|
+
tags: ["route", "fade", "transition"]
|
|
477
|
+
},
|
|
478
|
+
"route-shared-media": {
|
|
479
|
+
title: "Route shared media",
|
|
480
|
+
description: "Animate application-owned shared media placement through FLIP.",
|
|
481
|
+
intent: "Preserve visual continuity while a router changes page structure.",
|
|
482
|
+
family: "page-load-route",
|
|
483
|
+
tags: ["route", "shared", "flip"]
|
|
484
|
+
},
|
|
485
|
+
"route-scroll-restore": {
|
|
486
|
+
title: "Route scroll restore",
|
|
487
|
+
description: "Restore application-provided scroll position after route completion.",
|
|
488
|
+
intent: "Keep route navigation position predictable and refresh scroll measurements.",
|
|
489
|
+
family: "page-load-route",
|
|
490
|
+
tags: ["route", "scroll", "restore"]
|
|
491
|
+
}
|
|
492
|
+
});
|
|
493
|
+
function fixturesForSpec(spec) {
|
|
494
|
+
return [{
|
|
495
|
+
id: "default",
|
|
496
|
+
label: "Default",
|
|
497
|
+
markup: defaultFixtureMarkup(spec.id, spec.root.selector, spec.slots.filter((slot) => slot.name !== "root")),
|
|
498
|
+
parameters: {}
|
|
499
|
+
}];
|
|
500
|
+
}
|
|
501
|
+
function describeSpec(spec) {
|
|
502
|
+
const prose = motionRecipeAuthoring[spec.id];
|
|
503
|
+
if (!prose) throw new Error(`Missing authoring metadata for motion recipe "${spec.id}".`);
|
|
504
|
+
return {
|
|
505
|
+
...spec,
|
|
506
|
+
...prose,
|
|
507
|
+
noJs: SHARED_NO_JS,
|
|
508
|
+
accessibility: SHARED_ACCESSIBILITY,
|
|
509
|
+
preview: SHARED_PREVIEW,
|
|
510
|
+
fixtures: fixturesForSpec(spec)
|
|
511
|
+
};
|
|
512
|
+
}
|
|
513
|
+
function describeBuiltinSpecs() {
|
|
514
|
+
return builtinMotionSpecs.map(describeSpec);
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
// src/recipes/builtins.js
|
|
518
|
+
var builtinMotionRecipes = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ describeBuiltinSpecs());
|
|
519
|
+
function createBuiltinMotionRegistry() {
|
|
520
|
+
return createMotionRegistry(builtinMotionRecipes);
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
// src/services/motion-service.js
|
|
524
|
+
function terms(value) {
|
|
525
|
+
return new Set(String(value).toLowerCase().split(/[^a-z0-9]+/).filter(Boolean));
|
|
526
|
+
}
|
|
527
|
+
function score(recipe, requested) {
|
|
528
|
+
const searchable = terms([recipe.id, recipe.title, recipe.description, recipe.intent, recipe.family, ...recipe.tags].join(" "));
|
|
529
|
+
let result = 0;
|
|
530
|
+
for (const term of requested) if (searchable.has(term)) result += 1;
|
|
531
|
+
return result;
|
|
532
|
+
}
|
|
533
|
+
function selectorToken(selector) {
|
|
534
|
+
const match = /^\[([a-zA-Z0-9_-]+)(?:=["']?([^\]"']+)["']?)?\]$/.exec(selector.trim());
|
|
535
|
+
return match ? { attribute: match[1], value: match[2] } : void 0;
|
|
536
|
+
}
|
|
537
|
+
function sourceHasSelector(markup, selector) {
|
|
538
|
+
const token = selectorToken(selector);
|
|
539
|
+
if (!token) return false;
|
|
540
|
+
const escaped = token.attribute.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
541
|
+
if (token.value === void 0) return new RegExp(`\\s${escaped}(?:\\s*=|\\s|>|/|$)`, "i").test(markup);
|
|
542
|
+
const value = token.value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
543
|
+
return new RegExp(`\\s${escaped}\\s*=\\s*["']${value}["']`, "i").test(markup);
|
|
544
|
+
}
|
|
545
|
+
function elementRegions(markup, selector) {
|
|
546
|
+
const regions = [];
|
|
547
|
+
const stack = [];
|
|
548
|
+
const tags = /<\s*(\/)?\s*([a-zA-Z][\w:-]*)([^>]*?)(\/?)>/g;
|
|
549
|
+
let match;
|
|
550
|
+
while (match = tags.exec(markup)) {
|
|
551
|
+
const closing = Boolean(match[1]);
|
|
552
|
+
const tag = match[2].toLowerCase();
|
|
553
|
+
if (closing) {
|
|
554
|
+
for (let index = stack.length - 1; index >= 0; index -= 1) {
|
|
555
|
+
if (stack[index].tag !== tag) continue;
|
|
556
|
+
const [node2] = stack.splice(index, 1);
|
|
557
|
+
if (node2.matches) regions.push(markup.slice(node2.start, tags.lastIndex));
|
|
558
|
+
break;
|
|
559
|
+
}
|
|
560
|
+
continue;
|
|
561
|
+
}
|
|
562
|
+
const node = { tag, start: match.index, matches: sourceHasSelector(` ${match[3]}`, selector) };
|
|
563
|
+
const voidElement = Boolean(match[4]) || ["area", "base", "br", "col", "embed", "hr", "img", "input", "link", "meta", "param", "source", "track", "wbr"].includes(tag);
|
|
564
|
+
if (voidElement) {
|
|
565
|
+
if (node.matches) regions.push(match[0]);
|
|
566
|
+
} else stack.push(node);
|
|
567
|
+
}
|
|
568
|
+
for (const node of stack) if (node.matches) regions.push(markup.slice(node.start));
|
|
569
|
+
return regions;
|
|
570
|
+
}
|
|
571
|
+
function createMotionService(registry) {
|
|
572
|
+
if (!registry || typeof registry.catalog !== "function") throw new TypeError("A motion registry is required.");
|
|
573
|
+
return Object.freeze({
|
|
574
|
+
catalog() {
|
|
575
|
+
return registry.catalog();
|
|
576
|
+
},
|
|
577
|
+
recipe(id) {
|
|
578
|
+
const recipe = registry.get(id);
|
|
579
|
+
if (!recipe) return void 0;
|
|
580
|
+
const { setup, ...manifest } = recipe;
|
|
581
|
+
return { ...manifest, implementation: typeof setup === "function" ? "custom" : "declarative" };
|
|
582
|
+
},
|
|
583
|
+
suggest(brief, options = {}) {
|
|
584
|
+
const requested = terms(brief);
|
|
585
|
+
const limit = options.limit ?? 8;
|
|
586
|
+
return registry.list().map((recipe) => ({ recipe, score: score(recipe, requested) })).filter((entry) => entry.score > 0).sort((a, b) => b.score - a.score || a.recipe.id.localeCompare(b.recipe.id)).slice(0, limit).map(({ recipe, score: relevance }) => ({ id: recipe.id, relevance, intent: recipe.intent, family: recipe.family }));
|
|
587
|
+
},
|
|
588
|
+
validate(input) {
|
|
589
|
+
if (input) return validateMotionRecipe(input);
|
|
590
|
+
return registry.validate();
|
|
591
|
+
},
|
|
592
|
+
scanMarkup(markup) {
|
|
593
|
+
if (typeof markup !== "string") throw new TypeError("Markup must be a string.");
|
|
594
|
+
return {
|
|
595
|
+
recipes: registry.list().flatMap((recipe) => elementRegions(markup, recipe.root.selector).map((region, index) => {
|
|
596
|
+
const missingSlots = recipe.slots.filter((slot) => slot.name !== "root" && slot.required !== false && !sourceHasSelector(region, slot.selector)).map((slot) => slot.name);
|
|
597
|
+
return { id: recipe.id, instance: index, root: recipe.root.selector, missingSlots, ready: missingSlots.length === 0 };
|
|
598
|
+
}))
|
|
599
|
+
};
|
|
600
|
+
},
|
|
601
|
+
compose(brief, options = {}) {
|
|
602
|
+
const suggestions = this.suggest(brief, { limit: options.limit ?? 5 });
|
|
603
|
+
const ids = suggestions.map((suggestion) => suggestion.id);
|
|
604
|
+
const plan = this.plan(ids);
|
|
605
|
+
const scan = typeof options.markup === "string" ? this.scanMarkup(options.markup) : void 0;
|
|
606
|
+
const highCost = plan.recipes.filter((recipe) => recipe.performance.class === "high");
|
|
607
|
+
const warnings = [];
|
|
608
|
+
if (highCost.length > (options.maxHighCost ?? 2)) warnings.push(`Plan includes ${highCost.length} high-cost recipes.`);
|
|
609
|
+
for (const entry of scan?.recipes ?? []) {
|
|
610
|
+
if (!entry.ready) warnings.push(`${entry.id} is missing required slots: ${entry.missingSlots.join(", ")}`);
|
|
611
|
+
}
|
|
612
|
+
return { schemaVersion: "1", brief, suggestions, plan, scan, warnings };
|
|
613
|
+
},
|
|
614
|
+
plan(ids) {
|
|
615
|
+
const recipes = registry.resolve(ids);
|
|
616
|
+
return {
|
|
617
|
+
schemaVersion: "1",
|
|
618
|
+
recipes: recipes.map((recipe) => ({
|
|
619
|
+
id: recipe.id,
|
|
620
|
+
root: recipe.root,
|
|
621
|
+
slots: recipe.slots,
|
|
622
|
+
parameters: Object.fromEntries(Object.entries(recipe.parameters).map(([name, value]) => [name, value.default])),
|
|
623
|
+
performance: recipe.performance,
|
|
624
|
+
reducedMotion: recipe.reducedMotion
|
|
625
|
+
}))
|
|
626
|
+
};
|
|
627
|
+
}
|
|
628
|
+
});
|
|
629
|
+
}
|
|
630
|
+
|
|
631
|
+
// src/default-service.js
|
|
632
|
+
function createDefaultMotionService() {
|
|
633
|
+
return createMotionService(createBuiltinMotionRegistry());
|
|
634
|
+
}
|
|
635
|
+
|
|
636
|
+
export {
|
|
637
|
+
builtinMotionRecipes,
|
|
638
|
+
createBuiltinMotionRegistry,
|
|
639
|
+
createMotionService,
|
|
640
|
+
createDefaultMotionService
|
|
641
|
+
};
|
|
642
|
+
//# sourceMappingURL=chunk-ML7HTCZT.js.map
|