@blukavantui/community 1.1.0 → 1.2.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 (179) hide show
  1. package/dist/build-audit.json +127 -65
  2. package/dist/checkbox.js +1 -1
  3. package/dist/chunk-4LHIQROI.js +0 -0
  4. package/dist/chunk-6T677NSC.js +1 -0
  5. package/dist/chunk-7CEOCZ5L.js +0 -0
  6. package/dist/chunk-7DXFNK7C.js +1 -0
  7. package/dist/chunk-CNIIMGT3.js +0 -0
  8. package/dist/chunk-CSTCEBEK.js +1 -0
  9. package/dist/chunk-DRZM2AL3.js +0 -0
  10. package/dist/{chunk-JVE6CXCJ.js → chunk-EPPETNK6.js} +1 -1
  11. package/dist/chunk-EPPSBHZR.js +1 -0
  12. package/dist/{chunk-NTFHNL7T.js → chunk-G6OW4OZM.js} +1 -1
  13. package/dist/chunk-GS67ZBLI.js +1 -0
  14. package/dist/{chunk-6EHKOVZK.js → chunk-HVR2UDFX.js} +1 -1
  15. package/dist/chunk-IKYBFMHI.js +1 -0
  16. package/dist/chunk-KVOA55NE.js +0 -0
  17. package/dist/{chunk-MZBB5ROJ.js → chunk-LF2HELEZ.js} +1 -1
  18. package/dist/{chunk-UEOCR2C3.js → chunk-OLDA73XG.js} +2 -2
  19. package/dist/{chunk-IZJN3EF7.js → chunk-T45FJNEQ.js} +1 -1
  20. package/dist/chunk-TZSX4OTV.js +1 -0
  21. package/dist/{chunk-GK3INPAZ.js → chunk-V2YPIC6Z.js} +1 -1
  22. package/dist/chunk-V4ZQLQ7L.js +0 -0
  23. package/dist/chunk-VAM3FPCS.js +0 -0
  24. package/dist/chunk-VSTKXZYY.js +0 -0
  25. package/dist/chunk-WG4US4JH.js +1 -0
  26. package/dist/chunk-WGSTNXAQ.js +0 -0
  27. package/dist/chunk-WJHCVECU.js +1 -0
  28. package/dist/chunk-Y4AXWTAD.js +0 -0
  29. package/dist/chunk-YLFXSHKZ.js +0 -0
  30. package/dist/chunk-ZDGZGAYG.js +0 -0
  31. package/dist/command-palette.js +1 -1
  32. package/dist/component-motion.css +39 -0
  33. package/dist/component-motion.d.ts +45 -0
  34. package/dist/component-motion.js +1 -0
  35. package/dist/content.js +1 -1
  36. package/dist/core.css +8 -0
  37. package/dist/core.js +1 -1
  38. package/dist/data-motion.css +4 -0
  39. package/dist/data-motion.d.ts +23 -0
  40. package/dist/data-motion.js +1 -0
  41. package/dist/data.js +1 -1
  42. package/dist/date-picker.js +1 -1
  43. package/dist/date-range-picker.js +1 -1
  44. package/dist/date-time-picker.js +1 -1
  45. package/dist/drawer.js +1 -1
  46. package/dist/essentials.css +8 -0
  47. package/dist/feedback.js +1 -1
  48. package/dist/form-layout.js +1 -1
  49. package/dist/index.js +1 -1
  50. package/dist/inputs.js +1 -1
  51. package/dist/layout.js +1 -1
  52. package/dist/modal.js +1 -1
  53. package/dist/motion/action-feedback.d.ts +14 -0
  54. package/dist/motion/border-glow.d.ts +12 -0
  55. package/dist/motion/button.d.ts +11 -0
  56. package/dist/motion/component-settings.d.ts +51 -0
  57. package/dist/motion/controller.d.ts +33 -0
  58. package/dist/motion/effects.d.ts +30 -0
  59. package/dist/motion/feedback.d.ts +6 -0
  60. package/dist/motion/loading.d.ts +9 -0
  61. package/dist/motion/optional.d.ts +35 -0
  62. package/dist/motion/photo.d.ts +7 -0
  63. package/dist/motion/presence.d.ts +12 -0
  64. package/dist/motion/scene.d.ts +10 -0
  65. package/dist/motion/screen.d.ts +6 -0
  66. package/dist/motion/scroll-reveal.d.ts +13 -0
  67. package/dist/motion/settings-group.d.ts +14 -0
  68. package/dist/motion/stagger.d.ts +12 -0
  69. package/dist/motion/structure.d.ts +18 -0
  70. package/dist/motion/tabs.d.ts +19 -0
  71. package/dist/motion/tokens.d.ts +11 -0
  72. package/dist/motion/value.d.ts +21 -0
  73. package/dist/motion/view-transition.d.ts +14 -0
  74. package/dist/motion-enhancers.d.ts +12 -0
  75. package/dist/motion-enhancers.js +1 -0
  76. package/dist/motion-settings.d.ts +3 -0
  77. package/dist/motion-settings.js +1 -0
  78. package/dist/motion.css +1076 -0
  79. package/dist/motion.d.ts +20 -0
  80. package/dist/motion.js +1 -0
  81. package/dist/native/form-layout.js +1 -1
  82. package/dist/native-motion-components.d.ts +3 -0
  83. package/dist/native-motion-content.d.ts +1 -0
  84. package/dist/native-motion-foundations.d.ts +3 -0
  85. package/dist/native-motion-layout.d.ts +3 -0
  86. package/dist/native-motion-pilot.d.ts +34 -0
  87. package/dist/native-motion.d.ts +71 -0
  88. package/dist/notification.js +1 -1
  89. package/dist/pagination.js +1 -1
  90. package/dist/radio-button.js +1 -1
  91. package/dist/radio-group.js +1 -1
  92. package/dist/sidebar.js +1 -1
  93. package/dist/sizes.json +273 -151
  94. package/dist/skeleton.css +8 -0
  95. package/dist/styles.css +1130 -0
  96. package/dist/switch.js +1 -1
  97. package/dist/table.js +1 -1
  98. package/dist/time-picker.js +1 -1
  99. package/dist/tree-view.js +1 -1
  100. package/dist/web-motion/chip-ripple.d.ts +6 -0
  101. package/dist/web-motion/choice-ripple.d.ts +6 -0
  102. package/dist/web-motion/content.d.ts +9 -0
  103. package/dist/web-motion/data-rows.d.ts +12 -0
  104. package/dist/web-motion/fields.d.ts +6 -0
  105. package/dist/web-motion/navigation.d.ts +6 -0
  106. package/dist/web-motion/notifications.d.ts +6 -0
  107. package/dist/web-motion/select-ripple.d.ts +6 -0
  108. package/dist/web-motion/statistic.d.ts +13 -0
  109. package/dist/web-motion/surfaces.d.ts +10 -0
  110. package/dist/web-motion/tree.d.ts +6 -0
  111. package/docs/FORM-LAYOUT-ESSENTIALS.md +4 -4
  112. package/edition.json +1 -1
  113. package/npm-shrinkwrap.json +2 -2
  114. package/package.json +29 -3
  115. package/scripts/build-motion-css.mjs +36 -0
  116. package/scripts/build-motion-styles.mjs +29 -0
  117. package/scripts/build.mjs +5 -1
  118. package/scripts/motion-entries.mjs +2 -0
  119. package/src/component-motion.ts +101 -0
  120. package/src/data-motion.ts +60 -0
  121. package/src/form-layout/choice.ts +17 -1
  122. package/src/motion/action-feedback.ts +38 -0
  123. package/src/motion/border-glow.ts +41 -0
  124. package/src/motion/button.ts +56 -0
  125. package/src/motion/component-settings.ts +147 -0
  126. package/src/motion/controller.ts +191 -0
  127. package/src/motion/effects.ts +75 -0
  128. package/src/motion/feedback.ts +41 -0
  129. package/src/motion/loading.ts +40 -0
  130. package/src/motion/optional.ts +123 -0
  131. package/src/motion/photo.ts +19 -0
  132. package/src/motion/presence.ts +44 -0
  133. package/src/motion/scene.ts +103 -0
  134. package/src/motion/screen.ts +15 -0
  135. package/src/motion/scroll-reveal.ts +49 -0
  136. package/src/motion/settings-group.ts +28 -0
  137. package/src/motion/stagger.ts +26 -0
  138. package/src/motion/structure.ts +75 -0
  139. package/src/motion/tabs.ts +105 -0
  140. package/src/motion/tokens.ts +2 -0
  141. package/src/motion/value.ts +61 -0
  142. package/src/motion/view-transition.ts +57 -0
  143. package/src/motion-enhancers.ts +12 -0
  144. package/src/motion-settings.ts +3 -0
  145. package/src/motion.ts +20 -0
  146. package/src/native-motion-components.ts +76 -0
  147. package/src/native-motion-content.ts +10 -0
  148. package/src/native-motion-foundations.ts +56 -0
  149. package/src/native-motion-layout.ts +35 -0
  150. package/src/native-motion-pilot.ts +151 -0
  151. package/src/native-motion.ts +188 -0
  152. package/src/web-motion/chip-ripple.ts +31 -0
  153. package/src/web-motion/choice-ripple.ts +30 -0
  154. package/src/web-motion/content.ts +74 -0
  155. package/src/web-motion/data-rows.ts +106 -0
  156. package/src/web-motion/fields.ts +52 -0
  157. package/src/web-motion/navigation.ts +62 -0
  158. package/src/web-motion/notifications.ts +67 -0
  159. package/src/web-motion/select-ripple.ts +23 -0
  160. package/src/web-motion/statistic.ts +23 -0
  161. package/src/web-motion/surfaces.ts +137 -0
  162. package/src/web-motion/tree.ts +44 -0
  163. package/styles/component-motion.css +39 -0
  164. package/styles/data-motion.css +4 -0
  165. package/styles/motion-optional.css +15 -0
  166. package/styles/motion-presets.css +92 -0
  167. package/styles/motion-tokens.css +6 -0
  168. package/styles/motion.css +125 -0
  169. package/styles/skeleton.css +8 -0
  170. package/test/component-motion-settings.test.mjs +35 -0
  171. package/test/content-overlay-motion.test.mjs +36 -0
  172. package/test/isolation.test.mjs +1 -1
  173. package/test/motion.test.mjs +215 -0
  174. package/dist/chunk-GXHANADL.js +0 -1
  175. /package/dist/{chunk-3PNBQVYS.js → chunk-2BSBXDOT.js} +0 -0
  176. /package/dist/{chunk-H7ETPJVW.js → chunk-3PODK22R.js} +0 -0
  177. /package/dist/{chunk-SIU7DSOA.js → chunk-KBWB4OIF.js} +0 -0
  178. /package/dist/{chunk-OPMD7QNV.js → chunk-W3H7XTA7.js} +0 -0
  179. /package/dist/{chunk-XWXEH3SW.js → chunk-XVMJG26X.js} +0 -0
@@ -0,0 +1,23 @@
1
+ import type {MotionController,MotionOptions} from '../motion/controller.js';
2
+ import {createRipple} from '../motion/optional.js';
3
+
4
+ /** Decorate an actual open/close or selection change, after Select has updated
5
+ * its label. The component continues to own focus, keyboard and value events. */
6
+ export function enhanceSelectRipple(root:HTMLElement,motion:MotionController,timing:MotionOptions={}) {
7
+ let dead=false,frame=0,current:ReturnType<typeof createRipple>|undefined;
8
+ const state=()=>root.querySelector('.blu-select__trigger')?.getAttribute('aria-expanded')+'|'+root.querySelector<HTMLSelectElement>('.blu-select__native')?.value;
9
+ const activate=(event:Event)=>{
10
+ if(!(event.target instanceof Element)||!event.target.closest('.blu-select'))return;
11
+ const trigger=root.querySelector<HTMLButtonElement>('.blu-select__trigger');
12
+ if(!trigger||trigger.disabled||trigger.getAttribute('aria-readonly')==='true')return;
13
+ if(event instanceof KeyboardEvent&&!['Enter',' ','ArrowDown','ArrowUp','Home','End'].includes(event.key))return;
14
+ const before=state();
15
+ const point=event instanceof MouseEvent&&event.detail>0&&event.target.closest('.blu-select__trigger')?{clientX:event.clientX,clientY:event.clientY}:undefined;
16
+ cancelAnimationFrame(frame);frame=requestAnimationFrame(()=>{
17
+ frame=0;if(dead||!root.contains(trigger)||trigger.disabled||motion.mode!=='full'||state()===before)return;
18
+ current?.destroy();current=createRipple(trigger,motion,{duration:1000,easing:'ease-out',...timing,listen:false});current.play(point);
19
+ });
20
+ };
21
+ root.addEventListener('click',activate,true);root.addEventListener('keydown',activate,true);
22
+ return {destroy(){if(dead)return;dead=true;cancelAnimationFrame(frame);root.removeEventListener('click',activate,true);root.removeEventListener('keydown',activate,true);current?.destroy();}};
23
+ }
@@ -0,0 +1,23 @@
1
+ import type {MotionController} from '../motion/controller.js';
2
+ import {createNumberTransition} from '../motion/value.js';
3
+ type Values={Value?:number;Precision?:number;Locale?:string;IsLoading?:boolean};
4
+ /** Exact value remains accessible throughout the decorative number transition. */
5
+ export function enhanceStatistic(root:HTMLElement,motion:MotionController,read:()=>Values) {
6
+ const previousKind=root.getAttribute('data-blu-component-motion');root.setAttribute('data-blu-component-motion','statistic');
7
+ let dead=false,target:HTMLElement|null=null,previous=read().Value??0;
8
+ let number:ReturnType<typeof createNumberTransition>|undefined;
9
+ const sync=()=>{
10
+ if(dead)return;
11
+ const next=root.querySelector<HTMLElement>('.blu-statistic__value > strong');
12
+ if(next===target)return;
13
+ number?.destroy();number=undefined;target=next;
14
+ if(!next)return;
15
+ const options=read(),value=Number(options.Value??0);
16
+ if(!Number.isFinite(value))return;
17
+ const formatter=new Intl.NumberFormat(options.Locale||'en-US',{minimumFractionDigits:options.Precision??0,maximumFractionDigits:options.Precision??0});
18
+ number=createNumberTransition(next,motion,{value:previous,format:v=>formatter.format(v)});
19
+ void number.set(value,{duration:750,easing:'ease-in-out'});previous=value;
20
+ };
21
+ sync();const observer=new MutationObserver(sync);observer.observe(root,{childList:true,subtree:true});
22
+ return {capture(){},destroy(){if(dead)return;dead=true;observer.disconnect();number?.destroy();if(previousKind===null)root.removeAttribute('data-blu-component-motion');else root.setAttribute('data-blu-component-motion',previousKind);}};
23
+ }
@@ -0,0 +1,137 @@
1
+ import type { MotionController, MotionEffect } from '../motion/controller.js';
2
+
3
+ /** Existing components own focus trapping, dismissal and events; the first dialog
4
+ * presentation receives named context after paint. A closing surface is an inert,
5
+ * short-lived visual copy; its bounds are frozen so focus restoration cannot reflow it.
6
+ */
7
+ export function observeSurface(root: HTMLElement, motion: MotionController, kind: 'modal' | 'drawer' | 'palette' | 'select' | 'picker' | 'cascader' | 'tree-select') {
8
+ const previous = root.getAttribute('data-blu-surface-motion'); root.setAttribute('data-blu-surface-motion', kind);
9
+ const selector = kind === 'cascader' ? '.blu-cascader__popup' : kind === 'tree-select' ? '.blu-tree-select__popup' : kind === 'picker' ? '.blu-picker__popup' : kind === 'select' ? '.blu-select__popup' : kind === 'palette' ? '.blu-command__dialog' : '.blu-overlay';
10
+ let target: HTMLElement | null = null, wasOpen = false, destroyed = false, revision = 0, trackingFrame = 0, exitDeadline = 0;
11
+ let dialogFocusFrame = 0, dialogPresented = false;
12
+ let ghost: HTMLElement | null = null, ghostTargets: HTMLElement[] = [], rect: DOMRect | undefined, panelRect: DOMRect | undefined;
13
+ let font = '', background = '', color = '', backdrop = '#18223066', blur = 'none', opacity = '1', panelOpacity = '1', scrollPositions: number[] = [];
14
+ const win = root.ownerDocument.defaultView!;
15
+ function isOpen(node: HTMLElement) { return kind === 'palette' ? (node as HTMLDialogElement).open : !node.hidden; }
16
+ function entrance(): MotionEffect {
17
+ if (kind !== 'drawer') return kind === 'modal' ? 'scale' : 'slide-down';
18
+ return target?.dataset.placement === 'left' ? 'slide-right' : target?.dataset.placement === 'bottom' ? 'slide-up' : 'slide-left';
19
+ }
20
+ function remember() {
21
+ if (!target || !isOpen(target)) return;
22
+ const panel = target.querySelector<HTMLElement>('.blu-overlay__panel') || target;
23
+ rect = target.getBoundingClientRect(); panelRect = panel.getBoundingClientRect();
24
+ const css = win.getComputedStyle(target); font = css.font; background = css.backgroundColor; color = css.color;
25
+ opacity = panel === target ? '1' : css.opacity; panelOpacity = win.getComputedStyle(panel).opacity;
26
+ if (kind === 'palette') { const scrim = win.getComputedStyle(target, '::backdrop'); backdrop = scrim.backgroundColor; blur = scrim.backdropFilter; }
27
+ scrollPositions = [target, ...Array.from(target.querySelectorAll<HTMLElement>('*'))].map(el => el.scrollTop);
28
+ }
29
+ function stopTracking() { win.cancelAnimationFrame(trackingFrame); trackingFrame = 0; }
30
+ function cancelDialogFocus() { win.cancelAnimationFrame(dialogFocusFrame); dialogFocusFrame = 0; }
31
+ function clearGhost() {
32
+ ++revision;
33
+ win.clearTimeout(exitDeadline); exitDeadline = 0;
34
+ for (const element of ghostTargets) motion.cancel(element); ghostTargets = [];
35
+ ghost?.remove(); ghost = null;
36
+ }
37
+ function frozenBounds(element: HTMLElement, box: DOMRect) {
38
+ Object.assign(element.style, { position: 'fixed', boxSizing: 'border-box', inset: 'auto', left: box.left + 'px', top: box.top + 'px', width: box.width + 'px', height: box.height + 'px', maxWidth: 'none', maxHeight: 'none', margin: '0', transform: 'none' });
39
+ }
40
+ function closingCopy(node: HTMLElement) {
41
+ if (node.ownerDocument.querySelector('.blu-overlay:not([hidden]):not([data-blu-exit-visual]), dialog[open]')) return;
42
+ if (!rect?.width || !rect.height || !node.isConnected || node.querySelector('video,canvas,iframe')) return;
43
+ const copy = node.cloneNode(true) as HTMLElement;
44
+ const sourceNodes = [node, ...Array.from(node.querySelectorAll<HTMLElement>('*'))], copyNodes = [copy, ...Array.from(copy.querySelectorAll<HTMLElement>('*'))];
45
+ copyNodes.forEach((el, index) => {
46
+ if (index && el.hasAttribute('popover')) el.hidden = true;
47
+ for (const name of ['id','name','autofocus','popover','aria-live','aria-labelledby','aria-describedby','aria-controls']) el.removeAttribute(name);
48
+ // Inert + no names prevent interaction/submission. Preserve enabled styles to avoid a flash.
49
+ if (el instanceof HTMLInputElement) { const source = sourceNodes[index] as HTMLInputElement; el.value = source.type === 'password' ? '' : source.value; el.checked = source.checked; }
50
+ else if (el instanceof HTMLTextAreaElement || el instanceof HTMLSelectElement) el.value = (sourceNodes[index] as HTMLTextAreaElement).value;
51
+ el.style.pointerEvents = 'none';
52
+ });
53
+ copy.hidden = false; copy.inert = true; copy.setAttribute('aria-hidden', 'true');
54
+ const parent = node.parentElement!;
55
+ if (kind === 'modal' || kind === 'drawer') { ghost = copy; parent.append(copy); }
56
+ else {
57
+ const portal = node.ownerDocument.createElement('div');
58
+ portal.className = kind === 'palette' ? 'blu-command blu-motion-exit-surface' : 'blu-motion-exit-surface';
59
+ portal.style.cssText = `position:fixed;inset:0;width:100vw;height:100dvh;margin:0;padding:0;border:0;max-width:none;max-height:none;background:${kind === 'palette' ? backdrop : 'transparent'};backdrop-filter:${kind === 'palette' ? blur : 'none'};overflow:visible;pointer-events:none;`;
60
+ if (kind === 'palette') {
61
+ const surface = node.ownerDocument.createElement('div'); surface.className = copy.className; surface.append(...Array.from(copy.childNodes));
62
+ frozenBounds(surface, rect); Object.assign(surface.style, { background, color, font }); portal.append(surface);
63
+ } else { frozenBounds(copy, rect); copy.style.font = font; portal.append(copy); }
64
+ ghost = portal; parent.append(portal);
65
+ }
66
+ ghost.dataset.bluExitVisual = kind; ghost.inert = true; ghost.setAttribute('aria-hidden', 'true'); ghost.style.pointerEvents = 'none';
67
+ ghost.setAttribute('popover', 'manual');
68
+ if (ghost.showPopover) { try { ghost.showPopover(); } catch { ghost.removeAttribute('popover'); } }
69
+ const visual = ghost.querySelector<HTMLElement>('.blu-overlay__panel, .blu-command__dialog, .blu-select__popup, .blu-picker__popup') || ghost;
70
+ if (panelRect && (kind === 'modal' || kind === 'drawer')) frozenBounds(visual, panelRect);
71
+ visual.style.opacity = panelOpacity;
72
+ copyNodes.forEach((el, index) => { el.scrollTop = scrollPositions[index] || 0; });
73
+ // Fade exactly one layer. Fading both the parent and child multiplies opacity and
74
+ // makes the content disappear ahead of its backdrop. Popup exits do not rescale.
75
+ const own = revision, tasks = [motion.animate(ghost, [{ opacity }, { opacity: 0 }], { phase: 'exit', duration: 450, easing: 'ease-in-out' }, 'fade')];
76
+ ghostTargets = [ghost];
77
+ if (kind === 'drawer') {
78
+ const direction = node.dataset.placement === 'left' ? 'translateX(-18px)' : node.dataset.placement === 'bottom' ? 'translateY(18px)' : 'translateX(18px)';
79
+ ghostTargets.push(visual);
80
+ tasks.push(motion.animate(visual, [{ transform: 'none' }, { transform: direction }], { duration: 450, easing: 'ease-in-out' }));
81
+ }
82
+ // Some browsers suspend WAAPI for off-screen/inert top-layer visuals. The
83
+ // real popup has already closed; never let a paused cosmetic copy linger.
84
+ exitDeadline = win.setTimeout(() => { if (!destroyed && own === revision) clearGhost(); }, (motion.resolveOptions?.({ duration: 450 }).duration ?? (motion.mode === 'full' ? 450 : 200)) + 250);
85
+ void Promise.all(tasks).finally(() => { if (!destroyed && own === revision) clearGhost(); });
86
+ }
87
+ const resize = new ResizeObserver(() => { if (wasOpen) remember(); });
88
+ function sync() {
89
+ if (destroyed) return;
90
+ const next = root.matches(selector) ? root : root.querySelector<HTMLElement>(selector);
91
+ if (target !== next) { if (target) resize.unobserve(target); target = next; wasOpen = false; if (target) resize.observe(target); }
92
+ if (!target) return;
93
+ const open = isOpen(target);
94
+ if (open === wasOpen) return;
95
+ wasOpen = open; stopTracking(); cancelDialogFocus(); clearGhost();
96
+ const panel = target.querySelector<HTMLElement>('.blu-overlay__panel') || target;
97
+ motion.cancel(panel); motion.cancel(target);
98
+ if (open) {
99
+ remember(); const own = revision;
100
+ // Safari can announce only "button" when focus enters a newly exposed
101
+ // popover before its dialog subtree has reached the accessibility tree.
102
+ // On the first presentation, focus the named dialog after paint. Never
103
+ // override navigation the user has already made inside that dialog.
104
+ if ((kind === 'modal' || kind === 'drawer') && !dialogPresented) {
105
+ const initialFocus = root.ownerDocument.activeElement;
106
+ dialogFocusFrame = win.requestAnimationFrame(() => {
107
+ dialogFocusFrame = win.requestAnimationFrame(() => {
108
+ dialogFocusFrame = 0;
109
+ if (destroyed || !wasOpen || own !== revision) return;
110
+ dialogPresented = true;
111
+ if (panel.contains(initialFocus) && root.ownerDocument.activeElement === initialFocus) panel.focus({ preventScroll: true });
112
+ });
113
+ });
114
+ }
115
+ const tasks = [motion.play(panel, entrance(), { duration: kind === 'drawer' ? 450 : 350, easing: 'ease-out' })];
116
+ if (panel !== target) tasks.push(motion.play(target, 'fade', { duration: 280 }));
117
+ // ResizeObserver does not observe transforms. Track opening frames and record
118
+ // the final resting bounds, rather than reusing a partly translated/scaled box.
119
+ const trackOpening = () => { if (destroyed || !wasOpen || own !== revision) return; remember(); trackingFrame = win.requestAnimationFrame(trackOpening); };
120
+ trackingFrame = win.requestAnimationFrame(trackOpening);
121
+ void Promise.all(tasks).then(() => { if (!destroyed && wasOpen && own === revision) { stopTracking(); remember(); } });
122
+ } else if (motion.mode !== 'off' && !root.closest('.blu-motion-off, [data-blu-motion="off"]')) closingCopy(target);
123
+ }
124
+ const observer = new MutationObserver(sync);
125
+ observer.observe(root, { subtree: true, childList: true, attributes: true, attributeFilter: ['hidden', 'open'] });
126
+ const track = () => { if (wasOpen) remember(); };
127
+ win.addEventListener('resize', track); win.addEventListener('scroll', track, true);
128
+ root.ownerDocument.addEventListener('pointerdown', track, true); root.ownerDocument.addEventListener('keydown', track, true);
129
+ sync();
130
+ return { sync, capture: remember, destroy() {
131
+ if (destroyed) return; destroyed = true; stopTracking(); cancelDialogFocus(); observer.disconnect(); resize.disconnect(); clearGhost();
132
+ if (previous === null) root.removeAttribute('data-blu-surface-motion'); else root.setAttribute('data-blu-surface-motion', previous);
133
+ win.removeEventListener('resize', track); win.removeEventListener('scroll', track, true);
134
+ root.ownerDocument.removeEventListener('pointerdown', track, true); root.ownerDocument.removeEventListener('keydown', track, true);
135
+ if (target) { motion.cancel(target); const panel = target.querySelector<HTMLElement>('.blu-overlay__panel'); if (panel) motion.cancel(panel); }
136
+ } };
137
+ }
@@ -0,0 +1,44 @@
1
+ import type { MotionController } from '../motion/controller.js';
2
+ import { preserveStyles } from '../motion/structure.js';
3
+
4
+ /** The tree runtime owns hidden/expanded state and keyboard navigation. During a
5
+ * full-motion collapse only, keep the inert visual in layout until it reaches zero. */
6
+ export function enhanceTree(root: HTMLElement, motion: MotionController) {
7
+ const previous = root.getAttribute('data-blu-component-motion'); root.setAttribute('data-blu-component-motion', 'tree');
8
+ type State = { open: boolean; height: number; revision: number; release(): void };
9
+ const groups = new Map<HTMLElement, State>(); let dead = false;
10
+ const resize = new ResizeObserver(entries => {
11
+ for (const entry of entries) { const el = entry.target as HTMLElement, state = groups.get(el); if (state && (!el.hidden || el.style.display === 'block')) state.height = el.getBoundingClientRect().height; }
12
+ });
13
+ function sync() {
14
+ if (dead) return;
15
+ for (const [group, state] of groups) if (!root.contains(group)) { ++state.revision; motion.cancel(group); state.release(); resize.unobserve(group); groups.delete(group); }
16
+ for (const group of root.querySelectorAll<HTMLElement>('.blu-tree__group')) {
17
+ let state = groups.get(group);
18
+ if (!state) { state = { open: !group.hidden, height: group.getBoundingClientRect().height, revision: 0, release() {} }; groups.set(group, state); resize.observe(group); continue; }
19
+ const open = !group.hidden;
20
+ if (state.open === open) continue;
21
+ const from = state.height, own = ++state.revision;
22
+ state.open = open; motion.cancel(group); state.release(); state.release = () => {};
23
+ if (group.parentElement?.closest('[hidden]')) continue;
24
+ const full = motion.mode === 'full' && !group.closest('.blu-motion-off,[data-blu-motion=off],[data-blu-motion=reduced]');
25
+ if (!full) { if (open) void motion.play(group, 'fade', { duration: 350 }); continue; }
26
+ const restore = preserveStyles(group, ['display', 'overflow', 'height']);
27
+ const inert = group.inert, ariaHidden = group.getAttribute('aria-hidden');
28
+ state.release = () => { restore(); group.inert = inert; if (ariaHidden === null) group.removeAttribute('aria-hidden'); else group.setAttribute('aria-hidden', ariaHidden); };
29
+ if (!open) { group.inert = true; group.setAttribute('aria-hidden', 'true'); group.style.setProperty('display', 'block', 'important'); }
30
+ const to = open ? group.scrollHeight : 0;
31
+ group.style.overflow = 'hidden'; group.style.height = to + 'px';
32
+ void motion.animate(group, [{ height: from + 'px' }, { height: to + 'px' }], { duration: 650, easing: 'ease-in-out' }).then(() => {
33
+ if (!dead && state!.revision === own) { state!.release(); state!.release = () => {}; state!.height = group.getBoundingClientRect().height; }
34
+ });
35
+ }
36
+ }
37
+ const observer = new MutationObserver(sync); sync();
38
+ observer.observe(root, { subtree: true, childList: true, attributes: true, attributeFilter: ['hidden'] });
39
+ return { destroy() {
40
+ if (dead) return; dead = true; observer.disconnect(); resize.disconnect();
41
+ for (const [group, state] of groups) { ++state.revision; motion.cancel(group); state.release(); } groups.clear();
42
+ if (previous === null) root.removeAttribute('data-blu-component-motion'); else root.setAttribute('data-blu-component-motion', previous);
43
+ } };
44
+ }
@@ -0,0 +1,39 @@
1
+ /* Opt-in web motion. Existing components remain unchanged without the integration. */
2
+ [data-blu-component-motion=field] :is(.blu-text,.blu-textarea,.blu-select__trigger,.blu-choice__mark,.blu-text__action){transition:background-color var(--blu-component-motion-duration,280ms) var(--blu-component-motion-ease,ease),border-color var(--blu-component-motion-duration,280ms) var(--blu-component-motion-ease,ease),box-shadow var(--blu-component-motion-duration,280ms) var(--blu-component-motion-ease,ease),color var(--blu-component-motion-duration,280ms) var(--blu-component-motion-ease,ease)}
3
+ [data-blu-component-motion=field] .blu-choice--switch .blu-choice__mark::after{transition:transform var(--blu-component-motion-duration,320ms) var(--blu-component-motion-ease,cubic-bezier(.2,.8,.2,1))}
4
+ [data-blu-component-motion=field] .blu-choice--radio .blu-choice__mark::after{content:'';width:7px;height:7px;border-radius:50%;background:currentColor;transform:scale(0);transition:transform var(--blu-component-motion-duration,280ms) var(--blu-component-motion-ease,ease-out)}
5
+ [data-blu-component-motion=field] .blu-choice--radio input:checked+.blu-choice__mark::after{transform:scale(1)}
6
+ [data-blu-component-motion=field] .blu-select__trigger::after{transition:transform var(--blu-component-motion-duration,280ms) var(--blu-component-motion-ease,ease-out)}
7
+ [data-blu-component-motion=field] .blu-select__trigger[aria-expanded=true]::after{transform:translateY(2px) rotate(225deg)}
8
+ [data-blu-exit-visual] :disabled{opacity:1!important}
9
+ /* Focus rings and validation semantics stay immediate; only decorative feedback moves. */
10
+ :is([data-blu-motion=reduced],[data-blu-motion=off],.blu-motion-off) [data-blu-component-motion=field] *,
11
+ :is([data-blu-motion=reduced],[data-blu-motion=off],.blu-motion-off) [data-blu-component-motion=field] *::after{transition:none!important;animation:none!important}
12
+ @media(prefers-reduced-motion:reduce){[data-blu-component-motion=field] *,[data-blu-component-motion=field] *::after{transition:none!important;animation:none!important}}
13
+ [data-blu-surface-motion] :is(.blu-select__option,.blu-command__option,.blu-control){transition:background-color var(--blu-component-motion-duration,280ms) var(--blu-component-motion-ease,ease),color var(--blu-component-motion-duration,280ms) var(--blu-component-motion-ease,ease),border-color var(--blu-component-motion-duration,280ms) var(--blu-component-motion-ease,ease)}
14
+ :is([data-blu-motion=reduced],[data-blu-motion=off],.blu-motion-off) [data-blu-surface-motion] *{transition:none!important}
15
+ @media(prefers-reduced-motion:reduce){[data-blu-surface-motion] *{transition:none!important}}
16
+ /* Side drawers meet both viewport edges; the content scrolls between fixed header/footer. */
17
+ [data-blu-surface-motion=drawer].blu-overlay:is([data-placement=left],[data-placement=right]){padding:0;align-items:stretch}
18
+ [data-blu-surface-motion=drawer].blu-overlay:is([data-placement=left],[data-placement=right])>.blu-overlay__panel{margin:0;height:100dvh;max-height:100dvh;min-height:0;border-radius:0}
19
+ [data-blu-surface-motion=drawer] :is(.blu-overlay__header,.blu-overlay__footer){flex-shrink:0}
20
+ /* Existing notification keyframes are replaced by the scoped controller. */
21
+ [data-blu-component-motion=notifications] .blu-notification{animation:none}
22
+ [data-blu-component-motion=tree] .blu-tree__row{transition:background-color var(--blu-component-motion-duration,280ms) var(--blu-component-motion-ease,ease),color var(--blu-component-motion-duration,280ms) var(--blu-component-motion-ease,ease)}
23
+ [data-blu-surface-motion=picker] :is(.blu-picker__button,.blu-picker__input,.blu-picker__day){transition:background-color var(--blu-component-motion-duration,280ms) var(--blu-component-motion-ease,ease),border-color var(--blu-component-motion-duration,280ms) var(--blu-component-motion-ease,ease),color var(--blu-component-motion-duration,280ms) var(--blu-component-motion-ease,ease)}
24
+ :is([data-blu-motion=reduced],[data-blu-motion=off],.blu-motion-off) [data-blu-component-motion=tree] *{transition:none!important}
25
+ @media(prefers-reduced-motion:reduce){[data-blu-component-motion=tree] *{transition:none!important}}
26
+ /* Web-only active-location markers: existing links retain focus and behavior. */
27
+ .blu-motion-navigation-marker{position:absolute;display:block;border-radius:3px;background:var(--bluui-primary,#5144ed);pointer-events:none;transform-origin:top left;z-index:1}
28
+ .blu-motion-navigation-marker[hidden]{display:none}
29
+ .blu-motion-navigation-marker[data-kind=pagination]{box-sizing:border-box;border:2px solid var(--bluui-primary,#5144ed);border-radius:8px;background:transparent}
30
+ [data-blu-navigation-motion] .blu-navigation-menu .blu-navigation-menu__link--active{color:var(--bluui-primary,#5144ed);background:var(--bluui-color-primary-soft,#efedff)}
31
+ @media(forced-colors:active){.blu-motion-navigation-marker{background:Highlight}}
32
+ @media(forced-colors:active){.blu-motion-navigation-marker[data-kind=pagination]{background:transparent;border-color:Highlight}}
33
+
34
+ /* Shared finite transition settings; continuous loading keeps its own cadence. */
35
+ .blu-motion-button.blu-button{transition-duration:var(--blu-component-motion-duration,var(--blu-motion-duration));transition-timing-function:var(--blu-component-motion-ease,var(--blu-motion-ease))}
36
+ /* A preference may sit on the component root itself, not only its parent. */
37
+ :is([data-blu-component-motion],[data-blu-surface-motion],[data-blu-navigation-motion]):is([data-blu-motion=reduced],[data-blu-motion=off],.blu-motion-off) *,
38
+ :is([data-blu-component-motion],[data-blu-surface-motion],[data-blu-navigation-motion]):is([data-blu-motion=reduced],[data-blu-motion=off],.blu-motion-off) *::before,
39
+ :is([data-blu-component-motion],[data-blu-surface-motion],[data-blu-navigation-motion]):is([data-blu-motion=reduced],[data-blu-motion=off],.blu-motion-off) *::after{transition:none!important;animation:none!important}
@@ -0,0 +1,4 @@
1
+ .blu-data-motion-exits { position: absolute; inset: 0; overflow: hidden; pointer-events: none; z-index: 3; border-radius: inherit; }
2
+ .blu-data-motion-exit { position: absolute; display: grid; background: var(--bluui-surface, #fff); color: var(--bluui-text-muted, #68758b); font-size: 13px; align-items: center; border-bottom: 1px solid var(--bluui-border, #dce3ed); pointer-events: none; }
3
+ .blu-data-motion-exit > span { padding: 10px 12px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
4
+ @media (forced-colors: active) { .blu-data-motion-exits { display: none; } }
@@ -0,0 +1,15 @@
1
+ /* Opt-in decoration. Layers never consume pointer events or enter the accessibility tree. */
2
+ .blu-motion-ripple,.blu-motion-shimmer{position:absolute;inset:0;overflow:hidden;border-radius:inherit;pointer-events:none!important}
3
+ .blu-motion-ripple__wave{position:absolute;border-radius:50%;background:var(--blu-ripple-color,currentColor);pointer-events:none!important;opacity:0}
4
+ .blu-motion-choice-ripple{inset:-10px;border-radius:999px;--blu-ripple-color:var(--bluui-primary,#4f46e5)}
5
+ .blu-motion-shimmer__beam{position:absolute;inset:0;background:linear-gradient(100deg,transparent 20%,var(--blu-shimmer-color,#ffffffb8) 50%,transparent 80%);animation:blu-motion-shimmer var(--blu-shimmer-duration,1800ms) ease-in-out infinite}
6
+ @keyframes blu-motion-shimmer{from{transform:translateX(-100%)}to{transform:translateX(100%)}}
7
+ .blu-motion-shimmer[data-paused=true] .blu-motion-shimmer__beam{animation-play-state:paused}
8
+ .blu-motion-icon{display:block;width:24px;height:24px;overflow:visible;fill:none;stroke:currentColor;stroke-width:1.8;stroke-linecap:round;stroke-linejoin:round;pointer-events:none}
9
+ .blu-motion-emphasis{position:absolute;pointer-events:none!important;color:var(--blu-emphasis-color,currentColor)}
10
+ .blu-motion-emphasis--underline{inset:auto 0 0;height:2px;background:currentColor;transform-origin:left center;border-radius:2px}
11
+ .blu-motion-emphasis--border{inset:0;border-radius:inherit}
12
+ .blu-motion-emphasis--border svg{display:block;width:100%;height:100%;fill:none;stroke:currentColor;stroke-width:2px}
13
+ :is([data-blu-motion=reduced],[data-blu-motion=off],.blu-motion-off) :is(.blu-motion-shimmer__beam,.blu-motion-ripple__wave){display:none;animation:none!important}
14
+ @media(prefers-reduced-motion:reduce){.blu-motion-shimmer__beam,.blu-motion-ripple__wave{display:none;animation:none!important}}
15
+ @media(forced-colors:active){.blu-motion-shimmer__beam,.blu-motion-ripple__wave{display:none}.blu-motion-emphasis{color:Highlight}}
@@ -0,0 +1,92 @@
1
+ /* Generated by scripts/build-motion-css.mjs. Edit src/motion/effects.ts, then regenerate. */
2
+ .blu-motion-enter-fade, .blu-motion-fade-in { animation: blu-motion-enter-fade var(--blu-motion-duration) var(--blu-motion-ease) var(--blu-motion-delay) both; }
3
+ @keyframes blu-motion-enter-fade { 0% { opacity: 0; } 100% { opacity: 1; } }
4
+ .blu-motion-exit-fade, .blu-motion-fade-out { animation: blu-motion-exit-fade var(--blu-motion-duration) var(--blu-motion-ease) var(--blu-motion-delay) both; }
5
+ @keyframes blu-motion-exit-fade { 0% { opacity: 1; } 100% { opacity: 0; } }
6
+ .blu-motion-enter-slide-up, .blu-motion-slide-up { animation: blu-motion-enter-slide-up var(--blu-motion-duration) var(--blu-motion-ease) var(--blu-motion-delay) both; }
7
+ @keyframes blu-motion-enter-slide-up { 0% { opacity: 0; transform: translateY(var(--blu-motion-distance)); } 100% { opacity: 1; transform: none; } }
8
+ .blu-motion-exit-slide-up { animation: blu-motion-exit-slide-up var(--blu-motion-duration) var(--blu-motion-ease) var(--blu-motion-delay) both; }
9
+ @keyframes blu-motion-exit-slide-up { 0% { opacity: 1; transform: none; } 100% { opacity: 0; transform: translateY(calc(-1 * var(--blu-motion-distance))); } }
10
+ .blu-motion-enter-slide-down { animation: blu-motion-enter-slide-down var(--blu-motion-duration) var(--blu-motion-ease) var(--blu-motion-delay) both; }
11
+ @keyframes blu-motion-enter-slide-down { 0% { opacity: 0; transform: translateY(calc(-1 * var(--blu-motion-distance))); } 100% { opacity: 1; transform: none; } }
12
+ .blu-motion-exit-slide-down { animation: blu-motion-exit-slide-down var(--blu-motion-duration) var(--blu-motion-ease) var(--blu-motion-delay) both; }
13
+ @keyframes blu-motion-exit-slide-down { 0% { opacity: 1; transform: none; } 100% { opacity: 0; transform: translateY(var(--blu-motion-distance)); } }
14
+ .blu-motion-enter-slide-left { animation: blu-motion-enter-slide-left var(--blu-motion-duration) var(--blu-motion-ease) var(--blu-motion-delay) both; }
15
+ @keyframes blu-motion-enter-slide-left { 0% { opacity: 0; transform: translateX(var(--blu-motion-distance)); } 100% { opacity: 1; transform: none; } }
16
+ .blu-motion-exit-slide-left { animation: blu-motion-exit-slide-left var(--blu-motion-duration) var(--blu-motion-ease) var(--blu-motion-delay) both; }
17
+ @keyframes blu-motion-exit-slide-left { 0% { opacity: 1; transform: none; } 100% { opacity: 0; transform: translateX(calc(-1 * var(--blu-motion-distance))); } }
18
+ .blu-motion-enter-slide-right { animation: blu-motion-enter-slide-right var(--blu-motion-duration) var(--blu-motion-ease) var(--blu-motion-delay) both; }
19
+ @keyframes blu-motion-enter-slide-right { 0% { opacity: 0; transform: translateX(calc(-1 * var(--blu-motion-distance))); } 100% { opacity: 1; transform: none; } }
20
+ .blu-motion-exit-slide-right { animation: blu-motion-exit-slide-right var(--blu-motion-duration) var(--blu-motion-ease) var(--blu-motion-delay) both; }
21
+ @keyframes blu-motion-exit-slide-right { 0% { opacity: 1; transform: none; } 100% { opacity: 0; transform: translateX(var(--blu-motion-distance)); } }
22
+ .blu-motion-enter-zoom-in, .blu-motion-scale-in { animation: blu-motion-enter-zoom-in var(--blu-motion-duration) var(--blu-motion-ease) var(--blu-motion-delay) both; }
23
+ @keyframes blu-motion-enter-zoom-in { 0% { opacity: 0; transform: scale(.92); } 100% { opacity: 1; transform: none; } }
24
+ .blu-motion-exit-zoom-in { animation: blu-motion-exit-zoom-in var(--blu-motion-duration) var(--blu-motion-ease) var(--blu-motion-delay) both; }
25
+ @keyframes blu-motion-exit-zoom-in { 0% { opacity: 1; transform: none; } 100% { opacity: 0; transform: scale(1.08); } }
26
+ .blu-motion-enter-zoom-out { animation: blu-motion-enter-zoom-out var(--blu-motion-duration) var(--blu-motion-ease) var(--blu-motion-delay) both; }
27
+ @keyframes blu-motion-enter-zoom-out { 0% { opacity: 0; transform: scale(1.08); } 100% { opacity: 1; transform: none; } }
28
+ .blu-motion-exit-zoom-out { animation: blu-motion-exit-zoom-out var(--blu-motion-duration) var(--blu-motion-ease) var(--blu-motion-delay) both; }
29
+ @keyframes blu-motion-exit-zoom-out { 0% { opacity: 1; transform: none; } 100% { opacity: 0; transform: scale(.92); } }
30
+ .blu-motion-enter-rotate-left { animation: blu-motion-enter-rotate-left var(--blu-motion-duration) var(--blu-motion-ease) var(--blu-motion-delay) both; }
31
+ @keyframes blu-motion-enter-rotate-left { 0% { opacity: 0; transform: rotate(8deg) scale(.97); } 100% { opacity: 1; transform: none; } }
32
+ .blu-motion-exit-rotate-left { animation: blu-motion-exit-rotate-left var(--blu-motion-duration) var(--blu-motion-ease) var(--blu-motion-delay) both; }
33
+ @keyframes blu-motion-exit-rotate-left { 0% { opacity: 1; transform: none; } 100% { opacity: 0; transform: rotate(-8deg) scale(.97); } }
34
+ .blu-motion-enter-rotate-right { animation: blu-motion-enter-rotate-right var(--blu-motion-duration) var(--blu-motion-ease) var(--blu-motion-delay) both; }
35
+ @keyframes blu-motion-enter-rotate-right { 0% { opacity: 0; transform: rotate(-8deg) scale(.97); } 100% { opacity: 1; transform: none; } }
36
+ .blu-motion-exit-rotate-right { animation: blu-motion-exit-rotate-right var(--blu-motion-duration) var(--blu-motion-ease) var(--blu-motion-delay) both; }
37
+ @keyframes blu-motion-exit-rotate-right { 0% { opacity: 1; transform: none; } 100% { opacity: 0; transform: rotate(8deg) scale(.97); } }
38
+ .blu-motion-enter-flip-x { animation: blu-motion-enter-flip-x var(--blu-motion-duration) var(--blu-motion-ease) var(--blu-motion-delay) both; }
39
+ @keyframes blu-motion-enter-flip-x { 0% { opacity: 0; transform: perspective(600px) rotateX(30deg); } 100% { opacity: 1; transform: none; } }
40
+ .blu-motion-exit-flip-x { animation: blu-motion-exit-flip-x var(--blu-motion-duration) var(--blu-motion-ease) var(--blu-motion-delay) both; }
41
+ @keyframes blu-motion-exit-flip-x { 0% { opacity: 1; transform: none; } 100% { opacity: 0; transform: perspective(600px) rotateX(-30deg); } }
42
+ .blu-motion-enter-flip-y { animation: blu-motion-enter-flip-y var(--blu-motion-duration) var(--blu-motion-ease) var(--blu-motion-delay) both; }
43
+ @keyframes blu-motion-enter-flip-y { 0% { opacity: 0; transform: perspective(600px) rotateY(30deg); } 100% { opacity: 1; transform: none; } }
44
+ .blu-motion-exit-flip-y { animation: blu-motion-exit-flip-y var(--blu-motion-duration) var(--blu-motion-ease) var(--blu-motion-delay) both; }
45
+ @keyframes blu-motion-exit-flip-y { 0% { opacity: 1; transform: none; } 100% { opacity: 0; transform: perspective(600px) rotateY(-30deg); } }
46
+ .blu-motion-enter-pop { animation: blu-motion-enter-pop var(--blu-motion-duration) var(--blu-motion-ease) var(--blu-motion-delay) both; }
47
+ @keyframes blu-motion-enter-pop { 0% { opacity: 0; transform: scale(.9); } 72% { opacity: 1; transform: scale(1.025); } 100% { opacity: 1; transform: scale(1); } }
48
+ .blu-motion-exit-pop { animation: blu-motion-exit-pop var(--blu-motion-duration) var(--blu-motion-ease) var(--blu-motion-delay) both; }
49
+ @keyframes blu-motion-exit-pop { 0% { opacity: 1; transform: scale(1); } 25% { opacity: 1; transform: scale(1.025); } 100% { opacity: 0; transform: scale(.9); } }
50
+ .blu-motion-enter-bounce { animation: blu-motion-enter-bounce var(--blu-motion-duration) var(--blu-motion-ease) var(--blu-motion-delay) both; }
51
+ @keyframes blu-motion-enter-bounce { 0% { opacity: 0; transform: translateY(var(--blu-motion-distance)); } 72% { opacity: 1; transform: translateY(-3px); } 100% { opacity: 1; transform: translateY(0); } }
52
+ .blu-motion-exit-bounce { animation: blu-motion-exit-bounce var(--blu-motion-duration) var(--blu-motion-ease) var(--blu-motion-delay) both; }
53
+ @keyframes blu-motion-exit-bounce { 0% { opacity: 1; transform: translateY(0); } 25% { opacity: 1; transform: translateY(-3px); } 100% { opacity: 0; transform: translateY(var(--blu-motion-distance)); } }
54
+ .blu-motion-enter-wipe-up { animation: blu-motion-enter-wipe-up var(--blu-motion-duration) var(--blu-motion-ease) var(--blu-motion-delay) both; }
55
+ @keyframes blu-motion-enter-wipe-up { 0% { opacity: 0; clip-path: inset(100% 0 0 0); } 100% { opacity: 1; clip-path: inset(0 0 0 0); } }
56
+ .blu-motion-exit-wipe-up { animation: blu-motion-exit-wipe-up var(--blu-motion-duration) var(--blu-motion-ease) var(--blu-motion-delay) both; }
57
+ @keyframes blu-motion-exit-wipe-up { 0% { opacity: 1; clip-path: inset(0 0 0 0); } 100% { opacity: 0; clip-path: inset(0 0 100% 0); } }
58
+ .blu-motion-enter-wipe-down { animation: blu-motion-enter-wipe-down var(--blu-motion-duration) var(--blu-motion-ease) var(--blu-motion-delay) both; }
59
+ @keyframes blu-motion-enter-wipe-down { 0% { opacity: 0; clip-path: inset(0 0 100% 0); } 100% { opacity: 1; clip-path: inset(0 0 0 0); } }
60
+ .blu-motion-exit-wipe-down { animation: blu-motion-exit-wipe-down var(--blu-motion-duration) var(--blu-motion-ease) var(--blu-motion-delay) both; }
61
+ @keyframes blu-motion-exit-wipe-down { 0% { opacity: 1; clip-path: inset(0 0 0 0); } 100% { opacity: 0; clip-path: inset(100% 0 0 0); } }
62
+ .blu-motion-enter-wipe-left { animation: blu-motion-enter-wipe-left var(--blu-motion-duration) var(--blu-motion-ease) var(--blu-motion-delay) both; }
63
+ @keyframes blu-motion-enter-wipe-left { 0% { opacity: 0; clip-path: inset(0 0 0 100%); } 100% { opacity: 1; clip-path: inset(0 0 0 0); } }
64
+ .blu-motion-exit-wipe-left { animation: blu-motion-exit-wipe-left var(--blu-motion-duration) var(--blu-motion-ease) var(--blu-motion-delay) both; }
65
+ @keyframes blu-motion-exit-wipe-left { 0% { opacity: 1; clip-path: inset(0 0 0 0); } 100% { opacity: 0; clip-path: inset(0 100% 0 0); } }
66
+ .blu-motion-enter-wipe-right { animation: blu-motion-enter-wipe-right var(--blu-motion-duration) var(--blu-motion-ease) var(--blu-motion-delay) both; }
67
+ @keyframes blu-motion-enter-wipe-right { 0% { opacity: 0; clip-path: inset(0 100% 0 0); } 100% { opacity: 1; clip-path: inset(0 0 0 0); } }
68
+ .blu-motion-exit-wipe-right { animation: blu-motion-exit-wipe-right var(--blu-motion-duration) var(--blu-motion-ease) var(--blu-motion-delay) both; }
69
+ @keyframes blu-motion-exit-wipe-right { 0% { opacity: 1; clip-path: inset(0 0 0 0); } 100% { opacity: 0; clip-path: inset(0 0 0 100%); } }
70
+ .blu-motion-enter-circle-reveal { animation: blu-motion-enter-circle-reveal var(--blu-motion-duration) var(--blu-motion-ease) var(--blu-motion-delay) both; }
71
+ @keyframes blu-motion-enter-circle-reveal { 0% { opacity: 0; clip-path: circle(0% at 50% 50%); } 100% { opacity: 1; clip-path: circle(72% at 50% 50%); } }
72
+ .blu-motion-exit-circle-reveal { animation: blu-motion-exit-circle-reveal var(--blu-motion-duration) var(--blu-motion-ease) var(--blu-motion-delay) both; }
73
+ @keyframes blu-motion-exit-circle-reveal { 0% { opacity: 1; clip-path: circle(72% at 50% 50%); } 100% { opacity: 0; clip-path: circle(0% at 50% 50%); } }
74
+ .blu-motion-enter-blur-clear { animation: blu-motion-enter-blur-clear var(--blu-motion-duration) var(--blu-motion-ease) var(--blu-motion-delay) both; }
75
+ @keyframes blu-motion-enter-blur-clear { 0% { opacity: 0; filter: blur(6px); } 100% { opacity: 1; filter: blur(0px); } }
76
+ .blu-motion-exit-blur-clear { animation: blu-motion-exit-blur-clear var(--blu-motion-duration) var(--blu-motion-ease) var(--blu-motion-delay) both; }
77
+ @keyframes blu-motion-exit-blur-clear { 0% { opacity: 1; filter: blur(0px); } 100% { opacity: 0; filter: blur(6px); } }
78
+ .blu-motion-ease-standard { --blu-motion-ease: cubic-bezier(.2,.8,.2,1); }
79
+ .blu-motion-ease-in { --blu-motion-ease: cubic-bezier(.42,0,1,1); }
80
+ .blu-motion-ease-out { --blu-motion-ease: cubic-bezier(0,0,.58,1); }
81
+ .blu-motion-ease-in-out { --blu-motion-ease: cubic-bezier(.42,0,.58,1); }
82
+ .blu-motion-ease-linear { --blu-motion-ease: linear; }
83
+ .blu-motion-ease-spring { --blu-motion-ease: cubic-bezier(.34,1.25,.64,1); }
84
+ .blu-motion-origin-center { transform-origin: 50% 50%; }
85
+ .blu-motion-origin-top { transform-origin: 50% 0%; }
86
+ .blu-motion-origin-bottom { transform-origin: 50% 100%; }
87
+ .blu-motion-origin-left { transform-origin: 0% 50%; }
88
+ .blu-motion-origin-right { transform-origin: 100% 50%; }
89
+ .blu-motion-origin-top-left { transform-origin: 0% 0%; }
90
+ .blu-motion-origin-top-right { transform-origin: 100% 0%; }
91
+ .blu-motion-origin-bottom-left { transform-origin: 0% 100%; }
92
+ .blu-motion-origin-bottom-right { transform-origin: 100% 100%; }
@@ -0,0 +1,6 @@
1
+ /* Generated from src/motion/tokens.ts. */
2
+ :root {
3
+ --blu-motion-fast: 280ms;
4
+ --blu-motion-normal: 450ms;
5
+ --blu-motion-slow: 750ms;
6
+ }
@@ -0,0 +1,125 @@
1
+ /* Optional web-only 1.2 draft. Import after the existing theme/button styles. */
2
+ @import './motion-presets.css';
3
+ @import './motion-tokens.css';
4
+ @import './motion-optional.css';
5
+ :root {
6
+ --blu-motion-duration: var(--blu-motion-normal);
7
+ --blu-motion-delay: 0ms;
8
+ --blu-motion-distance: 12px;
9
+ --blu-motion-ease: cubic-bezier(.2, .8, .2, 1);
10
+ }
11
+ .blu-motion-fast { --blu-motion-duration: var(--blu-motion-fast); }
12
+ .blu-motion-slow { --blu-motion-duration: var(--blu-motion-slow); }
13
+ .blu-motion-delay-1 { --blu-motion-delay: 60ms; }
14
+ .blu-motion-delay-2 { --blu-motion-delay: 120ms; }
15
+ .blu-motion-delay-3 { --blu-motion-delay: 180ms; }
16
+ .blu-motion-spin { animation: blu-motion-spin 800ms linear infinite; }
17
+ .blu-motion-pulse { animation: blu-motion-pulse 900ms ease-in-out 3; }
18
+ .blu-motion-hover-lift { transition: transform var(--blu-motion-duration) var(--blu-motion-ease), box-shadow var(--blu-motion-duration) var(--blu-motion-ease); }
19
+ @media (hover: hover) { .blu-motion-hover-lift:hover { transform: translateY(calc(-1 * var(--blu-motion-lift-distance, 4px))); box-shadow: 0 8px 20px #28334b12; } }
20
+ .blu-motion-hover-lift:is(:focus-visible, [data-blu-lifted="true"]) { transform: translateY(calc(-1 * var(--blu-motion-lift-distance, 4px))); box-shadow: 0 8px 20px #28334b12; }
21
+ /* Requires one inner wrapper. Content remains in the DOM; the owner manages inert/hidden and focus. */
22
+ .blu-motion-expand { display: grid; grid-template-rows: 0fr; transition: grid-template-rows var(--blu-motion-duration) var(--blu-motion-ease); }
23
+ .blu-motion-expand[data-open="true"] { grid-template-rows: 1fr; }
24
+ .blu-motion-expand > * { min-height: 0; overflow: hidden; }
25
+ @keyframes blu-motion-spin { to { transform: rotate(360deg); } }
26
+ @keyframes blu-motion-pulse { 50% { opacity: .45; } }
27
+
28
+ .blu-motion-button.blu-button { display: inline-grid; gap: 0; transition-duration: var(--blu-motion-duration); }
29
+ .blu-motion-button__face, .blu-motion-button__reserve { grid-area: 1 / 1; padding-inline: 24px; }
30
+ .blu-motion-button__face { position: relative; display: inline-flex; align-items: center; justify-content: center; }
31
+ .blu-motion-button__reserve { visibility: hidden; pointer-events: none; }
32
+ .blu-motion-button .blu-button__spinner { position: absolute; inset-inline-start: 0; }
33
+ .blu-motion-button[data-spinner-position="Trailing"] .blu-button__spinner { inset-inline-start: auto; inset-inline-end: 0; }
34
+ .blu-motion-button .blu-button__spinner[hidden] { display: none; }
35
+ .blu-motion-button.blu-button--loading { pointer-events: auto; }
36
+ .blu-motion-sr-only { position: absolute; width: 1px; height: 1px; padding: 0; overflow: hidden; clip-path: inset(50%); white-space: nowrap; border: 0; }
37
+
38
+ .blu-loading { display: flex; align-items: center; gap: 10px; font: inherit; color: inherit; }
39
+ .blu-loading [hidden] { display: none !important; }
40
+ .blu-loading__visual { width: 20px; height: 20px; flex-shrink: 0; }
41
+ .blu-loading[data-variant="Spinner"] .blu-loading__visual { border: 2px solid currentColor; border-right-color: transparent; border-radius: 50%; animation: blu-motion-spin 800ms linear infinite; }
42
+ .blu-loading[data-variant="Spinner"] i { display: none; }
43
+ .blu-loading[data-variant="Dots"] .blu-loading__visual { display: flex; align-items: center; width: 32px; gap: 3px; }
44
+ .blu-loading[data-variant="Dots"] i { width: 7px; height: 7px; background: currentColor; border-radius: 50%; animation: blu-motion-pulse 900ms ease-in-out infinite; }
45
+ .blu-loading[data-variant="Dots"] i:nth-child(2) { animation-delay: 150ms; }
46
+ .blu-loading[data-variant="Dots"] i:nth-child(3) { animation-delay: 300ms; }
47
+ .blu-loading progress { width: 100%; height: 8px; accent-color: var(--bluui-primary, #5145e5); border: 0; border-radius: 8px; background: #e7e8f1; overflow: hidden; }
48
+ .blu-loading progress::-webkit-progress-bar { background: #e7e8f1; border-radius: 8px; }
49
+ .blu-loading progress::-webkit-progress-value { background: var(--bluui-primary, #5145e5); border-radius: 8px; }
50
+ .blu-loading progress::-moz-progress-bar { background: var(--bluui-primary, #5145e5); }
51
+ .blu-loading[data-variant="Bar"] { flex-direction: column; align-items: stretch; }
52
+ .blu-loading__bar { height: 8px; overflow: hidden; border-radius: 8px; background: #e7e8f1; }
53
+ .blu-loading__bar::after { content: ''; display: block; width: 35%; height: 100%; background: var(--bluui-primary, #5145e5); border-radius: inherit; animation: blu-motion-progress 1.2s ease-in-out infinite alternate; }
54
+ @keyframes blu-motion-progress { to { transform: translateX(185%); } }
55
+
56
+ /* Explicit off/reduced scopes and per-element opt-out; never hide useful content. */
57
+ :is([data-blu-motion="off"], [data-blu-motion="reduced"], .blu-motion-off),
58
+ :is([data-blu-motion="off"], [data-blu-motion="reduced"], .blu-motion-off) * {
59
+ --blu-motion-duration: 0ms;
60
+ --blu-motion-delay: 0ms;
61
+ }
62
+ :is([data-blu-motion="off"], [data-blu-motion="reduced"], .blu-motion-off) :is([class*="blu-motion-"], .blu-button__spinner, .blu-loading__visual, .blu-loading i),
63
+ :is([data-blu-motion="off"], [data-blu-motion="reduced"], .blu-motion-off)[class*="blu-motion-"],
64
+ .blu-motion-off { animation: none !important; transition: none !important; }
65
+ :is([data-blu-motion="off"], [data-blu-motion="reduced"], .blu-motion-off) .blu-loading__bar::after { animation: none !important; }
66
+ :is([data-blu-motion="off"], [data-blu-motion="reduced"], .blu-motion-off) .blu-motion-hover-lift:is(:hover, :focus-visible, [data-blu-lifted="true"]) { transform: none; }
67
+ @media (prefers-reduced-motion: reduce) {
68
+ [class*="blu-motion-"], .blu-button__spinner, .blu-loading__visual, .blu-loading i, .blu-loading__bar::after { animation: none !important; transition: none !important; }
69
+ .blu-motion-hover-lift:is(:hover, :focus-visible, [data-blu-lifted="true"]) { transform: none; }
70
+ }
71
+
72
+ /* Press state is finite and never changes a control's activation semantics. */
73
+ .blu-motion-press[data-blu-pressed="true"] { transform: scale(.97); }
74
+ .blu-motion-highlight { animation: blu-motion-highlight 1400ms ease-in-out 1; }
75
+ .blu-motion-nudge { animation: blu-motion-nudge 280ms var(--blu-motion-ease) 1; }
76
+ @keyframes blu-motion-highlight { 45% { box-shadow: 0 0 0 4px var(--blu-motion-highlight, #7770ed40); } }
77
+ @keyframes blu-motion-nudge { 25% { transform: translateX(-3px); } 60% { transform: translateX(3px); } }
78
+ :is([data-blu-motion="off"], [data-blu-motion="reduced"], .blu-motion-off) .blu-motion-press[data-blu-pressed="true"],
79
+ .blu-motion-press.blu-motion-off[data-blu-pressed="true"] { transform: none; }
80
+ @media (prefers-reduced-motion: reduce) { .blu-motion-press[data-blu-pressed="true"] { transform: none; } }
81
+
82
+ .blu-motion-photo-frame { position:relative;overflow:hidden;isolation:isolate; }
83
+ .blu-motion-photo-frame > img { display:block;width:100%;height:100%;object-fit:cover; }
84
+
85
+ /* A masked border, not an overlay on the content. The inner square rotates;
86
+ the outer mask and soft halo stay aligned to the original rounded box. */
87
+ .blu-motion-border-glow{position:absolute;inset:-1px;border-radius:inherit;pointer-events:none!important;z-index:1;filter:drop-shadow(0 0 4px var(--blu-glow-halo,#9b85ee80))}
88
+ .blu-motion-border-glow__rim{position:absolute;inset:0;padding:var(--blu-glow-width,2px);border-radius:inherit;overflow:hidden;pointer-events:none!important;-webkit-mask:linear-gradient(#000 0 0) content-box,linear-gradient(#000 0 0);-webkit-mask-composite:xor;mask:linear-gradient(#000 0 0) content-box,linear-gradient(#000 0 0);mask-composite:exclude}
89
+ .blu-motion-border-glow__beam{position:absolute;left:50%;top:50%;width:var(--blu-glow-size,1800px);height:var(--blu-glow-size,1800px);background:conic-gradient(transparent 0deg,transparent 240deg,var(--blu-glow-tail,#b5a4ef60) 265deg,var(--blu-glow-color,#8060dd) 295deg,var(--blu-glow-tip,#d7c6ff) 315deg,transparent 340deg);transform:translate(-50%,-50%) rotate(0turn);animation:blu-border-orbit var(--blu-glow-duration,12s) linear infinite}
90
+ .blu-motion-border-glow[data-paused=true] .blu-motion-border-glow__beam{animation-play-state:paused}
91
+ @keyframes blu-border-orbit{from{transform:translate(-50%,-50%) rotate(0turn)}to{transform:translate(-50%,-50%) rotate(1turn)}}
92
+ :is([data-blu-motion=reduced],[data-blu-motion=off],.blu-motion-off) .blu-motion-border-glow{filter:none}
93
+ :is([data-blu-motion=reduced],[data-blu-motion=off],.blu-motion-off) .blu-motion-border-glow__rim{background:var(--blu-glow-static,#b5a4ef80)}
94
+ :is([data-blu-motion=reduced],[data-blu-motion=off],.blu-motion-off) .blu-motion-border-glow__beam{animation:none!important;display:none}
95
+ @media(prefers-reduced-motion:reduce){.blu-motion-border-glow{filter:none}.blu-motion-border-glow__rim{background:var(--blu-glow-static,#b5a4ef80)}.blu-motion-border-glow__beam{animation:none!important;display:none}}
96
+ @media(forced-colors:active){.blu-motion-border-glow{display:none}}
97
+
98
+ /* Opt-in content adapters. Hover/focus lift composes with collection positioning. */
99
+ [data-blu-content-motion] .blu-profile-card{transition:translate var(--blu-component-motion-duration,450ms) var(--blu-component-motion-ease,ease-in-out)}
100
+ [data-blu-content-motion] .blu-profile-card:is(:hover,:focus-within){translate:0 -4px}
101
+ :is([data-blu-motion=reduced],[data-blu-motion=off],.blu-motion-off) [data-blu-content-motion] .blu-profile-card,
102
+ [data-blu-content-motion]:is([data-blu-motion=reduced],[data-blu-motion=off],.blu-motion-off) .blu-profile-card{translate:none;transition:none}
103
+ [data-blu-content-motion] :is(.blu-tree-select__check,.blu-video-player__heatmap){transition-duration:var(--blu-component-motion-duration,450ms);transition-timing-function:var(--blu-component-motion-ease,ease-in-out)}
104
+ :is([data-blu-motion=reduced],[data-blu-motion=off],.blu-motion-off) [data-blu-content-motion] :is(.blu-tree-select__check,.blu-video-player__heatmap){transition:none}
105
+ @media(prefers-reduced-motion:reduce){[data-blu-content-motion] .blu-profile-card{translate:none;transition:none}}
106
+
107
+ /* Optional ripple stays clipped to the selectable part of a chip. */
108
+ [data-blu-content-motion=chip] .blu-chip__select{border-radius:inherit}
109
+ [data-blu-content-motion=skeleton] .blu-skeleton--animated::after{animation-duration:1800ms;animation-iteration-count:2}
110
+ [data-blu-component-motion=statistic] .blu-statistic__loading{animation-duration:1800ms;animation-iteration-count:2}
111
+ :is([data-blu-motion=reduced],[data-blu-motion=off],.blu-motion-off) [data-blu-content-motion=skeleton] .blu-skeleton--animated::after{animation:none!important;display:none}
112
+ :is([data-blu-motion=reduced],[data-blu-motion=off],.blu-motion-off) [data-blu-component-motion=statistic] .blu-statistic__loading{animation:none!important}
113
+ @media(prefers-reduced-motion:reduce){[data-blu-content-motion=skeleton] .blu-skeleton--animated::after{animation:none!important;display:none}[data-blu-component-motion=statistic] .blu-statistic__loading{animation:none!important}}
114
+
115
+ /* Rich media hover must obey the same scope as its lightbox. */
116
+ [data-blu-content-motion=media-image] img{transition-duration:var(--blu-component-motion-duration,650ms);transition-timing-function:var(--blu-component-motion-ease,ease-in-out)}
117
+ :is([data-blu-motion=reduced],[data-blu-motion=off],.blu-motion-off) [data-blu-content-motion=media-image] img{transition:none!important;transform:none!important}
118
+ /* Legacy content CSS follows the opt-in controller as well as system preference. */
119
+ [data-blu-content-motion] .blu-button{transition-duration:var(--blu-component-motion-duration,280ms)}
120
+ :is([data-blu-motion=reduced],[data-blu-motion=off],.blu-motion-off) [data-blu-content-motion] *,
121
+ :is([data-blu-motion=reduced],[data-blu-motion=off],.blu-motion-off) [data-blu-content-motion] *::before,
122
+ :is([data-blu-motion=reduced],[data-blu-motion=off],.blu-motion-off) [data-blu-content-motion] *::after,
123
+ [data-blu-content-motion]:is([data-blu-motion=reduced],[data-blu-motion=off],.blu-motion-off) *,
124
+ [data-blu-content-motion]:is([data-blu-motion=reduced],[data-blu-motion=off],.blu-motion-off) *::before,
125
+ [data-blu-content-motion]:is([data-blu-motion=reduced],[data-blu-motion=off],.blu-motion-off) *::after{transition:none!important;animation:none!important}
@@ -67,3 +67,11 @@ The base shape remains visible when the animated overlay is removed. */
67
67
  -servicestudio-width: 180px;
68
68
  -servicestudio-height: 20px;
69
69
  }
70
+
71
+ /* The opt-in native motion adapter owns the shimmer. Avoid a second legacy
72
+ wave, including when the adapter is Reduced or Off. Removing the adapter
73
+ restores the existing IsAnimated behavior without changing its contract. */
74
+ .blu-skeleton[data-blu-native-motion="loading-skeleton"]::after {
75
+ animation: none !important;
76
+ display: none !important;
77
+ }
@@ -0,0 +1,35 @@
1
+ import test from 'node:test';
2
+ import assert from 'node:assert/strict';
3
+ import {build} from 'esbuild';
4
+ const built=await build({entryPoints:['src/motion/component-settings.ts'],bundle:true,format:'esm',platform:'node',write:false});
5
+ const {validateComponentMotion}=await import('data:text/javascript;base64,'+Buffer.from(built.outputFiles[0].text).toString('base64'));
6
+ test('component settings preserve explicit zero and omit undefined inheritance values',()=>{
7
+ assert.deepEqual(validateComponentMotion({duration:0,easing:undefined}),{duration:0});
8
+ assert.deepEqual(validateComponentMotion({mode:'inherit',duration:2000,easing:'ease-in-out'}),{mode:'inherit',duration:2000,easing:'ease-in-out'});
9
+ });
10
+ test('invalid settings cannot silently disable or produce unbounded component motion',()=>{
11
+ for(const invalid of [null,[],{duration:NaN},{duration:Infinity},{duration:-1},{duration:2001},{duration:'750'},{mode:'full'},{easing:'constructor'}])assert.throws(()=>validateComponentMotion(invalid));
12
+ });
13
+ test('validated settings are detached from later caller changes',()=>{
14
+ const source={duration:750,easing:'linear'}, settings=validateComponentMotion(source);source.duration=1000;
15
+ assert.equal(settings.duration,750);assert.equal(source.duration,1000);
16
+ });
17
+ const grouped=await build({entryPoints:['src/motion/settings-group.ts'],bundle:true,format:'esm',platform:'node',write:false});
18
+ const {createMotionSettingsGroup}=await import('data:text/javascript;base64,'+Buffer.from(grouped.outputFiles[0].text).toString('base64'));
19
+ test('collection configuration preserves instance overrides, resets inheritance and detaches disposed instances',()=>{
20
+ const group=createMotionSettingsGroup({duration:750,mode:'reduced'});let a,b;
21
+ const first=group.bind(value=>{a=value;}),second=group.bind(value=>{b=value;});
22
+ first.setMotion({duration:1200,mode:'off'});second.setMotion();
23
+ group.configure({duration:450,easing:'linear'});
24
+ assert.deepEqual(a,{duration:1200,easing:'linear',mode:'off'});assert.deepEqual(b,{duration:450,easing:'linear'});
25
+ first.setMotion({mode:'inherit'});assert.deepEqual(a,b);
26
+ second.destroy();group.configure({duration:1000});assert.equal(b.duration,450);assert.equal(a.duration,1000);
27
+ assert.throws(()=>second.setMotion());
28
+ });
29
+ test('invalid collection defaults and overrides are rejected before changing any member',()=>{
30
+ const group=createMotionSettingsGroup({duration:750});let current;
31
+ const member=group.bind(value=>{current=value;});member.setMotion();
32
+ assert.throws(()=>group.configure({duration:NaN}));assert.throws(()=>member.setMotion({mode:'full'}));
33
+ assert.deepEqual(current,{duration:750});assert.deepEqual(group.settings,{duration:750});
34
+ const detached=group.settings;detached.duration=1000;assert.equal(group.settings.duration,750);
35
+ });