@tobiasoberpaul/mdk 0.0.0-stage → 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.
Files changed (62) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +94 -2
  3. package/THIRD_PARTY_NOTICES.md +7 -0
  4. package/ai/INSTRUCTIONS.md +14 -0
  5. package/ai/mcp-server.mjs +112 -0
  6. package/dist/behaviors.d.ts +25 -0
  7. package/dist/behaviors.js +20 -0
  8. package/dist/core/animation.d.ts +16 -0
  9. package/dist/core/animation.js +150 -0
  10. package/dist/core/content-frames.d.ts +13 -0
  11. package/dist/core/content-frames.js +27 -0
  12. package/dist/core/content-groups.d.ts +18 -0
  13. package/dist/core/content-groups.js +34 -0
  14. package/dist/core/content-layout.d.ts +4 -0
  15. package/dist/core/content-layout.js +84 -0
  16. package/dist/core/content-timing.d.ts +16 -0
  17. package/dist/core/content-timing.js +20 -0
  18. package/dist/core/geometry.d.ts +69 -0
  19. package/dist/core/geometry.js +127 -0
  20. package/dist/core/lifecycle.d.ts +21 -0
  21. package/dist/core/lifecycle.js +1 -0
  22. package/dist/core/resize.d.ts +14 -0
  23. package/dist/core/resize.js +136 -0
  24. package/dist/core/rounded-shape.d.ts +2 -0
  25. package/dist/core/rounded-shape.js +6 -0
  26. package/dist/core/tokens.d.ts +7 -0
  27. package/dist/core/tokens.js +54 -0
  28. package/dist/declarative.d.ts +9 -0
  29. package/dist/declarative.js +83 -0
  30. package/dist/dismiss.d.ts +8 -0
  31. package/dist/dismiss.js +14 -0
  32. package/dist/index.d.ts +30 -0
  33. package/dist/index.js +15 -0
  34. package/dist/interactions.d.ts +25 -0
  35. package/dist/interactions.js +115 -0
  36. package/dist/layout.d.ts +3 -0
  37. package/dist/layout.js +22 -0
  38. package/dist/observe.d.ts +19 -0
  39. package/dist/observe.js +44 -0
  40. package/dist/position.d.ts +15 -0
  41. package/dist/position.js +60 -0
  42. package/dist/scroll.d.ts +12 -0
  43. package/dist/scroll.js +39 -0
  44. package/dist/styles/motion.css +76 -0
  45. package/dist/transitions/content.d.ts +8 -0
  46. package/dist/transitions/content.js +38 -0
  47. package/dist/transitions/extract.d.ts +5 -0
  48. package/dist/transitions/extract.js +5 -0
  49. package/dist/transitions/grow.d.ts +6 -0
  50. package/dist/transitions/grow.js +5 -0
  51. package/dist/transitions/liquid.d.ts +13 -0
  52. package/dist/transitions/liquid.js +215 -0
  53. package/dist/transitions/morph.d.ts +5 -0
  54. package/dist/transitions/morph.js +5 -0
  55. package/dist/transitions/scale.d.ts +7 -0
  56. package/dist/transitions/scale.js +5 -0
  57. package/dist/transitions/slide.d.ts +11 -0
  58. package/dist/transitions/slide.js +68 -0
  59. package/docs/AI_INTEGRATION.md +69 -0
  60. package/docs/LIBRARY.md +440 -0
  61. package/docs/PUBLISHING.md +33 -0
  62. package/package.json +88 -4
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 TOberpaul
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,3 +1,95 @@
1
- # Temporary Holding Version
1
+ # MDK · UI Motion Library
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ A small, framework-independent TypeScript motion library using native Web
4
+ Animations API (WAAPI), CSS tokens and reversible transitions. React is used only
5
+ by the DB React and shadcn/ui playground integrations.
6
+
7
+ ## Run locally
8
+
9
+ ```sh
10
+ npm install
11
+ npm run dev
12
+ ```
13
+
14
+ Open [the playground](http://127.0.0.1:4173/examples/).
15
+
16
+ ## Publish the website
17
+
18
+ ```sh
19
+ npm ci
20
+ npm run build:site
21
+ ```
22
+
23
+ Upload the contents of `site/` to a static host, or configure its build command
24
+ as `npm run build:site` and publish directory as `site`. The site includes
25
+ Examples, Code playground and Documentation. Subdirectory hosting works without
26
+ route rewrites. Preview the generated site locally at
27
+ [the static build](http://127.0.0.1:4173/site/) while the dev server is running.
28
+
29
+ GitHub Pages is deployed by `.github/workflows/pages.yml` on every push to
30
+ `main`. Set **Settings → Pages → Source → GitHub Actions** in the repository.
31
+ Website: https://toberpaul.github.io/animations/
32
+
33
+ - **Examples:** switch between DB React and shadcn/ui, light/dark mode, reduced
34
+ motion, duration, progression gap and overshoot; scrub real animation frames.
35
+ - **Code playground:** edit HTML with live preview. Define
36
+ motion in code through attributes/classes or direct factories. Drafts are
37
+ stored locally. Exported HTML references the running library server.
38
+
39
+ ```sh
40
+ npm run build # TypeScript library and CSS → dist/
41
+ npm run build:examples # Both component integrations and playground shell
42
+ ```
43
+
44
+ The current package is private and has not been published to npm. Consume this
45
+ checkout through a workspace/local package or import the built files. No runtime
46
+ framework dependencies are required by the library.
47
+
48
+ ## Documentation
49
+
50
+ See **[Library guide](docs/LIBRARY.md)** for the public API, declarative markup,
51
+ tokens, content sequencing, viewport entry and component integration examples.
52
+
53
+ | Playground animation | Example artifacts |
54
+ | --- | --- |
55
+ | Grow | Action menu, dialog, side panel, notification, three-column panels, split-button menu |
56
+ | Extract | Action menu with a retained source button |
57
+ | Morph | Replacing action menu, rename input |
58
+ | Slide | Two-level submenu |
59
+ | Content | Text block, image/caption and independently revealed page blocks |
60
+
61
+ All component examples use the same core library. Components own positioning,
62
+ content, accessibility, focus and lifecycle. The library owns visual transitions.
63
+ Grow/Morph/Extract contents use fade and blur without an independent content
64
+ slide. Content uses a fixed slide + blur + fade combination.
65
+
66
+ ## Defaults
67
+
68
+ - Duration: **300ms**; Content uses this duration per item.
69
+ - Easing: symmetric ease-in-out.
70
+ - Overshoot: **15% of the shorter side, capped at .75rem**.
71
+ - Playground progression: enabled, **24ms** start spacing.
72
+ - Library progression: enabled by default; set `--motion-content-progressive: 0` to disable.
73
+ - Reduced motion: respects the OS preference and the playground switch.
74
+
75
+ The DB integration uses installed DB component/theme packages. Optional licensed
76
+ DB Neo/icon assets require locally configured `ASSET_PASSWORD` and
77
+ `ASSET_INIT_VECTOR`; they are not included in this repository. The examples use
78
+ fallback icons where applicable. Keep credentials in ignored local environment
79
+ files.
80
+
81
+ Generated builds, dependencies and local visual-check output are ignored by Git.
82
+
83
+ ## AI coding clients
84
+
85
+ A local read-only MCP server shares the guide, built API declarations, tokens and integration examples with compatible AI clients. See [AI integration setup](docs/AI_INTEGRATION.md). Start with `npm run build`, then configure `node /absolute/path/to/animations/ai/mcp-server.mjs` as a stdio MCP server. Portable integration rules are in `ai/INSTRUCTIONS.md`.
86
+
87
+ ## Local Codex analytics
88
+
89
+ Run `npm run analytics` and open the printed address. It reads token counts and available five-hour account snapshots from local Codex history for recorded work-package turns. The optional project hook adds local request telemetry and can be trusted under **Settings → Hooks** or with `/hooks`. The [Context Starter](context-starter/README.md) describes the data format.
90
+
91
+ ## License and package releases
92
+
93
+ Original library and MCP code is licensed under [MIT](LICENSE). Third-party example dependencies retain their own licenses; see [third-party notices](THIRD_PARTY_NOTICES.md).
94
+
95
+ See [publishing](docs/PUBLISHING.md) for npm package validation and release steps. The registry package is not published yet.
@@ -0,0 +1,7 @@
1
+ # Third-party notices
2
+
3
+ The distributed library and local MCP server have no runtime dependencies. They use browser APIs and Node.js built-ins.
4
+
5
+ The repository's website examples use third-party design systems, React, Radix primitives, Lucide icons and development tools. Those dependencies retain their own licenses; their example source and bundled website assets are not included in the npm package. See each dependency's installed LICENSE/NOTICE files and examples/shadcn/SHADCN-LICENSE when distributing the website.
6
+
7
+ The package's LICENSE covers this project's original code, not third-party trademarks, fonts or proprietary design-system assets.
@@ -0,0 +1,14 @@
1
+ # Motion Library integration rules
2
+
3
+ Use the installed Motion Library instead of copying animation implementations into applications.
4
+
5
+ 1. Read `motion://guide` and `get_motion_reference` with topic `api` before choosing options. The built declarations and CSS tokens are authoritative.
6
+ 2. Choose Grow for anchored surfaces, Extract to retain the trigger, Morph to replace it, Slide for translation, and Fade for slide/blur/fade content reveals.
7
+ 3. Keep layout, positioning, semantics, visibility, focus, keyboard handling and lifecycle owned by the consuming component. Optional positioning is explicit; do not replace an existing positioning system.
8
+ 4. Include all visible elements in reveal groups: headings, controls, labels, icons, dividers and rows. Keep related elements together. Grow/Extract/Morph content does not slide.
9
+ 5. Use CSS custom properties for timing, easing and overshoot. Preserve progressive reveal and reduced-motion handling. Do not hardcode another animation recipe.
10
+ 6. Keep targets mounted through exit. Synchronize hidden/inert and aria-expanded. For modals, preserve the component's focus trap and focus restoration; generic examples are disclosures, not complete dialogs or menus.
11
+ 7. Destroy controllers and disconnect bindings/observers on unmount. Preserve interruption and reversal. Use seek or bindScrollMotion for scroll scrubbing rather than recreating controllers on each frame.
12
+ 8. Verify opening, closing, rapid reversal, keyboard operation, reduced motion and mobile geometry in the consuming application.
13
+
14
+ The MCP tools provide reference text and source examples only. They do not execute generated code, edit the application, or call an AI model. Any implementation changes remain actions performed by the user's AI client under its normal permissions.
@@ -0,0 +1,112 @@
1
+ #!/usr/bin/env node
2
+ // Local, read-only MCP adapter. stdout is reserved for protocol messages.
3
+ import {createInterface} from 'node:readline';
4
+ import {readFile,readdir} from 'node:fs/promises';
5
+ import {motionBehaviors} from '../dist/behaviors.js';
6
+ const base=new URL('../',import.meta.url);
7
+ const read=path=>readFile(new URL(path,base),'utf8');
8
+ const versions=['2026-07-28','2025-11-25','2025-06-18','2025-03-26'];
9
+ const capabilities={tools:{},resources:{},prompts:{}};
10
+ const packageInfo=JSON.parse(await read('package.json'));
11
+ const info={name:'motion-library',version:packageInfo.version};
12
+ const instructions='Read motion_guide before integrating. Use the installed library API, not copied animation recipes. Component integrations own visibility, accessibility and focus. Tools read bundled reference files only; generated examples are not executed.';
13
+ const resources=[
14
+ {uri:'motion://guide',name:'motion_guide',mimeType:'text/markdown',path:'docs/LIBRARY.md'},
15
+ {uri:'motion://tokens',name:'motion_tokens',mimeType:'text/css',path:'dist/styles/motion.css'},
16
+ {uri:'motion://instructions',name:'motion_integration_rules',mimeType:'text/markdown',path:'ai/INSTRUCTIONS.md'},
17
+ ];
18
+ const tools=[
19
+ {name:'get_motion_reference',description:'Read the current library guide, design tokens, API declarations or canonical behavior definitions.',inputSchema:{type:'object',properties:{topic:{type:'string',enum:['guide','tokens','api','behaviors','instructions']}},required:['topic'],additionalProperties:false}},
20
+ {name:'create_motion_example',description:'Generate a minimal integration using the real Motion Library API. Returns source text; never executes or writes it.',inputSchema:{type:'object',properties:{behavior:{type:'string',enum:Object.keys(motionBehaviors)},activation:{type:'string',enum:['click','hover','focus','observer','scroll','manual']},placement:{type:'string',enum:['center','top','bottom','left','right','bottom-end','top-end']},relativeTo:{type:'string',enum:['trigger','viewport']},space:{type:'number',minimum:0,maximum:128}},required:['behavior'],additionalProperties:false}},
21
+ ].map(tool=>({...tool,annotations:{readOnlyHint:true,destructiveHint:false,idempotentHint:true,openWorldHint:false}}));
22
+ function invalid(message){throw Object.assign(new Error(message),{code:-32602});}
23
+ async function reference(topic){
24
+ if(topic==='behaviors')return JSON.stringify(motionBehaviors,null,2);
25
+ if(topic==='api'){
26
+ const files=[];
27
+ async function walk(path){for(const entry of await readdir(new URL(path,base),{withFileTypes:true})){const file=path+'/'+entry.name;if(entry.isDirectory())await walk(file);else if(entry.name.endsWith('.d.ts'))files.push(file);}}
28
+ await walk('dist');files.sort();return (await Promise.all(files.map(async file=>'// '+file+'\n'+await read(file)))).join('\n\n');
29
+ }
30
+ const resource=resources.find(value=>value.uri==='motion://'+topic);
31
+ if(!resource)invalid('Unknown reference topic.');return read(resource.path);
32
+ }
33
+ function example(args){
34
+ const behavior=args.behavior,activation=args.activation??'click';
35
+ if(!Object.hasOwn(motionBehaviors,behavior))invalid('Unknown behavior.');
36
+ if(!tools[1].inputSchema.properties.activation.enum.includes(activation))invalid('Invalid activation.');
37
+ const placement=args.placement;
38
+ if(placement&&!tools[1].inputSchema.properties.placement.enum.includes(placement))invalid('Invalid placement.');
39
+ const relativeTo=args.relativeTo??'trigger';
40
+ if(!['trigger','viewport'].includes(relativeTo))invalid('Invalid relativeTo.');
41
+ const space=args.space??12;if(typeof space!=='number'||!Number.isFinite(space)||space<0||space>128)invalid('space must be between 0 and 128 pixels.');
42
+ const position=placement?`\n position: { relativeTo: '${relativeTo}', placement: '${placement}', space: ${space} },`:'';
43
+ const scroll=activation==='scroll'?`\n scroll: { mode: 'scrub', start: 200, end: 600 },`:'';
44
+ const observer=activation==='observer'?`\n observeElement: document.querySelector('#motion-slot'),`:'';
45
+ return `<!-- Reserve this slot so entry/exit cannot shift the surrounding page. -->
46
+ <div id="motion-slot">
47
+ <button id="trigger" aria-controls="target" aria-expanded="false">Open</button>
48
+ <section id="target" data-motion="${behavior}" data-motion-anchor="center" hidden inert>
49
+ <h2>Your next idea</h2>
50
+ <p>Content keeps its layout and follows the library reveal recipe.</p>
51
+ <button id="close">Close</button>
52
+ </section>
53
+ </div>
54
+
55
+ // Load the library's motion.css in your application.
56
+ import {createInteractiveMotion} from '@tobiasoberpaul/mdk';
57
+ const target = document.querySelector('#target');
58
+ const trigger = document.querySelector('#trigger');
59
+ const motion = createInteractiveMotion({
60
+ content: target, trigger, on: '${activation}',${position}${scroll}${observer}
61
+ close: target.querySelector('#close'),
62
+ reveal: [...target.children],
63
+ onStateChange(state) {
64
+ if (state === 'destroyed') return;
65
+ target.hidden = state === 'closed';
66
+ target.inert = state === 'closed' || state === 'closing';
67
+ trigger.setAttribute('aria-expanded', String(state === 'open' || state === 'opening'));
68
+ ${behavior==='morph'?" trigger.style.visibility = state === 'closed' ? '' : 'hidden';\n trigger.inert = state !== 'closed';\n":''} },
69
+ });
70
+ // Existing component positioning is used unless position is explicitly supplied.
71
+ // This is a generic disclosure, not a complete modal dialog or ARIA menu integration.
72
+ // Add component-owned focus restoration and semantics where appropriate.
73
+ // On unmount: motion.destroy();`;
74
+ }
75
+ async function dispatch(method,params={}){
76
+ if(method==='initialize')return {protocolVersion:versions.slice(1).includes(params.protocolVersion)?params.protocolVersion:'2025-06-18',capabilities,serverInfo:info,instructions};
77
+ if(method==='server/discover')return {supportedVersions:[versions[0]],capabilities,instructions,cacheScope:'public',ttlMs:0,resultType:'complete'};
78
+ if(method==='ping')return {};
79
+ if(method==='resources/list')return {resources:resources.map(({path,...resource})=>resource)};
80
+ if(method==='resources/templates/list')return {resourceTemplates:[]};
81
+ if(method==='resources/read'){
82
+ const resource=resources.find(value=>value.uri===params.uri);if(!resource)invalid('Unknown resource URI.');
83
+ return {contents:[{uri:resource.uri,mimeType:resource.mimeType,text:await read(resource.path)}]};
84
+ }
85
+ if(method==='tools/list')return {tools};
86
+ if(method==='tools/call'){
87
+ const args=params.arguments??{};if(!args||typeof args!=='object'||Array.isArray(args))invalid('Arguments must be an object.');
88
+ const tool=tools.find(value=>value.name===params.name);if(!tool)invalid('Unknown tool.');
89
+ if(Object.keys(args).some(key=>!Object.hasOwn(tool.inputSchema.properties,key)))invalid('Unknown argument.');
90
+ try{return {content:[{type:'text',text:params.name==='get_motion_reference'?await reference(args.topic):example(args)}]};}
91
+ catch(error){return {isError:true,content:[{type:'text',text:error.message}]};}
92
+ }
93
+ if(method==='prompts/list')return {prompts:[{name:'integrate_motion',description:'Plan an integration using the library reference and existing component lifecycle.'}]};
94
+ if(method==='prompts/get'){
95
+ if(params.name!=='integrate_motion')invalid('Unknown prompt.');
96
+ return {messages:[{role:'user',content:{type:'text',text:(await read('ai/INSTRUCTIONS.md'))+'\nRead the guide and API through get_motion_reference. Inspect the existing component and implement its motion with the library. Include cleanup and reduced-motion behavior.'}}]};
97
+ }
98
+ throw Object.assign(new Error('Method not found'),{code:-32601});
99
+ }
100
+ const send=value=>process.stdout.write(JSON.stringify(value)+'\n');
101
+ const lines=createInterface({input:process.stdin,crlfDelay:Infinity});
102
+ let queue=Promise.resolve();
103
+ lines.on('line',line=>{queue=queue.then(async()=>{
104
+ let request;try{request=JSON.parse(line);}catch{send({jsonrpc:'2.0',id:null,error:{code:-32700,message:'Parse error'}});return;}
105
+ if(!request||Array.isArray(request)||request.jsonrpc!=='2.0'||typeof request.method!=='string'){
106
+ send({jsonrpc:'2.0',id:request?.id??null,error:{code:-32600,message:'Invalid request'}});return;
107
+ }
108
+ if(request.params!==undefined&&(!request.params||typeof request.params!=='object'||Array.isArray(request.params))){send({jsonrpc:'2.0',id:request.id??null,error:{code:-32602,message:'Params must be an object'}});return;}
109
+ if(!Object.hasOwn(request,'id'))return; // Notifications never receive responses.
110
+ try{const modern=request.params?._meta?.['io.modelcontextprotocol/protocolVersion'];if(modern&&modern!==versions[0])invalid('Unsupported modern protocol version.');const result=await dispatch(request.method,request.params);send({jsonrpc:'2.0',id:request.id,result:modern?{...result,resultType:'complete'}:result});}
111
+ catch(error){send({jsonrpc:'2.0',id:request.id,error:{code:error.code??-32603,message:error.code?error.message:'Unable to read the bundled library reference. Run npm run build first.'}});}
112
+ }).catch(error=>process.stderr.write(error.message+'\n'));});
@@ -0,0 +1,25 @@
1
+ export declare const motionBehaviors: {
2
+ readonly grow: {
3
+ readonly replacesTrigger: false;
4
+ readonly contentEffect: "fade-blur";
5
+ };
6
+ readonly extract: {
7
+ readonly replacesTrigger: false;
8
+ readonly contentEffect: "fade-blur";
9
+ };
10
+ readonly morph: {
11
+ readonly replacesTrigger: true;
12
+ readonly contentEffect: "fade-blur";
13
+ };
14
+ readonly slide: {
15
+ readonly replacesTrigger: false;
16
+ readonly contentEffect: "fade-blur";
17
+ };
18
+ readonly fade: {
19
+ readonly replacesTrigger: false;
20
+ readonly contentEffect: "slide-blur-fade";
21
+ };
22
+ };
23
+ export type MotionKind = keyof typeof motionBehaviors;
24
+ /** Canonical naming shared by declarative integration and consuming components. */
25
+ export declare function getMotionKind(content: HTMLElement): MotionKind;
@@ -0,0 +1,20 @@
1
+ export const motionBehaviors = {
2
+ grow: { replacesTrigger: false, contentEffect: 'fade-blur' },
3
+ extract: { replacesTrigger: false, contentEffect: 'fade-blur' },
4
+ morph: { replacesTrigger: true, contentEffect: 'fade-blur' },
5
+ slide: { replacesTrigger: false, contentEffect: 'fade-blur' },
6
+ fade: { replacesTrigger: false, contentEffect: 'slide-blur-fade' },
7
+ };
8
+ /** Canonical naming shared by declarative integration and consuming components. */
9
+ export function getMotionKind(content) {
10
+ const names = [...Object.keys(motionBehaviors), 'liquid', 'content'];
11
+ const classes = names.filter(name => content.classList.contains(`motion-${name}`));
12
+ const raw = content.dataset.motion ?? (classes.length === 1 ? classes[0] : undefined);
13
+ if (raw === 'content')
14
+ return 'fade';
15
+ if (raw === 'liquid')
16
+ return content.hasAttribute('data-motion-replace-trigger') ? 'morph' : 'extract';
17
+ if (!raw || !Object.prototype.hasOwnProperty.call(motionBehaviors, raw))
18
+ throw new Error('Set data-motion to grow, extract, morph, slide or fade, or use one matching motion-* class.');
19
+ return raw;
20
+ }
@@ -0,0 +1,16 @@
1
+ import type { Options, Transition } from './lifecycle.js';
2
+ export interface Track {
3
+ element: Element;
4
+ frames: Keyframe[];
5
+ easing?: string;
6
+ pseudoElement?: string;
7
+ composite?: CompositeOperation;
8
+ }
9
+ export declare function createTransition(options: Options, pattern: string, tracks: () => Track[], behavior?: {
10
+ keepVisibleWhenClosed?: boolean;
11
+ durationScale?: () => number;
12
+ revealTiming?: (count: number, index: number) => {
13
+ start: number;
14
+ end: number;
15
+ };
16
+ }): Transition;
@@ -0,0 +1,150 @@
1
+ import { motionBehaviors } from '../behaviors.js';
2
+ import { contentTimeline } from './content-timing.js';
3
+ import { contentTracks } from './content-frames.js';
4
+ import { revealGroups, hasSurfaceSequence, sequenceTiming } from './content-groups.js';
5
+ import { duration, numberToken, token } from './tokens.js';
6
+ export function createTransition(options, pattern, tracks, behavior = {}) {
7
+ const { content } = options;
8
+ const reduced = matchMedia('(prefers-reduced-motion: reduce)');
9
+ let target = options.initiallyOpen ?? false;
10
+ let state = target ? 'open' : 'closed';
11
+ let animations = [];
12
+ let generation = 0;
13
+ const opacity = content.style.getPropertyValue('opacity');
14
+ const priority = content.style.getPropertyPriority('opacity');
15
+ function restore() {
16
+ if (opacity)
17
+ content.style.setProperty('opacity', opacity, priority);
18
+ else
19
+ content.style.removeProperty('opacity');
20
+ }
21
+ function endpoint() { restore(); if (!target && !behavior.keepVisibleWhenClosed)
22
+ content.style.opacity = '0'; }
23
+ function notify(next) { state = next; options.onStateChange?.(next); }
24
+ function cancel() { generation++; animations.forEach(a => a.cancel()); animations = []; }
25
+ function settle() {
26
+ cancel();
27
+ endpoint();
28
+ notify(target ? 'open' : 'closed');
29
+ }
30
+ endpoint();
31
+ function set(next) {
32
+ if (state === 'destroyed' || next === target)
33
+ return;
34
+ target = next;
35
+ // Let adapters settle native effects before the visual-state snapshot.
36
+ if (!animations.length)
37
+ restore();
38
+ // Notify before measurement so the component can unhide and position itself.
39
+ notify(target ? 'opening' : 'closing');
40
+ if (state === 'destroyed')
41
+ return;
42
+ if (reduced.matches || typeof content.animate !== 'function') {
43
+ settle();
44
+ return;
45
+ }
46
+ if (!animations.length) {
47
+ try {
48
+ let time = duration(content, pattern);
49
+ if (!time) {
50
+ settle();
51
+ return;
52
+ }
53
+ const requestedEasing = token(content, `easing-${pattern}`);
54
+ const easing = CSS.supports('animation-timing-function', requestedEasing) ? requestedEasing : token(content, 'easing-standard');
55
+ const recipe = motionBehaviors[(pattern === 'scale' ? 'grow' : pattern === 'liquid' ? 'extract' : pattern === 'content' ? 'fade' : pattern)];
56
+ const descriptions = tracks();
57
+ time *= behavior.durationScale?.() ?? 1;
58
+ const reveal = options.reveal ? (Array.isArray(options.reveal) ? options.reveal : [options.reveal]) : [];
59
+ const progressive = numberToken(content, 'content-progressive') > 0;
60
+ const surfaceSequence = progressive && hasSurfaceSequence(content);
61
+ const { targets: revealTargets, orders } = revealGroups(content, reveal, options.revealPseudoElements ?? [], progressive);
62
+ if (revealTargets.length) {
63
+ const start = numberToken(content, progressive ? 'component-content-offset' : `${pattern}-reveal-offset`);
64
+ const end = numberToken(content, progressive ? 'component-content-end' : `${pattern}-reveal-end`);
65
+ const blur = token(content, `${pattern}-reveal-blur`);
66
+ const plan = contentTimeline(content, orders.length, { start, end, stepToken: 'component-content-stagger', capToken: 'component-content-stagger-span' });
67
+ for (const { element, pseudoElement, order } of revealTargets) {
68
+ const index = orders.indexOf(order);
69
+ const sequence = surfaceSequence ? (behavior.revealTiming ?? ((count, index) => sequenceTiming(content, count, index)))(orders.length + 1, index + 1) : undefined;
70
+ const range = sequence ?? plan.at(index);
71
+ if (!pseudoElement && (element === content || !content.contains(element)))
72
+ throw new Error('Reveal targets must be inner elements, separate from the surface.');
73
+ descriptions.push(...contentTracks({ element, pseudoElement, ...range, blur, easing: token(content, 'easing-reveal'),
74
+ distance: recipe?.contentEffect === 'slide-blur-fade' ? token(content, 'reveal-distance') : undefined }));
75
+ }
76
+ }
77
+ for (const track of descriptions) {
78
+ const timing = { duration: time, easing: track.easing ?? easing, fill: 'both', composite: track.composite ?? 'replace' };
79
+ const a = track.pseudoElement
80
+ ? new Animation(new KeyframeEffect(track.element, track.frames, { ...timing, pseudoElement: track.pseudoElement }), document.timeline)
81
+ : track.element.animate(track.frames, timing);
82
+ a.pause();
83
+ a.currentTime = target ? 0 : time;
84
+ animations.push(a);
85
+ }
86
+ }
87
+ catch (error) {
88
+ settle();
89
+ throw error;
90
+ }
91
+ }
92
+ // Reversing at an exact endpoint must settle, rather than letting play()
93
+ // auto-rewind the effect to the opposite end of the timeline.
94
+ if (animations.every(a => target ? Number(a.currentTime) >= Number(a.effect.getTiming().duration) : Number(a.currentTime) <= 0)) {
95
+ settle();
96
+ return;
97
+ }
98
+ const version = ++generation;
99
+ animations.forEach(a => { a.playbackRate = target ? 1 : -1; a.play(); });
100
+ Promise.all(animations.map(a => a.finished)).then(() => {
101
+ if (version === generation && state !== 'destroyed')
102
+ settle();
103
+ }).catch(() => { });
104
+ }
105
+ function seek(progress) {
106
+ if (!Number.isFinite(progress))
107
+ throw new Error('Motion progress must be finite.');
108
+ if (state === 'destroyed')
109
+ return;
110
+ const p = Math.max(0, Math.min(1, progress));
111
+ if (reduced.matches) {
112
+ const next = p >= .5;
113
+ if (next !== target)
114
+ set(next);
115
+ else if (animations.length)
116
+ settle();
117
+ return;
118
+ }
119
+ if (!animations.length) {
120
+ if ((p === 0 && state === 'closed') || (p === 1 && state === 'open'))
121
+ return;
122
+ set(state !== 'open');
123
+ }
124
+ if (!animations.length)
125
+ return;
126
+ generation++;
127
+ target = p > 0;
128
+ animations.forEach(a => { a.pause(); a.currentTime = p * Number(a.effect.getTiming().duration); });
129
+ if (p === 0 || p === 1)
130
+ settle();
131
+ else if (state !== 'opening')
132
+ notify('opening');
133
+ }
134
+ function preferenceChanged() { if (reduced.matches && animations.length)
135
+ settle(); }
136
+ reduced.addEventListener('change', preferenceChanged);
137
+ return {
138
+ get state() { return state; }, get isOpen() { return target; },
139
+ seek,
140
+ open: () => set(true), close: () => set(false), toggle: () => set(!target),
141
+ destroy() {
142
+ if (state === 'destroyed')
143
+ return;
144
+ cancel();
145
+ restore();
146
+ reduced.removeEventListener('change', preferenceChanged);
147
+ notify('destroyed');
148
+ }
149
+ };
150
+ }
@@ -0,0 +1,13 @@
1
+ import type { Track } from './animation.js';
2
+ export interface ContentTrackOptions {
3
+ element: HTMLElement;
4
+ pseudoElement?: string;
5
+ start: number;
6
+ end: number;
7
+ blur: string;
8
+ easing: string;
9
+ distance?: string;
10
+ blurEnd?: number;
11
+ }
12
+ /** The single fade/blur recipe; Content adds an additive vertical translation. */
13
+ export declare function contentTracks(options: ContentTrackOptions): Track[];
@@ -0,0 +1,27 @@
1
+ /** The single fade/blur recipe; Content adds an additive vertical translation. */
2
+ export function contentTracks(options) {
3
+ const { element, pseudoElement, start, end, blur, easing, distance, blurEnd } = options;
4
+ if (!CSS.supports('filter', `blur(${blur})`))
5
+ throw new Error('Content blur must be a CSS length.');
6
+ const style = getComputedStyle(element, pseudoElement), filter = style.filter, opacity = style.opacity;
7
+ const blurred = `${filter === 'none' ? '' : `${filter} `}blur(${blur})`;
8
+ const display = style.display === 'inline' ? { display: 'inline-block' } : {};
9
+ const from = { ...display, opacity: 0, ...(blurEnd === undefined ? { filter: blurred } : {}) };
10
+ const to = { ...display, opacity, ...(blurEnd === undefined ? { filter } : {}) };
11
+ const tracks = [{ element, pseudoElement, easing: 'linear', frames: [
12
+ { ...from, offset: 0 }, { ...from, offset: start, easing }, { ...to, offset: end }, { ...to, offset: 1 }
13
+ ] }];
14
+ if (distance) {
15
+ if (!CSS.supports('translate', `0 ${distance}`))
16
+ throw new Error('Content distance must be a CSS length.');
17
+ tracks.push({ element, pseudoElement, easing: 'linear', composite: 'add', frames: [
18
+ { translate: `0 ${distance}`, offset: 0 }, { translate: `0 ${distance}`, offset: start, easing },
19
+ { translate: '0 0', offset: end }, { translate: '0 0', offset: 1 }
20
+ ] });
21
+ }
22
+ if (blurEnd !== undefined)
23
+ tracks.push({ element, pseudoElement, easing: 'linear', frames: [
24
+ { filter: blurred, offset: 0, easing }, { filter, offset: blurEnd }, { filter, offset: 1 }
25
+ ] });
26
+ return tracks;
27
+ }
@@ -0,0 +1,18 @@
1
+ /** Keep icon/label pairs and semantic sections together; never reveal nested groups twice. */
2
+ export declare function contentGroups(roots: HTMLElement[]): HTMLElement[];
3
+ /** Bound the stagger so any number of groups still finishes inside the same timeline. */
4
+ export declare function contentStagger(content: HTMLElement, count: number, span?: number): number;
5
+ /** Opt in to a shared overlapping sequence: surface first, then its groups. */
6
+ export declare function hasSurfaceSequence(content: HTMLElement): boolean;
7
+ export declare function sequenceTiming(content: HTMLElement, count: number, index: number): {
8
+ start: number;
9
+ end: number;
10
+ };
11
+ export declare function revealGroups(content: HTMLElement, roots: HTMLElement[], pseudos: string[], progressive: boolean): {
12
+ targets: {
13
+ element: HTMLElement;
14
+ pseudoElement: string | undefined;
15
+ order: number;
16
+ }[];
17
+ orders: number[];
18
+ };
@@ -0,0 +1,34 @@
1
+ import { contentTimeline } from './content-timing.js';
2
+ /** Keep icon/label pairs and semantic sections together; never reveal nested groups twice. */
3
+ export function contentGroups(roots) {
4
+ const selector = '[data-motion-order],[data-motion-content-group],header,footer,[role="menuitem"],button,h1,h2,h3,h4,h5,h6,p,small,figure,figcaption,label,input,textarea,select';
5
+ const groups = roots.flatMap(root => {
6
+ if (root.matches(selector))
7
+ return [root];
8
+ const candidates = Array.from(root.querySelectorAll(selector));
9
+ return candidates.length ? candidates.filter(item => !candidates.some(other => other !== item && other.contains(item))) : [root];
10
+ });
11
+ return Array.from(new Set(groups)).filter(item => !groups.some(other => other !== item && other.contains(item))).map((element, index) => ({ element, index, order: Number(element.dataset.motionOrder ?? index) })).sort((a, b) => a.order - b.order || a.index - b.index).map(item => item.element);
12
+ }
13
+ /** Bound the stagger so any number of groups still finishes inside the same timeline. */
14
+ export function contentStagger(content, count, span = 1) {
15
+ return contentTimeline(content, count, { end: span }).step;
16
+ }
17
+ /** Opt in to a shared overlapping sequence: surface first, then its groups. */
18
+ export function hasSurfaceSequence(content) {
19
+ return content.matches('[data-motion-sequence="surface-first"],.motion-sequence');
20
+ }
21
+ export function sequenceTiming(content, count, index) {
22
+ return contentTimeline(content, count).at(index);
23
+ }
24
+ export function revealGroups(content, roots, pseudos, progressive) {
25
+ const grouped = progressive ? contentGroups(roots) : roots;
26
+ const generated = Array.from(new Set(pseudos));
27
+ const targets = [
28
+ ...Array.from(new Set(grouped)).map((element, index) => ({ element, pseudoElement: undefined, order: Number(element.dataset.motionOrder ?? (index + generated.length)) })),
29
+ ...generated.map((pseudoElement, index) => ({ element: content, pseudoElement, order: Number(content.getAttribute(pseudoElement === '::before' ? 'data-motion-before-order' : 'data-motion-after-order') ?? index) })),
30
+ ].sort((a, b) => a.order - b.order);
31
+ if (targets.some(target => !Number.isFinite(target.order)))
32
+ throw new Error('Motion content order must be numeric.');
33
+ return { targets, orders: Array.from(new Set(targets.map(target => target.order))) };
34
+ }
@@ -0,0 +1,4 @@
1
+ import type { Track } from './animation.js';
2
+ /** Hold the existing inner layout in its final position while the surface
3
+ * resizes around it. Measurements happen once; reversal reuses native tracks. */
4
+ export declare function fixedContentTracks(content: HTMLElement, frames: Keyframe[], pseudoElements?: ReadonlyArray<'::before' | '::after'>): Track[];