@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
@@ -1,4 +1,4 @@
1
- // ../blu-grid/docs/scripts/advanced/shared.js
1
+ // docs/scripts/advanced/shared.js
2
2
  import { state, register } from "./kernel.js";
3
3
  var shared = state("advanced", () => ({ instances: /* @__PURE__ */ new Map(), serial: 0 }));
4
4
  var uid = () => `blu-advanced-${++shared.serial}`;
@@ -91,7 +91,7 @@ function createAPI(kind, init2, update2) {
91
91
  }, destroy }));
92
92
  }
93
93
 
94
- // ../blu-grid/docs/scripts/advanced/date-utils.js
94
+ // docs/scripts/advanced/date-utils.js
95
95
  var pad = (n) => String(n).padStart(2, "0");
96
96
  function dateValue(d) {
97
97
  return String(d.getFullYear()).padStart(4, "0") + "-" + pad(d.getMonth() + 1) + "-" + pad(d.getDate());
@@ -172,7 +172,7 @@ function validateRange(start, end, options = {}) {
172
172
  return "";
173
173
  }
174
174
 
175
- // ../blu-grid/docs/scripts/advanced/pickers.js
175
+ // docs/scripts/advanced/pickers.js
176
176
  var formatOptions = ["YYYY-MM-DD", "MM/DD/YYYY", "DD/MM/YYYY"];
177
177
  var today = () => dateValue(/* @__PURE__ */ new Date());
178
178
  var datePart = (value) => String(value || "").split("T")[0];
@@ -256,14 +256,15 @@ function positionPopup(s) {
256
256
  const below = Math.max(0, top + height - edge - anchor.bottom - gap), above = Math.max(0, anchor.top - gap - top - edge);
257
257
  const natural = panel.scrollHeight + 2, up = natural > below && above > below, room = Math.max(0, height - edge * 2);
258
258
  panel.style.maxHeight = room + "px";
259
- panel.style.left = Math.max(left + edge, Math.min(anchor.left, left + width - edge - panelWidth)) + "px";
259
+ const fields = s.fieldRow.getBoundingClientRect();
260
+ panel.style.left = Math.max(left + edge, Math.min(fields.left + (fields.width - panelWidth) / 2, left + width - edge - panelWidth)) + "px";
260
261
  const preferred = up ? anchor.top - gap - natural : anchor.bottom + gap;
261
262
  panel.style.top = Math.max(top + edge, Math.min(preferred, top + height - edge - Math.min(natural, room))) + "px";
262
263
  panel.dataset.placement = up ? "top" : "bottom";
263
264
  }
264
265
  function focusPopup(s) {
265
266
  const timeField = s.timeOnly || s.dateTime && s.anchor === s.fields[1].input;
266
- (timeField ? s.hours : s.calendar.querySelector('[data-date="' + s.focusDate + '"]') || s.monthSelect).focus();
267
+ (timeField ? s.hours : s.calendar.querySelector('[data-date="' + s.focusDate + '"]') || s.monthSelect).focus({ preventScroll: true });
267
268
  }
268
269
  function commit(s, first, end, time, keepOpen = false) {
269
270
  const issue = validate(s, first, end, time);
@@ -287,7 +288,7 @@ function commit(s, first, end, time, keepOpen = false) {
287
288
  s.inputSignature = JSON.stringify(s.range ? [s.start, s.end] : [s.value]);
288
289
  paintInputs(s);
289
290
  if (!keepOpen) close(s);
290
- if (returnToInput) s.fields[0].input.focus();
291
+ if (returnToInput) s.fields[0].input.focus({ preventScroll: true });
291
292
  s.notice.textContent = s.range ? first ? labelDate(s, first) + " to " + labelDate(s, end) : "Date range cleared" : value ? "Selected " + (s.timeOnly ? formatTime(value, s.cycle) : labelDate(s, first) + (s.hasTime ? " at " + formatTime(time, s.cycle) : "")) : "Selection cleared";
292
293
  if (changed) {
293
294
  if (s.range) emit(s, "Change", first, end);
@@ -391,11 +392,18 @@ function appendOption(select, value, label) {
391
392
  return option;
392
393
  }
393
394
  function selectField(s, label, name) {
394
- const wrap = node("label", "blu-picker__select-field"), caption = node("span", "", label), select = node("select", "blu-picker__input");
395
+ const wrap = node("label", "blu-picker__select-field"), caption = node("span", "blu-picker__select-caption", label), select = node("select", "blu-picker__input");
395
396
  select.id = s.uid + "-" + name;
396
397
  wrap.append(caption, select);
397
398
  return { wrap, select };
398
399
  }
400
+ function focusPopupButton(event) {
401
+ if (event.button !== 0) return;
402
+ const target = event.target.closest?.("button");
403
+ if (!target || target.disabled) return;
404
+ event.preventDefault();
405
+ target.focus({ preventScroll: true });
406
+ }
399
407
  function focusDay(s, value) {
400
408
  const min = datePart(s.options.Min) || "0001-01-01", max = datePart(s.options.Max) || "9999-12-31";
401
409
  value = value < min ? min : value > max ? max : value;
@@ -403,7 +411,7 @@ function focusDay(s, value) {
403
411
  s.focusDate = value;
404
412
  s.month = value.slice(0, 7);
405
413
  paintCalendar(s);
406
- s.calendar.querySelector('[data-date="' + value + '"]')?.focus();
414
+ s.calendar.querySelector('[data-date="' + value + '"]')?.focus({ preventScroll: true });
407
415
  }
408
416
  function keyDay(s, event, value) {
409
417
  if (event.altKey || event.ctrlKey || event.metaKey) return;
@@ -441,7 +449,7 @@ function chooseDay(s, value) {
441
449
  return;
442
450
  }
443
451
  paintCalendar(s);
444
- s.calendar.querySelector('[data-date="' + value + '"]')?.focus();
452
+ s.calendar.querySelector('[data-date="' + value + '"]')?.focus({ preventScroll: true });
445
453
  paintDraft(s);
446
454
  }
447
455
  function paintDraft(s) {
@@ -594,6 +602,7 @@ function init(s) {
594
602
  attr(s.dialog, "aria-modal", "false");
595
603
  if (s.dialog.showPopover) attr(s.dialog, "popover", "manual");
596
604
  s.root.append(s.dialog);
605
+ s.dialog.addEventListener("mousedown", focusPopupButton);
597
606
  s.title = node("h2", "blu-advanced-sr");
598
607
  s.title.id = s.uid + "-title";
599
608
  s.dialog.append(s.title);
@@ -606,7 +615,7 @@ function init(s) {
606
615
  const month = selectField(s, "Month", "month");
607
616
  s.monthSelect = month.select;
608
617
  const year = node("label", "blu-picker__select-field");
609
- year.append(node("span", "", "Year"));
618
+ year.append(node("span", "blu-picker__select-caption", "Year"));
610
619
  s.yearInput = node("input", "blu-picker__input");
611
620
  s.yearInput.type = "number";
612
621
  s.yearInput.step = "1";
@@ -804,8 +813,8 @@ function update(s) {
804
813
  else {
805
814
  paintCalendar(s);
806
815
  paintTimes(s);
807
- if (focusDate) s.calendar.querySelector('[data-date="' + focusDate + '"]')?.focus();
808
- else if (focusId) document.getElementById(focusId)?.focus();
816
+ if (focusDate) s.calendar.querySelector('[data-date="' + focusDate + '"]')?.focus({ preventScroll: true });
817
+ else if (focusId) document.getElementById(focusId)?.focus({ preventScroll: true });
809
818
  positionPopup(s);
810
819
  }
811
820
  }
package/src/select.ts ADDED
@@ -0,0 +1,2 @@
1
+ export {createSelect} from './form-layout/select.js';
2
+ export type * from './form-layout/types.js';
package/src/switch.ts ADDED
@@ -0,0 +1,2 @@
1
+ export {createSwitch} from './form-layout/choice.js';
2
+ export type * from './form-layout/types.js';
package/src/table.ts ADDED
@@ -0,0 +1,2 @@
1
+ export {createTable} from './form-layout/table.js';
2
+ export type * from './form-layout/types.js';
@@ -0,0 +1,2 @@
1
+ export {createTextArea} from './form-layout/text.js';
2
+ export type * from './form-layout/types.js';
@@ -0,0 +1,2 @@
1
+ export {createTextInput} from './form-layout/text.js';
2
+ export type * from './form-layout/types.js';
package/src/types.ts CHANGED
@@ -1285,15 +1285,3 @@ DurationSeconds: number;
1285
1285
  /** Played ranges since the previous checkpoint. Seeking gaps are excluded. */
1286
1286
  Segments: BluVideoWatchedSegment[];
1287
1287
  }
1288
-
1289
-
1290
-
1291
-
1292
-
1293
-
1294
-
1295
-
1296
-
1297
-
1298
-
1299
-
@@ -0,0 +1,31 @@
1
+ import type {MotionController} from '../motion/controller.js';
2
+ import {createRipple} from '../motion/optional.js';
3
+
4
+ /** Decorate the selected chip after its real click, even if that click replaces
5
+ * its button. No synthesized activation or application-state change. */
6
+ export function enhanceChipRipple(root:HTMLElement,motion:MotionController) {
7
+ let dead=false,revision=0,frame=0,current:ReturnType<typeof createRipple>|undefined;
8
+ const click=(event:MouseEvent)=>{
9
+ const source=event.target instanceof Element?event.target.closest<HTMLButtonElement>('.blu-chip__select'):null;
10
+ if(!source||!root.contains(source)||source.disabled)return;
11
+ const labels=[...root.querySelectorAll<HTMLButtonElement>('.blu-chip__select')];
12
+ const label=source.textContent,occurrence=labels.filter(b=>b.textContent===label).indexOf(source);
13
+ const selected=source.getAttribute('aria-pressed');
14
+ const point=event.detail?{clientX:event.clientX,clientY:event.clientY}:undefined,own=++revision;
15
+ // Microtasks can run between capture and the target's click handler.
16
+ // A frame waits until the component has completed its normal selection.
17
+ cancelAnimationFrame(frame);frame=requestAnimationFrame(()=>{
18
+ frame=0;
19
+ if(dead||own!==revision)return;
20
+ current?.destroy();current=undefined;
21
+ const target=source.isConnected?source:[...root.querySelectorAll<HTMLButtonElement>('.blu-chip__select')].filter(b=>b.textContent===label)[occurrence];
22
+ if(!target||target.disabled||motion.mode!=='full')return;
23
+ // O11 prevents the browser's default click while still updating selection.
24
+ // A cancelled activation with no real selection change stays undecorated.
25
+ if(event.defaultPrevented&&target.getAttribute('aria-pressed')===selected)return;
26
+ current=createRipple(target,motion,{duration:1000,easing:'ease-out',listen:false});current.play(point);
27
+ });
28
+ };
29
+ root.addEventListener('click',click,true);
30
+ return {destroy(){if(dead)return;dead=true;++revision;cancelAnimationFrame(frame);root.removeEventListener('click',click,true);current?.destroy();}};
31
+ }
@@ -0,0 +1,30 @@
1
+ import type {MotionController,MotionOptions} from '../motion/controller.js';
2
+ import {createRipple} from '../motion/optional.js';
3
+
4
+ /** One decorative pulse per native activation. Label clicks, Space and radio
5
+ * arrow changes keep the original input, focus, value and change events. */
6
+ export function enhanceChoiceRipple(root:HTMLElement,motion:MotionController,timing:MotionOptions={}) {
7
+ let dead=false,frame=0,current:ReturnType<typeof createRipple>|undefined;
8
+ const activate=(event:Event)=>{
9
+ const input=event.target;
10
+ if(!(input instanceof HTMLInputElement)||!input.matches('.blu-choice__input')||input.disabled||input.getAttribute('aria-readonly')==='true')return;
11
+ const name=input.name,value=input.value,type=input.type;
12
+ const matches=()=>[...root.querySelectorAll<HTMLInputElement>('.blu-choice__input')].filter(el=>el.name===name&&el.value===value&&el.type===type);
13
+ const occurrence=matches().indexOf(input),checked=input.checked;
14
+ cancelAnimationFrame(frame);frame=requestAnimationFrame(()=>{
15
+ frame=0;if(dead||event.defaultPrevented||motion.mode!=='full')return;
16
+ // Native parameter synchronization can replace a RadioGroup's inputs.
17
+ // Resolve only the same choice with its already-committed checked state.
18
+ const target=root.contains(input)?input:matches()[occurrence];
19
+ if(!target||target.disabled||target.getAttribute('aria-readonly')==='true'||target.checked!==checked)return;
20
+ const mark=target.nextElementSibling as HTMLElement|null;
21
+ if(!mark?.classList.contains('blu-choice__mark'))return;
22
+ current?.destroy();current=createRipple(mark,motion,{duration:1000,easing:'ease-out',...timing,listen:false});
23
+ current.element.classList.add('blu-motion-choice-ripple');current.play();
24
+ });
25
+ };
26
+ // Capture identity before application handlers can replace the input. The
27
+ // decorative work still waits until the event and native state commit finish.
28
+ root.addEventListener('click',activate,true);root.addEventListener('change',activate,true);
29
+ return {destroy(){if(dead)return;dead=true;cancelAnimationFrame(frame);root.removeEventListener('click',activate,true);root.removeEventListener('change',activate,true);current?.destroy();}};
30
+ }
@@ -0,0 +1,74 @@
1
+ import type {MotionController} from '../motion/controller.js';
2
+ import {observeSurface} from './surfaces.js';
3
+ import {preserveStyles} from '../motion/structure.js';
4
+ export type ContentMotionKind = 'profile-card'|'video-lightbox'|'video-player'|'collection-layout'|'cascader'|'transfer'|'chip'|'tree-select'|'timeline'|'skeleton'|'media-image'|'tour'|'notification-center';
5
+ /** DOM-only enhancement: the original component still owns data, focus and media playback. */
6
+ export function enhanceContentMotion(root:HTMLElement,motion:MotionController,kind:ContentMotionKind,options:{timelineKeys?:()=>string[]}={}) {
7
+ let dead=false,frame=0,serial=0;
8
+ const active=new Map<HTMLElement,number>(),identities=new WeakMap<Element,string>();
9
+ const identity=(el:Element)=>{let id=identities.get(el);if(!id){id='node-'+ ++serial;identities.set(el,id);}return id;};
10
+ const run=(el:HTMLElement,frames:Keyframe[],duration=650,fade=false)=>{const token=++serial;active.set(el,token);void motion.animate(el,frames,{duration,easing:'ease-in-out'},fade?'fade':'instant').finally(()=>{if(active.get(el)===token)active.delete(el);});};
11
+ type Item={el:HTMLElement;rect:DOMRect;signature:string};
12
+ function read(){
13
+ const origin=root.getBoundingClientRect();
14
+ const entries: [string,Item][]=[];
15
+ const add=(selector:string,key:(el:HTMLElement,i:number)=>string,signature:(el:HTMLElement)=>string=()=> '')=>{
16
+ [...root.querySelectorAll<HTMLElement>(selector)].slice(0,40).forEach((el,i)=>{const r=el.getBoundingClientRect(),shift=active.has(el)?getComputedStyle(el).translate.split(/\s+/).map(Number.parseFloat):[],rect=new DOMRect(r.left-origin.left-(shift[0]||0),r.top-origin.top-(shift[1]||0),r.width,r.height);if(rect.width&&rect.height&&!el.closest('[hidden]'))entries.push([key(el,i),{el,rect,signature:signature(el)}]);});
17
+ };
18
+ if(kind==='collection-layout')add('.blu-collection-layout__item',identity);
19
+ if(kind==='timeline')add('.blu-timeline__item',(el,i)=>options.timelineKeys?.()[i]||el.querySelector('h3')?.textContent||identity(el));
20
+ if(kind==='transfer')add('.blu-transfer__row',el=>el.dataset.key||el.querySelector('.blu-transfer__label')?.textContent||identity(el));
21
+ if(kind==='chip')add('.blu-chip',el=>el.querySelector('.blu-chip__select,.blu-chip__label')?.textContent||identity(el),el=>String(el.classList.contains('blu-chip--selected')));
22
+ if(kind==='tree-select')add('.blu-tree-select__item[data-key] > .blu-tree-select__row',el=>el.parentElement!.dataset.key!);
23
+ if(kind==='cascader')add('.blu-cascader__column',(_,i)=>'column-'+i,el=>el.textContent||'');
24
+ if(kind==='tour')add('.blu-tour__panel',()=> 'tour-step',el=>el.textContent||'');
25
+ if(kind==='notification-center'){add('.blu-notification-center__list',()=> 'inbox',el=>el.textContent||'');add('.blu-notification-center__item',el=>el.querySelector('[data-focus]')?.getAttribute('data-focus')||identity(el),el=>el.className);}
26
+ if(kind==='skeleton')add('[aria-busy]',()=> 'loading-content',el=>el.getAttribute('aria-busy')||'');
27
+ const unique=new Map<string,Item>(),duplicates=new Set<string>();
28
+ for(const [key,item] of entries){if(unique.has(key))duplicates.add(key);unique.set(key,item);}
29
+ for(const key of duplicates)unique.delete(key); // Ambiguous labels never identify moving records.
30
+ return unique;
31
+ }
32
+ let before=read();
33
+ const capture=()=>{if(!frame)before=read();};
34
+ function reconcile(){frame=0;if(dead)return;const after=read();
35
+ for(const [key,item] of after){const old=before.get(key);if(!old){run(item.el,[{opacity:0},{opacity:1}],450,true);continue;}
36
+ const x=old.rect.left-item.rect.left,y=old.rect.top-item.rect.top;
37
+ if(kind!=='tour'&&(Math.abs(x)>1||Math.abs(y)>1))run(item.el,[{translate:`${x}px ${y}px`},{translate:'0px 0px'}]);
38
+ else if(old.signature!==item.signature)run(item.el,[{opacity:.55},{opacity:1}],450,true);
39
+ }
40
+ for(const el of active.keys())if(!root.contains(el)){motion.cancel(el);active.delete(el);}
41
+ before=after;
42
+ }
43
+ const observer=new MutationObserver(()=>{if(!frame)frame=requestAnimationFrame(reconcile);});
44
+ observer.observe(root,{childList:true,subtree:true,attributes:true,attributeFilter:['hidden','class','aria-selected','aria-checked','style']});
45
+ for(const event of ['click','change','input','keydown','pointerdown'])root.addEventListener(event,capture,true);
46
+ const previousKind=root.getAttribute('data-blu-content-motion');root.setAttribute('data-blu-content-motion',kind);
47
+ const surface=kind==='cascader'||kind==='tree-select'?observeSurface(root,motion,kind):undefined;
48
+ // Media/profile dialogs retain the real content on close. It is already paused
49
+ // by the component, inert and inaccessible during the short cosmetic fade.
50
+ const layers=new Map<HTMLElement,{open:boolean;revision:number;restore:()=>void}>();
51
+ function overlay(){for(const layer of root.querySelectorAll<HTMLElement>('.blu-overlay,.blu-tour')){
52
+ // Nested components own their own dialogs. Otherwise a collection and its
53
+ // profile card both snapshot temporary exit styles and restore display:flex
54
+ // after the hidden dialog finishes closing.
55
+ if(layer.closest('[data-blu-content-motion]')!==root)continue;
56
+ let state=layers.get(layer);if(!state){state={open:false,revision:0,restore:()=>{}};layers.set(layer,state);}
57
+ const open=!layer.hidden;if(open===state.open)continue;state.open=open;const revision=++state.revision;state.restore();motion.cancel(layer);
58
+ if(open){void motion.play(layer,'fade',{duration:450});continue;}
59
+ if(motion.mode==='off')continue;
60
+ const restore=preserveStyles(layer,['display','pointer-events']),inert=layer.inert,aria=layer.getAttribute('aria-hidden');
61
+ let timer:ReturnType<typeof setTimeout>|undefined;
62
+ state.restore=()=>{clearTimeout(timer);restore();layer.inert=inert;if(aria===null)layer.removeAttribute('aria-hidden');else layer.setAttribute('aria-hidden',aria);};
63
+ layer.inert=true;layer.setAttribute('aria-hidden','true');layer.style.setProperty('display','flex','important');layer.style.pointerEvents='none';
64
+ const finish=()=>{if(!dead&&state!.revision===revision){state!.restore();state!.restore=()=>{};}};
65
+ timer=setTimeout(finish,(motion.resolveOptions?.({duration:450}).duration??450)+100);
66
+ void motion.animate(layer,[{opacity:1},{opacity:0}],{duration:450,easing:'ease-in-out'},'fade').finally(finish);
67
+ }}
68
+ const overlayObserver=new MutationObserver(overlay);
69
+ if(['profile-card','video-lightbox','collection-layout','media-image','tour'].includes(kind)){overlayObserver.observe(root,{subtree:true,childList:true,attributes:true,attributeFilter:['hidden']});overlay();}
70
+ // Playback is never delayed or restarted; only the pause/notice presentation fades.
71
+ const mediaFeedback=(event:Event)=>{const video=event.target as HTMLVideoElement;if(video.tagName!=='VIDEO')return;queueMicrotask(()=>{if(dead||!root.contains(video))return;const player=video.closest('.blu-video-player');const glass=player?.querySelector<HTMLElement>('.blu-video-player__pause-glass');const feedback=video.paused&&glass&&!glass.hidden&&getComputedStyle(glass).display!=='none'?glass:player?.querySelector<HTMLElement>('.blu-video-player__primary');if(feedback)run(feedback,[{opacity:.4},{opacity:1}],450,true);});};
72
+ for(const event of ['play','pause','waiting','loadeddata'])root.addEventListener(event,mediaFeedback,true);
73
+ return {capture,destroy(){if(dead)return;dead=true;observer.disconnect();overlayObserver.disconnect();cancelAnimationFrame(frame);surface?.destroy();for(const [el,state] of layers){state.revision++;state.restore();motion.cancel(el);}for(const el of active.keys())motion.cancel(el);active.clear();for(const event of ['click','change','input','keydown','pointerdown'])root.removeEventListener(event,capture,true);if(previousKind===null)root.removeAttribute('data-blu-content-motion');else root.setAttribute('data-blu-content-motion',previousKind);for(const event of ['play','pause','waiting','loadeddata'])root.removeEventListener(event,mediaFeedback,true);}};
74
+ }
@@ -0,0 +1,106 @@
1
+ import type { MotionController } from '../motion/controller.js';
2
+
3
+ export interface MotionRow { key: string; element: HTMLElement; cells: HTMLElement[]; cellKeys: string[]; }
4
+ interface Snapshot { row: MotionRow; top: number; left: number; width: number; height: number; text: string[]; widths: number[]; }
5
+ /** Presentation only. Data, selection, sorting, paging and keyboard handling stay with the component. */
6
+ export function enhanceDataRows(root: HTMLElement, motion: MotionController, read: () => MotionRow[]) {
7
+ let disposed = false, frame = 0, settle = 0, generation = 0, scrollUntil = 0, unsupportedRows = false;
8
+ const owned = new Set<HTMLElement>(), exits = new Map<string, { element: HTMLElement; timer: ReturnType<typeof setTimeout> }>();
9
+ const previousPosition = root.style.position;
10
+ if (getComputedStyle(root).position === 'static') root.style.position = 'relative';
11
+ const layer = document.createElement('div'); layer.className = 'blu-data-motion-exits'; layer.inert = true; layer.setAttribute('aria-hidden', 'true');
12
+ root.append(layer);
13
+ function snapshot() {
14
+ const rows = read();
15
+ const origin = root.getBoundingClientRect();
16
+ // Bound work to rendered rows. Never enumerate an entire virtual data set.
17
+ unsupportedRows = rows.length > 80 || new Set(rows.map(row => row.key)).size !== rows.length;
18
+ if (unsupportedRows) return new Map<string, Snapshot>();
19
+ return new Map(rows.map(row => {
20
+ const r = row.element.getBoundingClientRect();
21
+ // Read layout coordinates, excluding our in-flight visual translation.
22
+ const translate = owned.has(row.element) ? getComputedStyle(row.element).translate.split(/\s+/).map(Number.parseFloat) : [];
23
+ return [row.key, { row, top: r.top-origin.top-(translate[1]||0), left: r.left-origin.left-(translate[0]||0), width: r.width, height: r.height,
24
+ text: row.cells.map(cell => cell.textContent || ''), widths: row.cells.map(cell => cell.getBoundingClientRect().width) }];
25
+ }));
26
+ }
27
+ let before = snapshot();
28
+ function animate(el: HTMLElement, frames: Keyframe[], duration: number, fade = false) {
29
+ owned.add(el);
30
+ void motion.animate(el, frames, { duration, easing: 'ease-in-out' }, fade ? 'fade' : 'instant').then(() => { if (!el.getAnimations().length) owned.delete(el); });
31
+ }
32
+ function clearExits() {
33
+ for (const { element, timer } of exits.values()) { clearTimeout(timer); motion.cancel(element); owned.delete(element); element.remove(); }
34
+ exits.clear();
35
+ }
36
+ function capture() {
37
+ if (disposed) return;
38
+ if (frame) { cancelAnimationFrame(frame); frame = 0; reconcile(); }
39
+ before = snapshot();
40
+ }
41
+ function reconcile() {
42
+ if (disposed) return;
43
+ frame = 0;
44
+ const current = snapshot();
45
+ // Grid engines make additional style/scrollbar updates after sorting. Those
46
+ // must not cancel an entrance or reorder that is already playing.
47
+ const unchanged = current.size===before.size && [...current].every(([key,item])=>{
48
+ const old=before.get(key);return old && old.row.element===item.row.element && Math.abs(old.top-item.top)<1 && Math.abs(old.left-item.left)<1 && old.width===item.width && old.height===item.height && old.text.join('\u0000')===item.text.join('\u0000');
49
+ });
50
+ if(unchanged)return;
51
+ const revision = ++generation;
52
+ clearTimeout(settle); clearExits();
53
+ for (const el of owned) { motion.cancel(el); } owned.clear();
54
+ if (unsupportedRows || performance.now() < scrollUntil) { before = current; return; }
55
+ const bounds = root.getBoundingClientRect();
56
+ // Large page changes use a quiet entrance instead of dozens of crossing rows.
57
+ const modest = current.size <= 30 && before.size <= 30;
58
+ for (const [key, entry] of current) {
59
+ const old = before.get(key), el = entry.row.element;
60
+ if (!old) animate(el, [{ opacity: 0 }, { opacity: 1 }], 450, true);
61
+ else {
62
+ const dy = old.top - entry.top;
63
+ if (modest && Math.abs(dy) > 1 && Math.abs(dy) < bounds.height) {
64
+ // Individual translate composes with the grid engine's own transform.
65
+ animate(el, [{ translate: `0 ${dy}px` }, { translate: '0 0' }], 650);
66
+ }
67
+ entry.row.cells.forEach((cell, index) => {
68
+ const previousIndex = old.row.cellKeys.indexOf(entry.row.cellKeys[index]);
69
+ if (previousIndex >= 0 && old.text[previousIndex] !== entry.text[index]) {
70
+ const resting = getComputedStyle(cell).backgroundColor;
71
+ animate(cell, [{ backgroundColor: resting }, { backgroundColor: 'rgba(105, 88, 240, .18)', offset: .35 }, { backgroundColor: resting }], 1400);
72
+ }
73
+ });
74
+ }
75
+ }
76
+ if (modest && motion.mode !== 'off') for (const [key, old] of before) {
77
+ if (current.has(key) || !old.width || old.top + old.height < 0 || old.top > bounds.height) continue;
78
+ // Text-only decoration: no copied IDs, controls, handlers or live regions.
79
+ const ghost = document.createElement('div'); ghost.className = 'blu-data-motion-exit';
80
+ ghost.style.cssText = `left:${old.left}px;top:${old.top}px;width:${old.width}px;height:${old.height}px;grid-template-columns:${old.widths.map(w => w+'px').join(' ')};`;
81
+ for (const text of old.text) { const cell = document.createElement('span'); cell.textContent = text; ghost.append(cell); }
82
+ layer.append(ghost);
83
+ const remove = () => { if (exits.get(key)?.element !== ghost) return; clearTimeout(exits.get(key)!.timer); motion.cancel(ghost); owned.delete(ghost); ghost.remove(); exits.delete(key); };
84
+ exits.set(key, { element: ghost, timer: setTimeout(remove, (motion.resolveOptions?.({duration:450}).duration ?? 450) + 250) }); owned.add(ghost);
85
+ void motion.play(ghost, 'fade', { phase: 'exit', duration: 450, easing: 'ease-in-out' }).then(remove);
86
+ }
87
+ before = current;
88
+ settle = window.setTimeout(() => { if (!disposed && generation === revision) before = snapshot(); }, (motion.resolveOptions?.({duration:650}).duration ?? 650) + 50);
89
+ }
90
+ function changed(records: MutationRecord[]) {
91
+ if (records.every(r => layer.contains(r.target) || r.target === layer)) return;
92
+ if (!frame) frame = requestAnimationFrame(reconcile);
93
+ }
94
+ const observer = new MutationObserver(changed);
95
+ observer.observe(root, { subtree: true, childList: true, characterData: true, attributes: true, attributeFilter: ['row-index', 'row-id', 'style'] });
96
+ // Capture the current visual positions before the existing click handler rerenders.
97
+ root.addEventListener('click', capture, true);
98
+ const scrollPositions = new WeakMap<Element, string>();
99
+ for(const el of [root,...root.querySelectorAll('*')])scrollPositions.set(el,`${el.scrollLeft},${el.scrollTop}`);
100
+ const scrolling = (event:Event) => { const el=event.target;if(!(el instanceof Element))return;const position=`${el.scrollLeft},${el.scrollTop}`,previous=scrollPositions.get(el)||'0,0';scrollPositions.set(el,position);if(previous===position)return;scrollUntil = performance.now() + 150; if (frame) cancelAnimationFrame(frame); frame = 0; clearExits(); for (const el of owned) motion.cancel(el); owned.clear(); before = snapshot(); };
101
+ root.addEventListener('scroll', scrolling, true);
102
+ return {
103
+ capture,
104
+ destroy() { if (disposed) return; disposed = true; observer.disconnect(); cancelAnimationFrame(frame); clearTimeout(settle); clearExits(); for (const el of owned) motion.cancel(el); owned.clear(); root.removeEventListener('click', capture, true); root.removeEventListener('scroll', scrolling, true); layer.remove(); root.style.position = previousPosition; },
105
+ };
106
+ }
@@ -0,0 +1,52 @@
1
+ import type { MotionController } from '../motion/controller.js';
2
+ import { createPressFeedback } from '../motion/feedback.js';
3
+
4
+ /** Adds feedback to existing controls without replacing inputs or intercepting native events. */
5
+ export function enhanceField(root: HTMLElement, motion: MotionController) {
6
+ const previous = root.getAttribute('data-blu-component-motion'); root.setAttribute('data-blu-component-motion', 'field');
7
+ let destroyed = false, revision = 0, errorText = '', signature = '', height = root.getBoundingClientRect().height;
8
+ const buttons = new Map<HTMLButtonElement, ReturnType<typeof createPressFeedback>>();
9
+ const originalOverflow = root.style.overflow;
10
+ let nudgeTarget: HTMLElement | null = null;
11
+ function sync(initial = false) {
12
+ if (destroyed) return;
13
+ for (const [button, press] of buttons) if (!root.contains(button)) { press.destroy(); buttons.delete(button); }
14
+ for (const button of Array.from(root.querySelectorAll<HTMLButtonElement>('.blu-text__action'))) if (!buttons.has(button)) buttons.set(button, createPressFeedback(button, motion));
15
+ const error = root.querySelector<HTMLElement>('.blu-field__error'), next = error && !error.hidden ? error.textContent || '' : '';
16
+ const nextSignature = [root.textContent, next, root.querySelector('textarea')?.style.height].join('|');
17
+ if (!initial && nextSignature === signature) return;
18
+ signature = nextSignature;
19
+ const from = root.getBoundingClientRect().height;
20
+ motion.cancel(root); root.style.overflow = originalOverflow;
21
+ const to = root.getBoundingClientRect().height, own = ++revision;
22
+ if (!initial && height > 0 && to > 0 && Math.abs(height - to) > 1) {
23
+ root.style.overflow = 'visible';
24
+ void motion.animate(root, [{ height: from === to ? height + 'px' : from + 'px' }, { height: to + 'px' }], { duration: 280, easing: 'ease-in-out' }).finally(() => { if (!destroyed && own === revision) root.style.overflow = originalOverflow; });
25
+ }
26
+ height = to;
27
+ if (!initial && next && !errorText && error) {
28
+ void motion.play(error, 'fade', { duration: 350 });
29
+ nudgeTarget = root.querySelector<HTMLElement>('.blu-text, .blu-textarea, .blu-select__trigger, .blu-radio-group, .blu-choice');
30
+ if (nudgeTarget) void motion.animate(nudgeTarget, [
31
+ { transform: 'translateX(0)' }, { transform: 'translateX(-3px)', offset: .22 },
32
+ { transform: 'translateX(2px)', offset: .5 }, { transform: 'translateX(-1px)', offset: .75 },
33
+ { transform: 'translateX(0)' },
34
+ ], { duration: 550, easing: 'ease-in-out' });
35
+ }
36
+ if (!next && nudgeTarget) { motion.cancel(nudgeTarget); nudgeTarget = null; }
37
+ if (!next && error) motion.cancel(error);
38
+ errorText = next;
39
+ }
40
+ sync(true);
41
+ const observer = new MutationObserver(records => {
42
+ if (records.some(record => record.type === 'childList' || record.attributeName !== 'style' || (record.target as Element).matches('textarea'))) sync();
43
+ });
44
+ observer.observe(root, { subtree: true, childList: true, attributes: true, attributeFilter: ['hidden', 'aria-invalid', 'data-invalid', 'disabled', 'style'] });
45
+ return { sync: () => sync(), destroy() {
46
+ if (destroyed) return; destroyed = true; ++revision; observer.disconnect();
47
+ motion.cancel(root); root.style.overflow = originalOverflow; if (nudgeTarget) motion.cancel(nudgeTarget);
48
+ const error = root.querySelector<HTMLElement>('.blu-field__error'); if (error) motion.cancel(error);
49
+ for (const press of buttons.values()) press.destroy(); buttons.clear();
50
+ if (previous === null) root.removeAttribute('data-blu-component-motion'); else root.setAttribute('data-blu-component-motion', previous);
51
+ } };
52
+ }
@@ -0,0 +1,62 @@
1
+ import type { MotionController } from '../motion/controller.js';
2
+ import { preserveStyles } from '../motion/structure.js';
3
+
4
+ /** The existing Sidebar/Pagination still owns navigation, focus and events.
5
+ * This marker only follows aria-current; it never intercepts links or history. */
6
+ export function enhanceNavigation(root: HTMLElement, motion: MotionController, kind: 'sidebar' | 'pagination' | 'tabs') {
7
+ const win = root.ownerDocument.defaultView!, restore = preserveStyles(root, ['position']);
8
+ const originalKind = root.getAttribute('data-blu-navigation-motion'); root.setAttribute('data-blu-navigation-motion', kind);
9
+ const marker = root.ownerDocument.createElement('span');
10
+ marker.className = 'blu-motion-navigation-marker'; marker.dataset.kind = kind;
11
+ marker.setAttribute('aria-hidden', 'true'); marker.hidden = true;
12
+ root.style.position = 'relative'; root.append(marker);
13
+ let current: HTMLElement | null = null, key = '', geometry = '', dead = false, moving = false, generation = 0;
14
+ let range: HTMLElement | null = null, rangeText = '';
15
+ const stop = () => { ++generation; moving = false; motion.cancel(marker); };
16
+ function place(animate = true) {
17
+ if (dead) return;
18
+ // Sidebar renders replace its children, so keep our decoration outside that render.
19
+ if (!root.contains(marker)) root.append(marker);
20
+ const next = root.querySelector<HTMLElement>(kind === 'tabs' ? '[role=tab][aria-selected=true]' : '[aria-current="page"]')
21
+ || (kind === 'pagination' ? root.querySelector<HTMLElement>('.blu-pagination__current') : null);
22
+ if (current !== next) { if (current) resize.unobserve(current); current = next; if (current) resize.observe(current); }
23
+ if (!next || !next.getClientRects().length) { if (!marker.hidden) marker.hidden = true; key = ''; geometry = ''; stop(); return; }
24
+ const nextKey = next.dataset.navigationKey || next.getAttribute('aria-label') || next.textContent || '';
25
+ const from = marker.hidden ? null : marker.getBoundingClientRect();
26
+ const parent = root.getBoundingClientRect(), target = next.getBoundingClientRect();
27
+ const x = target.left - parent.left + root.scrollLeft - root.clientLeft;
28
+ const y = target.top - parent.top + root.scrollTop - root.clientTop;
29
+ const nextGeometry = [x, y, target.width, target.height].join(',');
30
+ if (nextGeometry === geometry && nextKey === key && !marker.hidden) return;
31
+ // A page-size chooser can finish its layout after the selected page changes.
32
+ // Continue from the visible position instead of cancelling that transition.
33
+ const continueMove = moving, changed = key !== nextKey;
34
+ stop(); if (marker.hidden) marker.hidden = false;
35
+ Object.assign(marker.style, {
36
+ left: (x + (kind === 'tabs' ? 6 : 0)) + 'px',
37
+ top: (y + (kind === 'sidebar' ? 8 : kind === 'tabs' ? target.height - 3 : 0)) + 'px',
38
+ width: (kind === 'sidebar' ? 3 : kind === 'tabs' ? Math.max(1, target.width - 12) : target.width) + 'px',
39
+ height: (kind === 'sidebar' ? Math.max(1, target.height - 16) : kind === 'tabs' ? 3 : target.height) + 'px',
40
+ });
41
+ if ((animate && changed || continueMove) && from) {
42
+ const to = marker.getBoundingClientRect();
43
+ const own = ++generation; moving = true;
44
+ void motion.animate(marker, [{ transform: `translate(${from.left-to.left}px,${from.top-to.top}px) scale(${from.width/to.width},${from.height/to.height})` }, { transform: 'none' }], { duration: kind === 'pagination' ? 750 : 450, easing: 'ease-in-out' }).finally(() => { if (own === generation) moving = false; });
45
+ }
46
+ if (kind === 'pagination') {
47
+ const nextRange = root.querySelector<HTMLElement>('.blu-pagination__range');
48
+ if (range !== nextRange && range) motion.cancel(range);
49
+ const text = nextRange?.textContent || '';
50
+ if (animate && rangeText && text !== rangeText && nextRange) void motion.animate(nextRange, [{ opacity: .35 }, { opacity: 1 }], { duration: 750, easing: 'ease-in-out' }, 'fade');
51
+ range = nextRange; rangeText = text;
52
+ }
53
+ key = nextKey; geometry = nextGeometry;
54
+ }
55
+ const resize = new ResizeObserver(() => place(false));
56
+ const observer = new MutationObserver(() => place());
57
+ const onScroll = () => { stop(); place(false); };
58
+ place(false); resize.observe(root);
59
+ observer.observe(root, { subtree: true, childList: true, attributes: true, attributeFilter: ['aria-current', 'aria-selected', 'hidden'] });
60
+ root.addEventListener('scroll', onScroll, true); win.addEventListener('resize', onScroll);
61
+ return { destroy() { if (dead) return; dead = true; observer.disconnect(); resize.disconnect(); stop(); if(range)motion.cancel(range); marker.remove(); restore(); if (originalKind === null) root.removeAttribute('data-blu-navigation-motion'); else root.setAttribute('data-blu-navigation-motion', originalKind); root.removeEventListener('scroll', onScroll, true); win.removeEventListener('resize', onScroll); } };
62
+ }
@@ -0,0 +1,67 @@
1
+ import type { MotionController } from '../motion/controller.js';
2
+
3
+ /** Observe the existing message stack. Its runtime still owns timers, pause on
4
+ * hover/focus, announcements and dismissal. Exits are decorative, inert copies. */
5
+ export function enhanceNotifications(root: HTMLElement, motion: MotionController) {
6
+ const previous = root.getAttribute('data-blu-component-motion'); root.setAttribute('data-blu-component-motion', 'notifications');
7
+ const cards = new Map<HTMLElement, { rect: DOMRect; opacity: string }>(), ghosts = new Set<HTMLElement>();
8
+ let dead = false, frame = 0, generation = 0;
9
+ const win = root.ownerDocument.defaultView!;
10
+ // Some engines emit focusin/out but do not support onfocusin/out properties.
11
+ // Forward only in those engines; the existing handlers own the timer state.
12
+ type FocusCard = HTMLElement & { onfocusin?: (event: FocusEvent) => void; onfocusout?: (event: FocusEvent) => void };
13
+ const forwardFocus = (event: FocusEvent) => {
14
+ const card = (event.target as Element).closest<FocusCard>('.blu-notifications > .blu-notification');
15
+ if (!card || !root.contains(card)) return;
16
+ if (event.type === 'focusin') card.onfocusin?.call(card, event); else card.onfocusout?.call(card, event);
17
+ };
18
+ const bridgeIn = !('onfocusin' in root), bridgeOut = !('onfocusout' in root);
19
+ if (bridgeIn) root.addEventListener('focusin', forwardFocus);
20
+ if (bridgeOut) root.addEventListener('focusout', forwardFocus);
21
+ const measure = () => { for (const [card, state] of cards) if (card.isConnected) { state.rect = card.getBoundingClientRect(); state.opacity = win.getComputedStyle(card).opacity; } };
22
+ const resize = new ResizeObserver(measure);
23
+ function exit(card: HTMLElement, state: { rect: DOMRect; opacity: string }) {
24
+ if (!state.rect.width || motion.mode === 'off' || root.closest('.blu-motion-off,[data-blu-motion=off]')) return;
25
+ const copy = card.cloneNode(true) as HTMLElement;
26
+ for (const el of [copy, ...copy.querySelectorAll<HTMLElement>('*')]) {
27
+ for (const attr of ['id','name','role','aria-live','aria-labelledby','aria-describedby','aria-controls','tabindex','autofocus']) el.removeAttribute(attr);
28
+ }
29
+ copy.dataset.bluExitVisual = 'notification'; copy.inert = true; copy.setAttribute('aria-hidden', 'true');
30
+ const rect = state.rect;
31
+ Object.assign(copy.style, { position: 'fixed', left: rect.left + 'px', top: rect.top + 'px', width: rect.width + 'px', height: rect.height + 'px', boxSizing: 'border-box', margin: '0', zIndex: '1300', pointerEvents: 'none', animation: 'none', opacity: state.opacity });
32
+ root.append(copy); ghosts.add(copy);
33
+ void motion.animate(copy, [{ opacity: state.opacity }, { opacity: 0 }], { phase: 'exit', duration: 450, easing: 'ease-in-out' }, 'fade').then(() => { copy.remove(); ghosts.delete(copy); });
34
+ }
35
+ function sync() {
36
+ if (dead) return;
37
+ const current = Array.from(root.querySelectorAll<HTMLElement>('.blu-notifications > .blu-notification'));
38
+ if (current.length === cards.size && current.every(card => cards.has(card))) return;
39
+ const own = ++generation; win.cancelAnimationFrame(frame);
40
+ const tasks: Promise<unknown>[] = [];
41
+ for (const [card, state] of cards) if (!current.includes(card)) { motion.cancel(card); resize.unobserve(card); cards.delete(card); exit(card, state); }
42
+ for (const card of current) {
43
+ const before = cards.get(card); motion.cancel(card);
44
+ const rect = card.getBoundingClientRect();
45
+ cards.set(card, { rect, opacity: win.getComputedStyle(card).opacity }); resize.observe(card);
46
+ if (!before) tasks.push(motion.play(card, 'slide-up', { duration: 450, easing: 'ease-out' }));
47
+ else {
48
+ const x = before.rect.left - rect.left, y = before.rect.top - rect.top;
49
+ if (Math.abs(x) > .5 || Math.abs(y) > .5 || Number(before.opacity) < .99) tasks.push(motion.animate(card, [{ transform: `translate(${x}px,${y}px)`, opacity: before.opacity }, { transform: 'none', opacity: 1 }], { duration: 450, easing: 'ease-in-out' }));
50
+ }
51
+ }
52
+ const track = () => { if (dead || own !== generation) return; measure(); frame = win.requestAnimationFrame(track); };
53
+ frame = win.requestAnimationFrame(track);
54
+ void Promise.all(tasks).then(() => { if (!dead && own === generation) { win.cancelAnimationFrame(frame); measure(); } });
55
+ }
56
+ const observer = new MutationObserver(sync); observer.observe(root, { childList: true, subtree: true }); sync();
57
+ win.addEventListener('resize', measure); win.addEventListener('scroll', measure, true);
58
+ return { destroy() {
59
+ if (dead) return; dead = true; ++generation; observer.disconnect(); resize.disconnect(); win.cancelAnimationFrame(frame);
60
+ win.removeEventListener('resize', measure); win.removeEventListener('scroll', measure, true);
61
+ if (bridgeIn) root.removeEventListener('focusin', forwardFocus);
62
+ if (bridgeOut) root.removeEventListener('focusout', forwardFocus);
63
+ for (const card of cards.keys()) motion.cancel(card); cards.clear();
64
+ for (const ghost of ghosts) { motion.cancel(ghost); ghost.remove(); } ghosts.clear();
65
+ if (previous === null) root.removeAttribute('data-blu-component-motion'); else root.setAttribute('data-blu-component-motion', previous);
66
+ } };
67
+ }
@@ -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
+ }