@blukavantui/community 1.0.1 → 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 (304) hide show
  1. package/README.md +11 -5
  2. package/dist/build-audit.json +234 -44
  3. package/dist/checkbox.css +1 -0
  4. package/dist/checkbox.d.ts +2 -0
  5. package/dist/checkbox.js +1 -0
  6. package/dist/chunk-2BSBXDOT.js +1 -0
  7. package/dist/chunk-4LHIQROI.js +0 -0
  8. package/dist/chunk-5ENZVZGF.js +0 -0
  9. package/dist/chunk-6T677NSC.js +1 -0
  10. package/dist/chunk-7CEOCZ5L.js +0 -0
  11. package/dist/chunk-7DXFNK7C.js +1 -0
  12. package/dist/chunk-AL2ETLJV.js +1 -0
  13. package/dist/chunk-BN3PDE5G.js +0 -0
  14. package/dist/chunk-CNIIMGT3.js +0 -0
  15. package/dist/chunk-CSTCEBEK.js +1 -0
  16. package/dist/chunk-DRZM2AL3.js +0 -0
  17. package/dist/chunk-EOKNLCNS.js +1 -0
  18. package/dist/chunk-EPPETNK6.js +1 -0
  19. package/dist/chunk-EPPSBHZR.js +1 -0
  20. package/dist/chunk-EPZGLHPD.js +0 -0
  21. package/dist/chunk-FNX33PDD.js +0 -0
  22. package/dist/{chunk-NTFHNL7T.js → chunk-G6OW4OZM.js} +1 -1
  23. package/dist/chunk-GS67ZBLI.js +1 -0
  24. package/dist/chunk-HO6CYWED.js +0 -0
  25. package/dist/{chunk-6EHKOVZK.js → chunk-HVR2UDFX.js} +1 -1
  26. package/dist/chunk-IJ62ILKE.js +0 -0
  27. package/dist/chunk-IKYBFMHI.js +1 -0
  28. package/dist/chunk-IO22GWON.js +0 -0
  29. package/dist/chunk-KVOA55NE.js +0 -0
  30. package/dist/{chunk-MZBB5ROJ.js → chunk-LF2HELEZ.js} +1 -1
  31. package/dist/chunk-MYVLBALF.js +1 -0
  32. package/dist/{chunk-UEOCR2C3.js → chunk-OLDA73XG.js} +2 -2
  33. package/dist/chunk-RWBLICYX.js +0 -0
  34. package/dist/chunk-RYD6RHRX.js +0 -0
  35. package/dist/chunk-T45FJNEQ.js +1 -0
  36. package/dist/chunk-TZSX4OTV.js +1 -0
  37. package/dist/chunk-UN2SLVFG.js +1 -0
  38. package/dist/{chunk-GK3INPAZ.js → chunk-V2YPIC6Z.js} +1 -1
  39. package/dist/chunk-V4ZQLQ7L.js +0 -0
  40. package/dist/chunk-VAM3FPCS.js +0 -0
  41. package/dist/chunk-VJVFHL6P.js +0 -0
  42. package/dist/chunk-VSTKXZYY.js +0 -0
  43. package/dist/chunk-W3H7XTA7.js +1 -0
  44. package/dist/chunk-WG4US4JH.js +1 -0
  45. package/dist/chunk-WGSTNXAQ.js +0 -0
  46. package/dist/chunk-WJHCVECU.js +1 -0
  47. package/dist/chunk-XVMJG26X.js +1 -0
  48. package/dist/chunk-Y4AXWTAD.js +0 -0
  49. package/dist/chunk-YCLR3IE3.js +0 -0
  50. package/dist/chunk-YLFXSHKZ.js +0 -0
  51. package/dist/chunk-ZDGZGAYG.js +0 -0
  52. package/dist/columns2.css +1 -0
  53. package/dist/columns2.d.ts +2 -0
  54. package/dist/columns2.js +1 -0
  55. package/dist/columns3.css +1 -0
  56. package/dist/columns3.d.ts +2 -0
  57. package/dist/columns3.js +1 -0
  58. package/dist/columns4.css +1 -0
  59. package/dist/columns4.d.ts +2 -0
  60. package/dist/columns4.js +1 -0
  61. package/dist/command-palette.js +1 -1
  62. package/dist/component-motion.css +39 -0
  63. package/dist/component-motion.d.ts +45 -0
  64. package/dist/component-motion.js +1 -0
  65. package/dist/content.js +1 -1
  66. package/dist/core.css +8 -0
  67. package/dist/core.js +1 -1
  68. package/dist/data-motion.css +4 -0
  69. package/dist/data-motion.d.ts +23 -0
  70. package/dist/data-motion.js +1 -0
  71. package/dist/data.css +63 -0
  72. package/dist/data.d.ts +1 -0
  73. package/dist/data.js +1 -1
  74. package/dist/date-picker.css +5 -5
  75. package/dist/date-picker.js +1 -1
  76. package/dist/date-range-picker.css +5 -5
  77. package/dist/date-range-picker.js +1 -1
  78. package/dist/date-time-picker.css +5 -5
  79. package/dist/date-time-picker.js +1 -1
  80. package/dist/draft-components.json +77 -0
  81. package/dist/drawer.js +1 -1
  82. package/dist/essentials.css +8 -0
  83. package/dist/feedback.js +1 -1
  84. package/dist/form-field.css +1 -0
  85. package/dist/form-field.d.ts +2 -0
  86. package/dist/form-field.js +1 -0
  87. package/dist/form-layout/choice.d.ts +15 -0
  88. package/dist/form-layout/layout.d.ts +12 -0
  89. package/dist/form-layout/select.d.ts +3 -0
  90. package/dist/form-layout/shared.d.ts +21 -0
  91. package/dist/form-layout/table.d.ts +20 -0
  92. package/dist/form-layout/text.d.ts +3 -0
  93. package/dist/form-layout/types.d.ts +151 -0
  94. package/dist/form-layout.css +62 -0
  95. package/dist/form-layout.d.ts +12 -0
  96. package/dist/form-layout.js +1 -0
  97. package/dist/index.d.ts +1 -0
  98. package/dist/index.js +1 -1
  99. package/dist/inputs.css +83 -20
  100. package/dist/inputs.d.ts +8 -0
  101. package/dist/inputs.js +1 -1
  102. package/dist/layout.css +63 -0
  103. package/dist/layout.d.ts +3 -0
  104. package/dist/layout.js +1 -1
  105. package/dist/modal.js +1 -1
  106. package/dist/motion/action-feedback.d.ts +14 -0
  107. package/dist/motion/border-glow.d.ts +12 -0
  108. package/dist/motion/button.d.ts +11 -0
  109. package/dist/motion/component-settings.d.ts +51 -0
  110. package/dist/motion/controller.d.ts +33 -0
  111. package/dist/motion/effects.d.ts +30 -0
  112. package/dist/motion/feedback.d.ts +6 -0
  113. package/dist/motion/loading.d.ts +9 -0
  114. package/dist/motion/optional.d.ts +35 -0
  115. package/dist/motion/photo.d.ts +7 -0
  116. package/dist/motion/presence.d.ts +12 -0
  117. package/dist/motion/scene.d.ts +10 -0
  118. package/dist/motion/screen.d.ts +6 -0
  119. package/dist/motion/scroll-reveal.d.ts +13 -0
  120. package/dist/motion/settings-group.d.ts +14 -0
  121. package/dist/motion/stagger.d.ts +12 -0
  122. package/dist/motion/structure.d.ts +18 -0
  123. package/dist/motion/tabs.d.ts +19 -0
  124. package/dist/motion/tokens.d.ts +11 -0
  125. package/dist/motion/value.d.ts +21 -0
  126. package/dist/motion/view-transition.d.ts +14 -0
  127. package/dist/motion-enhancers.d.ts +12 -0
  128. package/dist/motion-enhancers.js +1 -0
  129. package/dist/motion-settings.d.ts +3 -0
  130. package/dist/motion-settings.js +1 -0
  131. package/dist/motion.css +1076 -0
  132. package/dist/motion.d.ts +20 -0
  133. package/dist/motion.js +1 -0
  134. package/dist/native/date-picker.js +1 -1
  135. package/dist/native/date-range-picker.js +1 -1
  136. package/dist/native/date-time-picker.js +1 -1
  137. package/dist/native/form-layout.js +1 -0
  138. package/dist/native/time-picker.js +1 -1
  139. package/dist/native-components/form-layout.d.ts +1 -0
  140. package/dist/native-motion-components.d.ts +3 -0
  141. package/dist/native-motion-content.d.ts +1 -0
  142. package/dist/native-motion-foundations.d.ts +3 -0
  143. package/dist/native-motion-layout.d.ts +3 -0
  144. package/dist/native-motion-pilot.d.ts +34 -0
  145. package/dist/native-motion.d.ts +71 -0
  146. package/dist/notification.js +1 -1
  147. package/dist/pagination.js +1 -1
  148. package/dist/radio-button.css +1 -0
  149. package/dist/radio-button.d.ts +2 -0
  150. package/dist/radio-button.js +1 -0
  151. package/dist/radio-group.css +1 -0
  152. package/dist/radio-group.d.ts +2 -0
  153. package/dist/radio-group.js +1 -0
  154. package/dist/select.css +1 -0
  155. package/dist/select.d.ts +2 -0
  156. package/dist/select.js +1 -0
  157. package/dist/sidebar.js +1 -1
  158. package/dist/sizes.json +416 -112
  159. package/dist/skeleton.css +8 -0
  160. package/dist/styles.css +1213 -20
  161. package/dist/switch.css +1 -0
  162. package/dist/switch.d.ts +2 -0
  163. package/dist/switch.js +1 -0
  164. package/dist/table.css +1 -0
  165. package/dist/table.d.ts +2 -0
  166. package/dist/table.js +1 -0
  167. package/dist/text-area.css +1 -0
  168. package/dist/text-area.d.ts +2 -0
  169. package/dist/text-area.js +1 -0
  170. package/dist/text-input.css +1 -0
  171. package/dist/text-input.d.ts +2 -0
  172. package/dist/text-input.js +1 -0
  173. package/dist/time-picker.css +5 -5
  174. package/dist/time-picker.js +1 -1
  175. package/dist/tree-view.js +1 -1
  176. package/dist/web-motion/chip-ripple.d.ts +6 -0
  177. package/dist/web-motion/choice-ripple.d.ts +6 -0
  178. package/dist/web-motion/content.d.ts +9 -0
  179. package/dist/web-motion/data-rows.d.ts +12 -0
  180. package/dist/web-motion/fields.d.ts +6 -0
  181. package/dist/web-motion/navigation.d.ts +6 -0
  182. package/dist/web-motion/notifications.d.ts +6 -0
  183. package/dist/web-motion/select-ripple.d.ts +6 -0
  184. package/dist/web-motion/statistic.d.ts +13 -0
  185. package/dist/web-motion/surfaces.d.ts +10 -0
  186. package/dist/web-motion/tree.d.ts +6 -0
  187. package/docs/FORM-LAYOUT-ESSENTIALS.md +122 -0
  188. package/docs/PUBLISHING.md +129 -25
  189. package/draft-components.json +77 -0
  190. package/edition.json +44 -7
  191. package/npm-shrinkwrap.json +2 -2
  192. package/package.json +102 -10
  193. package/scripts/build-motion-css.mjs +36 -0
  194. package/scripts/build-motion-styles.mjs +29 -0
  195. package/scripts/build.mjs +20 -8
  196. package/scripts/motion-entries.mjs +2 -0
  197. package/src/checkbox.ts +2 -0
  198. package/src/columns2.ts +2 -0
  199. package/src/columns3.ts +2 -0
  200. package/src/columns4.ts +2 -0
  201. package/src/component-motion.ts +101 -0
  202. package/src/data-motion.ts +60 -0
  203. package/src/data.ts +1 -0
  204. package/src/form-field.ts +2 -0
  205. package/src/form-layout/choice.ts +82 -0
  206. package/src/form-layout/layout.ts +40 -0
  207. package/src/form-layout/select.ts +97 -0
  208. package/src/form-layout/shared.ts +57 -0
  209. package/src/form-layout/table.ts +86 -0
  210. package/src/form-layout/text.ts +73 -0
  211. package/src/form-layout/types.ts +59 -0
  212. package/src/form-layout.ts +12 -0
  213. package/src/index.ts +2 -0
  214. package/src/inputs.ts +8 -0
  215. package/src/layout.ts +3 -0
  216. package/src/motion/action-feedback.ts +38 -0
  217. package/src/motion/border-glow.ts +41 -0
  218. package/src/motion/button.ts +56 -0
  219. package/src/motion/component-settings.ts +147 -0
  220. package/src/motion/controller.ts +191 -0
  221. package/src/motion/effects.ts +75 -0
  222. package/src/motion/feedback.ts +41 -0
  223. package/src/motion/loading.ts +40 -0
  224. package/src/motion/optional.ts +123 -0
  225. package/src/motion/photo.ts +19 -0
  226. package/src/motion/presence.ts +44 -0
  227. package/src/motion/scene.ts +103 -0
  228. package/src/motion/screen.ts +15 -0
  229. package/src/motion/scroll-reveal.ts +49 -0
  230. package/src/motion/settings-group.ts +28 -0
  231. package/src/motion/stagger.ts +26 -0
  232. package/src/motion/structure.ts +75 -0
  233. package/src/motion/tabs.ts +105 -0
  234. package/src/motion/tokens.ts +2 -0
  235. package/src/motion/value.ts +61 -0
  236. package/src/motion/view-transition.ts +57 -0
  237. package/src/motion-enhancers.ts +12 -0
  238. package/src/motion-settings.ts +3 -0
  239. package/src/motion.ts +20 -0
  240. package/src/native-components/form-layout.ts +21 -0
  241. package/src/native-motion-components.ts +76 -0
  242. package/src/native-motion-content.ts +10 -0
  243. package/src/native-motion-foundations.ts +56 -0
  244. package/src/native-motion-layout.ts +35 -0
  245. package/src/native-motion-pilot.ts +151 -0
  246. package/src/native-motion.ts +188 -0
  247. package/src/radio-button.ts +2 -0
  248. package/src/radio-group.ts +2 -0
  249. package/src/runtime/date-picker.js +21 -12
  250. package/src/runtime/date-range-picker.js +21 -12
  251. package/src/runtime/date-time-picker.js +21 -12
  252. package/src/runtime/time-picker.js +21 -12
  253. package/src/select.ts +2 -0
  254. package/src/switch.ts +2 -0
  255. package/src/table.ts +2 -0
  256. package/src/text-area.ts +2 -0
  257. package/src/text-input.ts +2 -0
  258. package/src/types.ts +0 -12
  259. package/src/web-motion/chip-ripple.ts +31 -0
  260. package/src/web-motion/choice-ripple.ts +30 -0
  261. package/src/web-motion/content.ts +74 -0
  262. package/src/web-motion/data-rows.ts +106 -0
  263. package/src/web-motion/fields.ts +52 -0
  264. package/src/web-motion/navigation.ts +62 -0
  265. package/src/web-motion/notifications.ts +67 -0
  266. package/src/web-motion/select-ripple.ts +23 -0
  267. package/src/web-motion/statistic.ts +23 -0
  268. package/src/web-motion/surfaces.ts +137 -0
  269. package/src/web-motion/tree.ts +44 -0
  270. package/styles/checkbox.css +1 -0
  271. package/styles/columns2.css +1 -0
  272. package/styles/columns3.css +1 -0
  273. package/styles/columns4.css +1 -0
  274. package/styles/component-motion.css +39 -0
  275. package/styles/data-motion.css +4 -0
  276. package/styles/date-picker.css +5 -5
  277. package/styles/date-range-picker.css +5 -5
  278. package/styles/date-time-picker.css +5 -5
  279. package/styles/form-field.css +1 -0
  280. package/styles/form-layout.css +62 -0
  281. package/styles/motion-optional.css +15 -0
  282. package/styles/motion-presets.css +92 -0
  283. package/styles/motion-tokens.css +6 -0
  284. package/styles/motion.css +125 -0
  285. package/styles/radio-button.css +1 -0
  286. package/styles/radio-group.css +1 -0
  287. package/styles/select.css +1 -0
  288. package/styles/skeleton.css +8 -0
  289. package/styles/switch.css +1 -0
  290. package/styles/table.css +1 -0
  291. package/styles/text-area.css +1 -0
  292. package/styles/text-input.css +1 -0
  293. package/styles/time-picker.css +5 -5
  294. package/test/component-motion-settings.test.mjs +35 -0
  295. package/test/content-overlay-motion.test.mjs +36 -0
  296. package/test/form-layout.test.mjs +28 -0
  297. package/test/isolation.test.mjs +2 -2
  298. package/test/motion.test.mjs +215 -0
  299. package/dist/chunk-2TCNAQC6.js +0 -1
  300. package/dist/chunk-CTLZIWMX.js +0 -1
  301. package/dist/chunk-MHHJZYXW.js +0 -1
  302. package/dist/chunk-XDYBNO4P.js +0 -1
  303. /package/dist/{chunk-H7ETPJVW.js → chunk-3PODK22R.js} +0 -0
  304. /package/dist/{chunk-SIU7DSOA.js → chunk-KBWB4OIF.js} +0 -0
@@ -0,0 +1,61 @@
1
+ import type { MotionController, MotionResult } from './controller.js';
2
+ import { preserveStyles, type TransitionOptions } from './structure.js';
3
+
4
+ export function finiteValue(value: number) { if (!Number.isFinite(value)) throw new Error('Provide a finite value.'); return value; }
5
+ const percent = (value: number) => Math.max(0, Math.min(100, finiteValue(value)));
6
+
7
+ /** Visual fill only. Owner supplies the real value and progress semantics. */
8
+ export function createProgressTransition(fill: HTMLElement, motion: MotionController, initial = 0) {
9
+ const restore = preserveStyles(fill, ['transform', 'transform-origin']);
10
+ let value = percent(initial), revision = 0, destroyed = false;
11
+ fill.style.transformOrigin = 'left center'; fill.style.transform = `scaleX(${value / 100})`;
12
+ return {
13
+ async set(next: number, options: TransitionOptions = {}): Promise<MotionResult> {
14
+ next = percent(next); if (destroyed) return 'cancelled';
15
+ const own = ++revision, from = fill.ownerDocument.defaultView!.getComputedStyle(fill).transform;
16
+ motion.cancel(fill); value = next; fill.style.transform = `scaleX(${value / 100})`;
17
+ await motion.animate(fill, [{ transform: from }, { transform: fill.style.transform }], options);
18
+ return !destroyed && own === revision ? 'finished' : 'cancelled';
19
+ },
20
+ destroy() { destroyed = true; ++revision; motion.cancel(fill); restore(); },
21
+ };
22
+ }
23
+
24
+ /** SVG drawing or measured progress, normalized to 0–100 without guessing path length. */
25
+ export function createStrokeTransition(path: SVGGeometryElement, motion: MotionController, initial = 0) {
26
+ const oldLength = path.getAttribute('pathLength'), restore = preserveStyles(path, ['stroke-dasharray', 'stroke-dashoffset']);
27
+ let revision = 0, destroyed = false;
28
+ const start = percent(initial);
29
+ path.setAttribute('pathLength', '1'); path.style.strokeDasharray = '1'; path.style.strokeDashoffset = String(1 - start / 100);
30
+ return {
31
+ async set(next: number, options: TransitionOptions = {}): Promise<MotionResult> {
32
+ next = percent(next); if (destroyed) return 'cancelled';
33
+ const own = ++revision, from = path.ownerDocument.defaultView!.getComputedStyle(path).strokeDashoffset;
34
+ motion.cancel(path); path.style.strokeDashoffset = String(1 - next / 100);
35
+ await motion.animate(path, [{ strokeDashoffset: from }, { strokeDashoffset: path.style.strokeDashoffset }], options);
36
+ return !destroyed && own === revision ? 'finished' : 'cancelled';
37
+ },
38
+ destroy() { destroyed = true; ++revision; motion.cancel(path); restore(); if (oldLength === null) path.removeAttribute('pathLength'); else path.setAttribute('pathLength', oldLength); },
39
+ };
40
+ }
41
+
42
+ /** One stable accessible value. Intermediate visual numbers are never live announcements. */
43
+ export function createNumberTransition(element: HTMLElement, motion: MotionController, options: { value: number; format?: (value: number) => string }) {
44
+ let current = finiteValue(options.value), revision = 0, destroyed = false;
45
+ const original = Array.from(element.childNodes), doc = element.ownerDocument;
46
+ const visual = doc.createElement('span'), accessible = doc.createElement('span');
47
+ visual.setAttribute('aria-hidden', 'true'); accessible.className = 'blu-motion-sr-only';
48
+ const format = options.format || ((value: number) => Math.round(value).toLocaleString());
49
+ visual.textContent = accessible.textContent = format(current); element.replaceChildren(visual, accessible);
50
+ return {
51
+ async set(next: number, timing: TransitionOptions = {}): Promise<MotionResult> {
52
+ finiteValue(next); if (destroyed) return 'cancelled';
53
+ const text = format(next), own = ++revision;
54
+ accessible.textContent = text;
55
+ await motion.tween(element, current, next, value => { current = value; visual.textContent = format(value); }, timing);
56
+ if (destroyed || own !== revision) return 'cancelled';
57
+ current = next; visual.textContent = text; return 'finished';
58
+ },
59
+ destroy() { if (destroyed) return; destroyed = true; ++revision; motion.cancel(element); element.replaceChildren(...original); },
60
+ };
61
+ }
@@ -0,0 +1,57 @@
1
+ import type { MotionController, MotionEffect, MotionResult } from './controller.js';
2
+
3
+ export interface ViewTransitionOptions {
4
+ effect?: MotionEffect | 'instant';
5
+ /** Container scroll only. Routing, browser history and document scroll stay app-owned. */
6
+ scroll?: 'preserve' | 'top';
7
+ /** Entrance duration; exit uses 280 ms. */
8
+ duration?: number;
9
+ }
10
+
11
+ /** Replace one workspace/page outlet. Latest request wins; no document/router interception. */
12
+ export function createViewTransition(host: HTMLElement, motion: MotionController) {
13
+ let revision = 0, destroyed = false;
14
+ const originalBusy = host.getAttribute('aria-busy');
15
+ const restoreBusy = () => originalBusy === null ? host.removeAttribute('aria-busy') : host.setAttribute('aria-busy', originalBusy);
16
+ return {
17
+ async show(next: HTMLElement, options: ViewTransitionOptions = {}): Promise<MotionResult> {
18
+ if (destroyed) return 'cancelled';
19
+ if (next.contains(host) || next === host) throw new Error('A view cannot contain its outlet.');
20
+ const ownRevision = ++revision;
21
+ const opacity = host.ownerDocument.defaultView!.getComputedStyle(host).opacity;
22
+ motion.cancel(host);
23
+ host.setAttribute('aria-busy', 'true');
24
+ const current = () => !destroyed && ownRevision === revision;
25
+ try {
26
+ if (options.effect !== 'instant') await motion.animate(host, [{ opacity }, { opacity: 0 }], { phase: 'exit', duration: 280, easing: 'ease-in-out' }, 'fade');
27
+ if (!current()) return 'cancelled';
28
+ // Read focus immediately before replacement; do not steal focus if the user moved away.
29
+ const active = host.ownerDocument.activeElement as HTMLElement | null;
30
+ const hadFocus = !!active && host.contains(active);
31
+ const focusKey = active?.getAttribute('data-focus-key');
32
+ const scrollTop = host.scrollTop, scrollLeft = host.scrollLeft;
33
+ host.replaceChildren(next);
34
+ host.scrollTop = options.scroll === 'top' ? 0 : scrollTop;
35
+ host.scrollLeft = options.scroll === 'top' ? 0 : scrollLeft;
36
+ if (hadFocus) {
37
+ const keyed = focusKey ? Array.from(next.querySelectorAll<HTMLElement>('[data-focus-key]')).find(el => el.getAttribute('data-focus-key') === focusKey) : undefined;
38
+ const target = keyed || next.querySelector<HTMLElement>('[data-view-heading]') || next;
39
+ if (!target.hasAttribute('tabindex') && !target.matches('button,input,select,textarea,a[href]')) target.tabIndex = -1;
40
+ target.focus({ preventScroll: true });
41
+ }
42
+ if (options.effect !== 'instant') await motion.play(host, options.effect || 'fade', { duration: options.duration ?? 450, easing: 'ease-in-out' });
43
+ return current() ? 'finished' : 'cancelled';
44
+ } finally {
45
+ if (current()) restoreBusy();
46
+ }
47
+ },
48
+ cancel() { ++revision; motion.cancel(host); restoreBusy(); },
49
+ destroy() {
50
+ if (destroyed) return;
51
+ destroyed = true;
52
+ ++revision;
53
+ motion.cancel(host);
54
+ restoreBusy();
55
+ },
56
+ };
57
+ }
@@ -0,0 +1,12 @@
1
+ /** DOM-only adapters. Attach after the component mounts; dispose before its root. */
2
+ export * from './web-motion/data-rows.js';
3
+ export * from './web-motion/content.js';
4
+ export * from './web-motion/statistic.js';
5
+ export * from './web-motion/tree.js';
6
+ export * from './web-motion/navigation.js';
7
+ export * from './web-motion/notifications.js';
8
+ export * from './web-motion/fields.js';
9
+ export * from './web-motion/surfaces.js';
10
+ export * from './web-motion/chip-ripple.js';
11
+ export * from './web-motion/choice-ripple.js';
12
+ export * from './web-motion/select-ripple.js';
@@ -0,0 +1,3 @@
1
+ /** Shared settings contract for optional component, data, chart and workflow adapters. */
2
+ export * from './motion/component-settings.js';
3
+ export * from './motion/settings-group.js';
package/src/motion.ts ADDED
@@ -0,0 +1,20 @@
1
+ /** Experimental web-only 1.2 entry. Not exported by the published 1.1.0 package. */
2
+ export * from './motion/controller.js';
3
+ export * from './motion/view-transition.js';
4
+ export * from './motion/button.js';
5
+ export * from './motion/loading.js';
6
+ export * from './motion/effects.js';
7
+ export * from './motion/presence.js';
8
+ export * from './motion/stagger.js';
9
+ export * from './motion/scroll-reveal.js';
10
+ export * from './motion/structure.js';
11
+ export * from './motion/feedback.js';
12
+ export * from './motion/value.js';
13
+ export * from './motion/scene.js';
14
+ export * from './motion/photo.js';
15
+ export * from './motion/border-glow.js';
16
+ export * from './motion/tabs.js';
17
+ export * from './motion/action-feedback.js';
18
+ export * from './motion/screen.js';
19
+ export * from './motion/tokens.js';
20
+ export * from './motion/optional.js';
@@ -0,0 +1,21 @@
1
+ import {createCheckbox} from '../checkbox.js';
2
+ import {createTextInput} from '../text-input.js';
3
+ import {createTextArea} from '../text-area.js';
4
+ import {createRadioButton} from '../radio-button.js';
5
+ import {createRadioGroup} from '../radio-group.js';
6
+ import {createSwitch} from '../switch.js';
7
+ import {createSelect} from '../select.js';
8
+ import {createTable} from '../table.js';
9
+ import {install} from '../native-base.js';
10
+ const wrap = (factory:any) => (host:HTMLElement,p:any,events:any) => { const instance=factory(host,json(p),events); return {...instance,update:(patch:any)=>instance.update(json(patch))}; };
11
+ const json = (p:Record<string,unknown>) => { const result={...p}; for(const key of ['Options','Rows','Columns','Actions','Leading','Trailing']) if(key+'Json' in result) { result[key]=JSON.parse(String(result[key+'Json'] || (['Leading','Trailing'].includes(key)?'null':'[]'))); delete result[key+'Json']; } return result; };
12
+ install({
13
+ Checkbox:wrap(createCheckbox),
14
+ TextInput:wrap(createTextInput),
15
+ TextArea:wrap(createTextArea),
16
+ RadioButton:wrap(createRadioButton),
17
+ RadioGroup:wrap(createRadioGroup),
18
+ Switch:wrap(createSwitch),
19
+ Select:wrap(createSelect),
20
+ Table:wrap(createTable)
21
+ });
@@ -0,0 +1,76 @@
1
+ /** Native DOM enhancement only. Existing Blocks own all content and application events. */
2
+ import {nativeLayoutAdapter} from './native-motion-layout.js';
3
+ import type {MotionController} from './motion/controller.js';
4
+ import type {Adapter,NativeMotionOptions} from './native-motion.js';
5
+ import {observeSurface} from './web-motion/surfaces.js';
6
+ import {enhanceField} from './web-motion/fields.js';
7
+ import {enhanceChoiceRipple} from './web-motion/choice-ripple.js';
8
+ import {enhanceChipRipple} from './web-motion/chip-ripple.js';
9
+ import {enhanceSelectRipple} from './web-motion/select-ripple.js';
10
+ import {enhanceContentMotion,type ContentMotionKind} from './web-motion/content.js';
11
+ import {enhanceTree} from './web-motion/tree.js';
12
+ import {enhanceNavigation} from './web-motion/navigation.js';
13
+ import {enhanceNotifications} from './web-motion/notifications.js';
14
+ import {enhanceDataRows,type MotionRow} from './web-motion/data-rows.js';
15
+ import {enhanceStatistic} from './web-motion/statistic.js';
16
+ import {tableRows} from './form-layout/table.js';
17
+ import {createShimmer} from './motion/optional.js';
18
+
19
+ const content=new Set(['profile-card','video-lightbox','video-player','collection-layout','cascader','transfer','chip','tree-select','timeline','skeleton','media-image','tour','notification-center']);
20
+ export function nativeComponentAdapter(kind:string,root:HTMLElement,motion:MotionController):Adapter|undefined {
21
+ const layout=nativeLayoutAdapter(kind,root,motion);if(layout)return layout;
22
+ if(['drawer','palette','picker'].includes(kind)) {
23
+ const x=observeSurface(root,motion,kind as 'drawer'|'palette'|'picker');return {update(){x.sync();},capture:x.capture,destroy:x.destroy};
24
+ }
25
+ if(['choice','select','form-field'].includes(kind)) {
26
+ let target:HTMLElement|null=null,field:ReturnType<typeof enhanceField>|undefined,ripple:{destroy():void}|undefined;
27
+ const menu=kind==='select'?observeSurface(root,motion,'select'):undefined;
28
+ return {update(p){
29
+ const next=root.matches('.blu-field')?root:root.querySelector<HTMLElement>('.blu-field');
30
+ if(next!==target){field?.destroy();target=next;field=next?enhanceField(next,motion):undefined;}
31
+ field?.sync();menu?.sync();
32
+ if(p.Ripple&&!ripple)ripple=kind==='select'?enhanceSelectRipple(root,motion):enhanceChoiceRipple(root,motion);
33
+ else if(!p.Ripple&&ripple){ripple.destroy();ripple=undefined;}
34
+ },capture:()=>menu?.capture(),destroy(){field?.destroy();menu?.destroy();ripple?.destroy();}};
35
+ }
36
+ if(kind==='state-feedback'){
37
+ let signature=root.textContent+'|'+root.className;
38
+ const changed=()=>{const next=root.textContent+'|'+root.className;if(next===signature)return;signature=next;void motion.play(root,'fade',{duration:450});};
39
+ const observer=new MutationObserver(changed);observer.observe(root,{childList:true,subtree:true,characterData:true,attributes:true,attributeFilter:['class','aria-pressed','aria-selected']});
40
+ return {update:changed,destroy(){observer.disconnect();motion.cancel(root);}};
41
+ }
42
+ if(content.has(kind)) {
43
+ const x=enhanceContentMotion(root,motion,kind as ContentMotionKind);
44
+ let selected=root.classList.contains('blu-chip--selected');
45
+ const watch=kind==='chip'&&root.matches('.blu-chip')?new MutationObserver(()=>{const next=root.classList.contains('blu-chip--selected');if(next!==selected){selected=next;void motion.play(root,'fade',{duration:450});}}):undefined;watch?.observe(root,{attributes:true,attributeFilter:['class']});
46
+ let ripple:ReturnType<typeof enhanceChipRipple>|undefined;
47
+ return {capture:x.capture,update(p){
48
+ if(kind==='chip'&&p.Ripple&&!ripple)ripple=enhanceChipRipple(root,motion);
49
+ else if(!p.Ripple&&ripple){ripple.destroy();ripple=undefined;}
50
+ },destroy(){watch?.disconnect();motion.cancel(root);ripple?.destroy();x.destroy();}};
51
+ }
52
+ if(kind==='tree'){const x=enhanceTree(root,motion);return {update(){},destroy:x.destroy};}
53
+ if(kind==='notifications'){const x=enhanceNotifications(root,motion);return {update(){},destroy:x.destroy};}
54
+ if(['sidebar','pagination','tabs'].includes(kind)){const x=enhanceNavigation(root,motion,kind as 'sidebar'|'pagination'|'tabs');return {update(){},destroy:x.destroy};}
55
+ if(kind==='statistic'){
56
+ let p:NativeMotionOptions={},x:ReturnType<typeof enhanceStatistic>|undefined;
57
+ return {update(next){p=next;x??=enhanceStatistic(root,motion,()=>p);},capture:()=>x?.capture(),destroy:()=>x?.destroy()};
58
+ }
59
+ if(kind==='loading-skeleton'){
60
+ let x:ReturnType<typeof createShimmer>|undefined;
61
+ return {update(p){x??=createShimmer(root,{paused:p.Paused});x.update({paused:p.Paused});},destroy:()=>x?.destroy()};
62
+ }
63
+ if(kind==='grid'||kind==='table') {
64
+ let p:NativeMotionOptions={};
65
+ const read=():MotionRow[]=>{
66
+ if(kind==='grid')return [...root.querySelectorAll<HTMLElement>('.ag-center-cols-container > .ag-row[row-id], .ag-grid-scrolling-container > .ag-row[row-id]')].map(element=>{const cells=[...element.querySelectorAll<HTMLElement>('.ag-cell')];return {key:element.getAttribute('row-id')!,element,cells,cellKeys:cells.map(c=>c.getAttribute('col-id')||'')};});
67
+ // Stable native keys are supplied from the actual rendered data, never row position/text.
68
+ let keys:string[]=[];try{if(p.RowKeysJson) keys=JSON.parse(p.RowKeysJson).map(String); else if(p.RowKey&&!p.Loading){ const options={Caption:"",Rows:JSON.parse(p.RowsJson||'[]'),Columns:JSON.parse(p.ColumnsJson||'[]'),Page:p.Page,PageSize:p.PageSize,SortKey:p.SortKey,SortDirection:p.SortDirection as 'Ascending'|'Descending'}; keys=tableRows<Record<string,unknown>>(options).rows.map(row=>String(row[p.RowKey!])); }}catch{return [];}
69
+ const rows=[...root.querySelectorAll<HTMLTableRowElement>('.blu-table tbody > tr')];
70
+ if(rows.length!==keys.length||keys.some(k=>!k)||new Set(keys).size!==keys.length)return [];
71
+ return rows.map((element,i)=>({key:keys[i],element,cells:[...element.cells],cellKeys:[...element.cells].map((c,j)=>c.dataset.key||String(j))}));
72
+ };
73
+ const x=enhanceDataRows(root,motion,read);
74
+ return {capture:x.capture,update(next){p=next;},destroy:x.destroy};
75
+ }
76
+ }
@@ -0,0 +1,10 @@
1
+ /** Content producers install their current adapters regardless of which
2
+ * producer initialized the shared native motion registry first. */
3
+ import { createNativeMotionRuntime } from './native-motion.js';
4
+ import { nativeComponentAdapter } from './native-motion-components.js';
5
+ if (typeof window !== 'undefined') {
6
+ window.BluUIMotion12 ??= createNativeMotionRuntime();
7
+ for (const kind of ['profile-card', 'collection-layout', 'video-lightbox', 'media-image']) {
8
+ window.BluUIMotion12.register(kind, (root, motion) => nativeComponentAdapter(kind, root, motion)!);
9
+ }
10
+ }
@@ -0,0 +1,56 @@
1
+ import type {MotionController} from './motion/controller.js';
2
+ import type {Adapter} from './native-motion.js';
3
+ import {createMotionPresence} from './motion/presence.js';
4
+ import {createScrollReveal} from './motion/scroll-reveal.js';
5
+ import {playStagger,type StaggerOrder} from './motion/stagger.js';
6
+ import {createBorderGlow} from './motion/border-glow.js';
7
+ import {createRipple,createShimmer,createIconTransition,createEmphasis,type MotionIcon} from './motion/optional.js';
8
+ import {createPhotoZoom} from './motion/photo.js';
9
+ import {createSizeTransition} from './motion/structure.js';
10
+ import {playFeedback} from './motion/feedback.js';
11
+ import {createLoadingIndicator} from './motion/loading.js';
12
+
13
+ export function nativeFoundationAdapter(kind:string,root:HTMLElement,motion:MotionController):Adapter|undefined {
14
+ if(kind==='feedback'){let key:string|undefined;return {update(p){if(key===p.ReplayKey)return;key=p.ReplayKey;void playFeedback(root,motion,p.Decoration==='nudge'?'nudge':'highlight',{duration:p.Decoration==='nudge'?650:1400});},destroy(){motion.cancel(root);}};}
15
+ if(kind==='size'){let x:ReturnType<typeof createSizeTransition>|undefined,axis='';let value:boolean|undefined;return {update(p){const nextAxis=p.Variant==='width'?'width':'height';if(nextAxis!==axis){x?.destroy();axis=nextAxis;x=createSizeTransition(root,motion,nextAxis);value=undefined;}const next=p.IsVisible!==false;if(next===value)return;const fallback=p.FocusFallbackId?document.getElementById(p.FocusFallbackId)||undefined:undefined;if(!next&&root.contains(document.activeElement)&&(!fallback?.isConnected||root.contains(fallback)))throw Error('A focusable FocusFallbackId is required before collapsing focused content.');if(!next&&root.contains(document.activeElement)){fallback?.focus({preventScroll:true});if(root.contains(document.activeElement))throw Error('FocusFallbackId cannot receive focus.');}value=next;void x!.setOpen(next,{focusFallback:fallback,duration:650});},destroy:()=>x?.destroy()};}
16
+ if(kind==='visibility'){
17
+ const presence=createMotionPresence(root,motion);let value:boolean|undefined;
18
+ return {update(p){const next=p.IsVisible!==false;if(next===value)return;const fallback=p.FocusFallbackId?document.getElementById(p.FocusFallbackId)||undefined:undefined;
19
+ if(!next&&root.contains(document.activeElement)&&(!fallback?.isConnected||root.contains(fallback)))throw Error('A focusable FocusFallbackId outside MotionVisibility is required before hiding focused content.');
20
+ if(!next&&root.contains(document.activeElement)){fallback?.focus({preventScroll:true});if(root.contains(document.activeElement))throw Error('FocusFallbackId cannot receive focus.');}value=next; if(next)void presence.show({effect:p.Effect||'fade'});else void presence.hide({effect:p.Effect||'fade',focusFallback:fallback});
21
+ },destroy:presence.destroy};
22
+ }
23
+ if(kind==='reveal'){
24
+ let x:ReturnType<typeof createScrollReveal>|undefined,signature='';
25
+ return {update(p){const s=JSON.stringify([p.Effect,p.Once]);if(s===signature)return;signature=s;x?.destroy();x=createScrollReveal([root],motion,{effect:p.Effect||'fade',once:p.Once!==false,duration:750});},destroy:()=>x?.destroy()};
26
+ }
27
+ if(kind==='stagger'){
28
+ let key:string|undefined;const targets=new Set<HTMLElement>();
29
+ return {update(p){if(key===p.ReplayKey)return;key=p.ReplayKey;for(const el of targets)motion.cancel(el);targets.clear();
30
+ const items=[...root.querySelectorAll<HTMLElement>('[data-blu-stagger-item],.blu-motion-stagger-item')].slice(0,60);for(const el of items)targets.add(el);
31
+ void playStagger(motion,items,p.Effect||'fade',{duration:750,gap:p.GapMs??45,columns:p.Columns??1,order:(p.Order||'forward') as StaggerOrder});
32
+ },destroy(){for(const el of targets)motion.cancel(el);targets.clear();}};
33
+ }
34
+ if(kind==='decoration'){
35
+ let signature='',x:{destroy():void;update?:(p:any)=>void;setActive?:(on:boolean)=>unknown}|undefined;
36
+ return {update(p){const type=p.Decoration||'ripple',s=JSON.stringify([type,p.LoopDurationMs]);if(s!==signature){x?.destroy();x=undefined;signature=s;
37
+ if(type==='ripple')x=createRipple(root,motion);
38
+ else if(type==='shimmer')x=createShimmer(root,{duration:p.LoopDurationMs||1800,paused:p.Paused});
39
+ else if(type==='border-glow')x=createBorderGlow(root,{duration:p.LoopDurationMs||12000,paused:p.Paused});
40
+ else if(type==='underline'||type==='border')x=createEmphasis(root,motion,type);
41
+ else throw Error('Unsupported decoration.');
42
+ }x?.update?.({paused:p.Paused});void x?.setActive?.(p.IsActive===true);},destroy:()=>x?.destroy()};
43
+ }
44
+ if(kind==='icon'){
45
+ let x:ReturnType<typeof createIconTransition>|undefined,kind='';
46
+ return {update(p){if(kind!==(p.Decoration||'chevron')){x?.destroy();kind=p.Decoration||'chevron';x=createIconTransition(root,motion,kind as MotionIcon);}void x?.setActive(p.IsActive===true);},destroy:()=>x?.destroy()};
47
+ }
48
+ if(kind==='photo'){
49
+ let target:HTMLImageElement|null=null,x:ReturnType<typeof createPhotoZoom>|undefined;
50
+ return {update(p){const next=root.querySelector('img');if(next!==target){x?.destroy();target=next;x=next?createPhotoZoom(next,motion):undefined;}void x?.setZoomed(p.IsActive===true);},destroy:()=>x?.destroy()};
51
+ }
52
+ if(kind==='loading'){
53
+ let x:ReturnType<typeof createLoadingIndicator>|undefined;
54
+ return {update(p){const options={Label:p.Label||'Loading',Variant:(p.Variant||'Spinner') as 'Spinner'|'Dots'|'Bar',Progress:p.Progress};if(x)x.update(options);else x=createLoadingIndicator(root,options);},destroy:()=>x?.destroy()};
55
+ }
56
+ }
@@ -0,0 +1,35 @@
1
+ import type {MotionController} from './motion/controller.js';
2
+ import type {Adapter} from './native-motion.js';
3
+ import {preserveStyles} from './motion/structure.js';
4
+ import {createStrokeTransition} from './motion/value.js';
5
+ export function nativeLayoutAdapter(kind:string,root:HTMLElement,motion:MotionController):Adapter|undefined {
6
+ if(kind==='sidebar'){
7
+ let open=!root.classList.contains('blu-sidebar--closed'),dead=false,revision=0,release=()=>{};
8
+ const sync=()=>{const next=!root.classList.contains('blu-sidebar--closed');if(next===open)return;open=next;const own=++revision;motion.cancel(root);release();release=()=>{};
9
+ if(next){void motion.play(root,'fade',{duration:450});return;}
10
+ if(motion.mode==='off'||root.contains(document.activeElement))return;
11
+ const style=preserveStyles(root,['display','pointer-events']),inert=root.inert,aria=root.getAttribute('aria-hidden');
12
+ release=()=>{style();root.inert=inert;if(aria===null)root.removeAttribute('aria-hidden');else root.setAttribute('aria-hidden',aria);};
13
+ root.style.setProperty('display','block','important');root.style.pointerEvents='none';root.inert=true;root.setAttribute('aria-hidden','true');
14
+ void motion.animate(root,[{opacity:1},{opacity:0}],{duration:450},'fade').finally(()=>{if(!dead&&own===revision){release();release=()=>{};}});
15
+ };const observer=new MutationObserver(sync);observer.observe(root,{attributes:true,attributeFilter:['class']});
16
+ return {update:sync,destroy(){dead=true;++revision;observer.disconnect();motion.cancel(root);release();}};
17
+ }
18
+ if(kind==='splitter'){
19
+ let columns=getComputedStyle(root).gridTemplateColumns,rows=getComputedStyle(root).gridTemplateRows,dragging=false;
20
+ const capture=()=>{const css=getComputedStyle(root);columns=css.gridTemplateColumns;rows=css.gridTemplateRows;};
21
+ const down=()=>{dragging=true;motion.cancel(root);capture();},up=()=>{dragging=false;capture();};
22
+ const refresh=()=>{const css=getComputedStyle(root),nextColumns=css.gridTemplateColumns,nextRows=css.gridTemplateRows;
23
+ if(!dragging&&(columns!==nextColumns||rows!==nextRows))void motion.animate(root,[{gridTemplateColumns:columns,gridTemplateRows:rows},{gridTemplateColumns:nextColumns,gridTemplateRows:nextRows}],{duration:650,easing:'ease-in-out'});
24
+ columns=nextColumns;rows=nextRows;
25
+ };const observer=new MutationObserver(refresh);observer.observe(root,{attributes:true,attributeFilter:['style','class']});
26
+ root.addEventListener('pointerdown',down,true);root.ownerDocument.addEventListener('pointerup',up,true);root.ownerDocument.addEventListener('pointercancel',up,true);
27
+ return {capture,update:refresh,destroy(){observer.disconnect();motion.cancel(root);root.removeEventListener('pointerdown',down,true);root.ownerDocument.removeEventListener('pointerup',up,true);root.ownerDocument.removeEventListener('pointercancel',up,true);}};
28
+ }
29
+ if(kind==='sparkline'){
30
+ let path:SVGGeometryElement|null=null,stroke:ReturnType<typeof createStrokeTransition>|undefined;
31
+ const sync=()=>{const next=root.querySelector<SVGGeometryElement>('.blu-sparkline__line');if(next===path)return;stroke?.destroy();path=next;stroke=next?createStrokeTransition(next,motion,0):undefined;void stroke?.set(100,{duration:750});};
32
+ const observer=new MutationObserver(sync);observer.observe(root,{childList:true,subtree:true});sync();
33
+ return {update:sync,destroy(){observer.disconnect();stroke?.destroy();}};
34
+ }
35
+ }
@@ -0,0 +1,151 @@
1
+ /** Private O11 pilot. Enhances stable native wrappers; never owns their children. */
2
+ import { createMotionController, type MotionController, type MotionEffect } from './motion/controller.js';
3
+ import { createComponentMotionScope, validateComponentMotion, type ComponentMotionSettings } from './motion/component-settings.js';
4
+ import { motionEffects } from './motion/effects.js';
5
+ import { createPressFeedback } from './motion/feedback.js';
6
+ import { createMotionPresence } from './motion/presence.js';
7
+ import { enhanceField } from './web-motion/fields.js';
8
+ import { observeSurface } from './web-motion/surfaces.js';
9
+
10
+ export interface NativeMotionOptions {
11
+ EnableMotion?: boolean;
12
+ Preference?: 'System' | 'Reduced' | 'Off';
13
+ UseDefaultDuration?: boolean;
14
+ DurationMs?: number;
15
+ Easing?: ComponentMotionSettings['easing'];
16
+ Effect?: MotionEffect;
17
+ ReplayKey?: string;
18
+ ActivePanel?: string;
19
+ FocusFallbackId?: string;
20
+ }
21
+ type Kind = 'provider' | 'container' | 'button' | 'field' | 'modal' | 'workspace';
22
+ type Adapter = { update(p: NativeMotionOptions): void; capture?(): void; destroy(): void };
23
+ type Owner = { root: HTMLElement; kind: Kind; parent?: Owner; controller?: MotionController; scope?: ReturnType<typeof createComponentMotionScope>; adapter: Adapter; signature: string; priorKind: string | null; options: NativeMotionOptions };
24
+
25
+ export function nativeMotionSettings(p: NativeMotionOptions): ComponentMotionSettings {
26
+ const preference = p.Preference ?? 'System';
27
+ if (!['System', 'Reduced', 'Off'].includes(preference)) throw Error('Preference must be System, Reduced or Off.');
28
+ if (p.Effect !== undefined && !(motionEffects as readonly string[]).includes(p.Effect) && p.Effect !== 'scale') throw Error('Unknown motion effect.');
29
+ if (p.UseDefaultDuration === false && p.DurationMs === undefined) throw Error('DurationMs is required when UseDefaultDuration is False.');
30
+ return validateComponentMotion({ mode: preference === 'System' ? 'inherit' : preference.toLowerCase() as 'reduced' | 'off',
31
+ duration: p.UseDefaultDuration === false ? p.DurationMs : undefined, easing: p.Easing });
32
+ }
33
+
34
+ /** Multiple producer scripts share this registry, never overlapping body controllers. */
35
+ export function createNativeMotionRuntime() {
36
+ const owners = new Map<string, Owner>();
37
+ const hosts = new WeakMap<HTMLElement, string>();
38
+ function parentOf(root: HTMLElement): Owner | undefined {
39
+ for (let el = root.parentElement; el; el = el.parentElement) {
40
+ const owner = owners.get(hosts.get(el) || '');
41
+ if (owner?.kind === 'provider') return owner;
42
+ }
43
+ }
44
+ function destroy(id: string) {
45
+ const owner = owners.get(id); if (!owner) return;
46
+ // Children release their adapters before a screen provider disappears.
47
+ for (const [childId, child] of owners) if (child.parent === owner) destroy(childId);
48
+ owner.adapter.destroy(); owner.scope?.destroy(); owner.controller?.destroy();
49
+ if (owner.priorKind === null) owner.root.removeAttribute('data-blu-native-motion');
50
+ else owner.root.setAttribute('data-blu-native-motion', owner.priorKind);
51
+ owners.delete(id); if (hosts.get(owner.root) === id) hosts.delete(owner.root);
52
+ }
53
+ function makeAdapter(kind: Kind, root: HTMLElement, motion: MotionController): Adapter {
54
+ if (kind === 'modal') {
55
+ // The original modal owns focus, events and slots. Only its JS-created
56
+ // surface is enhanced; closing copies are inert, unnamed visual decoration.
57
+ const surface = observeSurface(root, motion, 'modal');
58
+ return { update() { surface.sync(); }, capture: surface.capture, destroy: surface.destroy };
59
+ }
60
+ if (kind === 'button') {
61
+ const press = createPressFeedback(root, motion);
62
+ let busy = root.getAttribute('aria-busy'), face: HTMLElement | null = null;
63
+ const refresh = () => {
64
+ const next = root.getAttribute('aria-busy');
65
+ if (next === busy) return;
66
+ busy = next; if (face) motion.cancel(face);
67
+ face = root.querySelector<HTMLElement>('.blu-button-content');
68
+ // Native If widgets still own the label/spinner. Fade the stable face only.
69
+ if (face) void motion.play(face, 'fade', { duration: 350 });
70
+ };
71
+ const observer = new MutationObserver(refresh);
72
+ observer.observe(root, { attributes: true, attributeFilter: ['aria-busy'], childList: true, subtree: true });
73
+ return { update: refresh, destroy() { observer.disconnect(); if (face) motion.cancel(face); press.destroy(); } };
74
+ }
75
+ if (kind === 'field') {
76
+ let target: HTMLElement | null = null, field: ReturnType<typeof enhanceField> | undefined;
77
+ return { update() {
78
+ const next = root.matches('.blu-field') ? root : root.querySelector<HTMLElement>('.blu-field');
79
+ if (next !== target) { field?.destroy(); target = next; field = target ? enhanceField(target, motion) : undefined; }
80
+ field?.sync();
81
+ }, destroy() { field?.destroy(); } };
82
+ }
83
+ if (kind === 'workspace') {
84
+ const panels = Array.from(root.children).filter((el): el is HTMLElement => el instanceof HTMLElement && el.classList.contains('blu-native-workspace-panel'));
85
+ if (panels.length !== 2) throw Error('WorkspaceTransition requires two stable native panel wrappers.');
86
+ const presence = panels.map(el => createMotionPresence(el, motion));
87
+ let active = -1, dead = false, revision = 0;
88
+ return { update(p) {
89
+ const next = p.ActivePanel === 'Second' ? 1 : 0;
90
+ if (next === active) return;
91
+ const old = active; active = next; const own = ++revision;
92
+ const fallback = (p.FocusFallbackId ? root.ownerDocument.getElementById(p.FocusFallbackId) : root) || root;
93
+ if (old < 0) { panels.forEach((panel, i) => { panel.hidden = i !== next; panel.inert = i !== next; }); return; }
94
+ // Existing values and nodes stay in place; application state already changed.
95
+ if (panels[old].contains(root.ownerDocument.activeElement)) fallback.focus({ preventScroll: true });
96
+ if (panels[old].contains(root.ownerDocument.activeElement)) root.focus({ preventScroll: true });
97
+ const options = { effect: p.Effect || 'fade' as MotionEffect, duration: 650, focusFallback: root };
98
+ void presence[old].hide(options).then(() => { if (!dead && own === revision) panels[old].inert = true; });
99
+ void presence[next].show(options);
100
+ }, destroy() { dead = true; ++revision; presence.forEach(p => p.destroy()); } };
101
+ }
102
+ let replay: string | undefined;
103
+ return { update(p) {
104
+ if (kind !== 'container' || p.ReplayKey === replay) return;
105
+ replay = p.ReplayKey;
106
+ void motion.play(root, p.Effect || 'fade', { duration: 750 });
107
+ }, destroy() { motion.cancel(root); } };
108
+ }
109
+ function sync(kind: Kind, hostId: string, p: NativeMotionOptions, ownerId = hostId): string {
110
+ try {
111
+ if (!ownerId) throw Error('A stable native motion owner is required.');
112
+ if (p.EnableMotion !== true) { destroy(ownerId); return ''; }
113
+ const settings = nativeMotionSettings(p); // Reject invalid changes before touching the old adapter.
114
+ const root = document.getElementById(hostId); if (!root) { destroy(ownerId); return 'Native motion host is not rendered yet.'; }
115
+ const parent = parentOf(root), previous = owners.get(ownerId), occupant = hosts.get(root);
116
+ if (occupant && occupant !== ownerId) throw Error('Native motion host already has an owner.');
117
+ if (previous && (previous.root !== root || previous.kind !== kind || previous.parent !== parent)) destroy(ownerId);
118
+ let owner = owners.get(ownerId);
119
+ const signature = JSON.stringify(settings);
120
+ if (!owner) {
121
+ const controller = parent ? undefined : createMotionController(root, 'system');
122
+ const base = parent?.scope?.motion || parent?.controller || controller!;
123
+ // A private controller owns the root attribute; its component scope is destroyed first.
124
+ const scope = createComponentMotionScope(root, base, settings);
125
+ const priorKind = root.getAttribute('data-blu-native-motion');
126
+ root.setAttribute('data-blu-native-motion', kind);
127
+ try {
128
+ owner = { root, kind, parent, controller, scope, adapter: makeAdapter(kind, root, scope.motion), signature, priorKind, options: { ...p } };
129
+ owners.set(ownerId, owner); hosts.set(root, ownerId);
130
+ } catch (error) { scope.destroy(); controller?.destroy(); if (priorKind === null) root.removeAttribute('data-blu-native-motion'); else root.setAttribute('data-blu-native-motion', priorKind); throw error; }
131
+ } else if (owner.signature !== signature) { owner.scope!.update(settings); owner.signature = signature; }
132
+ const preference = settings.mode === 'inherit' ? 'system' : settings.mode || 'system';
133
+ if (owner.controller && owner.controller.preference !== preference) owner.controller.setPreference(preference);
134
+ owner.options = { ...p };
135
+ owner.adapter.update({ ...p, ReplayKey: p.ReplayKey ?? '' });
136
+ // O11 can ready inner Blocks before their enclosing provider.
137
+ if (kind === 'provider') for (const [childId, child] of Array.from(owners)) {
138
+ if (child !== owner && root.contains(child.root) && child.parent !== parentOf(child.root)) sync(child.kind, child.root.id, child.options, childId);
139
+ }
140
+ return '';
141
+ } catch (error) { return error instanceof Error ? error.message : String(error); }
142
+ }
143
+ return { version: '1.2.0-local-pilot', sync, destroy,
144
+ capture: (id: string) => owners.get(id)?.adapter.capture?.(),
145
+ /** Read-only counts for lifecycle checks; does not expose application data. */
146
+ inspect: () => ({ owners: owners.size, kinds: Array.from(owners.values(), o => o.kind) }),
147
+ };
148
+ }
149
+
150
+ declare global { interface Window { BluUIMotionPilot?: ReturnType<typeof createNativeMotionRuntime> } }
151
+ if (typeof window !== 'undefined' && !window.BluUIMotionPilot) window.BluUIMotionPilot = createNativeMotionRuntime();