@yewdiel21/motion-kit 0.14.0 → 0.15.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 (154) hide show
  1. package/README.md +174 -155
  2. package/bin/motion-kit.mjs +3 -3
  3. package/dist/auto/index.d.ts +6 -3
  4. package/dist/auto/index.js +8 -2
  5. package/dist/auto/index.js.map +1 -1
  6. package/dist/{chunk-EY4BJKC4.js → chunk-6IRJYXJU.js} +2 -2
  7. package/dist/{chunk-CU6SXW3E.js → chunk-A6QQCF6V.js} +1309 -310
  8. package/dist/chunk-A6QQCF6V.js.map +1 -0
  9. package/dist/chunk-JXRS3OEA.js +3638 -0
  10. package/dist/chunk-JXRS3OEA.js.map +1 -0
  11. package/dist/css/components/add-to-cart.css +72 -0
  12. package/dist/css/components/bidirectional-slider.css +168 -0
  13. package/dist/css/components/data-table.css +121 -26
  14. package/dist/css/components/filter-builder.css +10 -15
  15. package/dist/css/components/heatmap-chart.css +391 -0
  16. package/dist/css/components/hover-card.css +8 -2
  17. package/dist/css/components/image-preview.css +90 -2
  18. package/dist/css/components/json-viewer.css +33 -1
  19. package/dist/css/components/nav.css +102 -102
  20. package/dist/css/components/orbit-menu.css +130 -0
  21. package/dist/css/components/pagination.css +10 -5
  22. package/dist/css/components/pie-chart.css +390 -0
  23. package/dist/css/components/pill-button.css +135 -0
  24. package/dist/css/components/radar-chart.css +370 -0
  25. package/dist/css/components/radial-chart.css +398 -0
  26. package/dist/css/components/sankey-chart.css +358 -0
  27. package/dist/css/components/scroll-island.css +111 -0
  28. package/dist/css/components/select.css +136 -0
  29. package/dist/css/components/toolbar.css +122 -0
  30. package/dist/css/components/tree-view.css +79 -0
  31. package/dist/css/components/voice-dots.css +70 -0
  32. package/dist/elements/index.d.ts +180 -7
  33. package/dist/elements/index.js +287 -6
  34. package/dist/elements/index.js.map +1 -1
  35. package/dist/{engine-Cn6IhE3z.d.ts → engine-5zRIssTO.d.ts} +11 -1
  36. package/dist/index.css +1832 -168
  37. package/dist/index.d.ts +4 -4
  38. package/dist/index.js +33 -3
  39. package/dist/index.js.map +1 -1
  40. package/dist/react/index.d.ts +117 -11
  41. package/dist/react/index.js +314 -13
  42. package/dist/react/index.js.map +1 -1
  43. package/dist/{reduced-BBUusld6.d.ts → reduced-6X_bns60.d.ts} +1 -1
  44. package/dist/{reveal-DEvp6U6T.d.ts → reveal-BfyKKsw4.d.ts} +97 -4
  45. package/dist/store-CA2Qcun_.d.ts +453 -0
  46. package/package.json +9 -16
  47. package/registry.json +1096 -948
  48. package/src/auto/auto-init.ts +6 -0
  49. package/src/components/add-to-cart/README.md +34 -0
  50. package/src/components/add-to-cart/add-to-cart.css +71 -0
  51. package/src/components/add-to-cart/element.ts +20 -0
  52. package/src/components/add-to-cart/engine.ts +206 -0
  53. package/src/components/add-to-cart/react.tsx +29 -0
  54. package/src/components/bidirectional-slider/README.md +46 -0
  55. package/src/components/bidirectional-slider/bidirectional-slider.css +167 -0
  56. package/src/components/bidirectional-slider/element.ts +42 -0
  57. package/src/components/bidirectional-slider/engine.ts +289 -0
  58. package/src/components/bidirectional-slider/react.tsx +45 -0
  59. package/src/components/chart/README.md +10 -0
  60. package/src/components/chart/element.ts +2 -0
  61. package/src/components/chart/engine.ts +50 -23
  62. package/src/components/chart/parts.ts +237 -0
  63. package/src/components/chart/react.tsx +2 -2
  64. package/src/components/chart/use-chart.ts +55 -0
  65. package/src/components/data-table/README.md +3 -3
  66. package/src/components/data-table/data-table.css +121 -26
  67. package/src/components/data-table/engine.ts +81 -29
  68. package/src/components/editable-table/engine.ts +67 -11
  69. package/src/components/filter-builder/engine.ts +33 -2
  70. package/src/components/filter-builder/filter-builder.css +10 -15
  71. package/src/components/heatmap-chart/README.md +48 -0
  72. package/src/components/heatmap-chart/element.ts +21 -0
  73. package/src/components/heatmap-chart/engine.ts +281 -0
  74. package/src/components/heatmap-chart/heatmap-chart.css +93 -0
  75. package/src/components/heatmap-chart/react.tsx +14 -0
  76. package/src/components/hover-card/engine.ts +30 -1
  77. package/src/components/hover-card/hover-card.css +8 -2
  78. package/src/components/image-preview/element.ts +1 -1
  79. package/src/components/image-preview/engine.ts +95 -15
  80. package/src/components/image-preview/image-preview.css +90 -2
  81. package/src/components/image-preview/react.tsx +3 -3
  82. package/src/components/json-viewer/engine.ts +11 -2
  83. package/src/components/json-viewer/json-viewer.css +33 -1
  84. package/src/components/nav/README.md +12 -10
  85. package/src/components/nav/element.ts +5 -5
  86. package/src/components/nav/engine.ts +21 -14
  87. package/src/components/nav/nav.css +102 -102
  88. package/src/components/nav/react.tsx +4 -4
  89. package/src/components/orbit-menu/README.md +46 -0
  90. package/src/components/orbit-menu/element.ts +27 -0
  91. package/src/components/orbit-menu/engine.ts +179 -0
  92. package/src/components/orbit-menu/orbit-menu.css +129 -0
  93. package/src/components/orbit-menu/react.tsx +44 -0
  94. package/src/components/pagination/README.md +1 -1
  95. package/src/components/pagination/engine.ts +50 -4
  96. package/src/components/pagination/pagination.css +10 -5
  97. package/src/components/pie-chart/README.md +48 -0
  98. package/src/components/pie-chart/element.ts +30 -0
  99. package/src/components/pie-chart/engine.ts +283 -0
  100. package/src/components/pie-chart/pie-chart.css +92 -0
  101. package/src/components/pie-chart/react.tsx +14 -0
  102. package/src/components/pill-button/README.md +46 -0
  103. package/src/components/pill-button/element.ts +16 -0
  104. package/src/components/pill-button/engine.ts +77 -0
  105. package/src/components/pill-button/pill-button.css +134 -0
  106. package/src/components/pill-button/react.tsx +32 -0
  107. package/src/components/radar-chart/README.md +48 -0
  108. package/src/components/radar-chart/element.ts +30 -0
  109. package/src/components/radar-chart/engine.ts +327 -0
  110. package/src/components/radar-chart/radar-chart.css +72 -0
  111. package/src/components/radar-chart/react.tsx +14 -0
  112. package/src/components/radial-chart/README.md +48 -0
  113. package/src/components/radial-chart/element.ts +30 -0
  114. package/src/components/radial-chart/engine.ts +290 -0
  115. package/src/components/radial-chart/radial-chart.css +100 -0
  116. package/src/components/radial-chart/react.tsx +14 -0
  117. package/src/components/sankey-chart/README.md +48 -0
  118. package/src/components/sankey-chart/element.ts +31 -0
  119. package/src/components/sankey-chart/engine.ts +401 -0
  120. package/src/components/sankey-chart/react.tsx +14 -0
  121. package/src/components/sankey-chart/sankey-chart.css +60 -0
  122. package/src/components/scroll-island/README.md +34 -0
  123. package/src/components/scroll-island/element.ts +16 -0
  124. package/src/components/scroll-island/engine.ts +252 -0
  125. package/src/components/scroll-island/react.tsx +26 -0
  126. package/src/components/scroll-island/scroll-island.css +110 -0
  127. package/src/components/select/README.md +34 -0
  128. package/src/components/select/element.ts +26 -0
  129. package/src/components/select/engine.ts +267 -0
  130. package/src/components/select/react.tsx +33 -0
  131. package/src/components/select/select.css +135 -0
  132. package/src/components/toolbar/README.md +46 -0
  133. package/src/components/toolbar/element.ts +30 -0
  134. package/src/components/toolbar/engine.ts +230 -0
  135. package/src/components/toolbar/react.tsx +34 -0
  136. package/src/components/toolbar/toolbar.css +121 -0
  137. package/src/components/tree-view/element.ts +1 -1
  138. package/src/components/tree-view/engine.ts +174 -0
  139. package/src/components/tree-view/react.tsx +7 -3
  140. package/src/components/tree-view/tree-view.css +79 -0
  141. package/src/components/voice-dots/README.md +48 -0
  142. package/src/components/voice-dots/element.ts +45 -0
  143. package/src/components/voice-dots/engine.ts +243 -0
  144. package/src/components/voice-dots/react.tsx +52 -0
  145. package/src/components/voice-dots/voice-dots.css +69 -0
  146. package/src/elements/index.ts +39 -0
  147. package/src/index.ts +16 -0
  148. package/src/motion/liquid.ts +14 -4
  149. package/src/react/index.ts +14 -1
  150. package/dist/chunk-6P4P7NU6.js +0 -1401
  151. package/dist/chunk-6P4P7NU6.js.map +0 -1
  152. package/dist/chunk-CU6SXW3E.js.map +0 -1
  153. package/dist/store-DjkTBX7f.d.ts +0 -195
  154. /package/dist/{chunk-EY4BJKC4.js.map → chunk-6IRJYXJU.js.map} +0 -0
@@ -147,9 +147,11 @@ const ICONS = {
147
147
  next: SVG('<path d="M9 6l6 6-6 6"/>'),
148
148
  close: SVG('<path d="M7 7l10 10M17 7L7 17"/>', 14),
149
149
  arrow: SVG('<path d="M12 19V5M6 11l6-6 6 6"/>', 12),
150
+ chevron: SVG('<path d="M7 10l5 5 5-5"/>', 14),
151
+ check: SVG('<path d="M5 12.5l4.5 4.5L19 7.5"/>', 16),
150
152
  };
151
153
  const CHECK =
152
- '<svg aria-hidden="true" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="3" stroke-linecap="round" stroke-linejoin="round"><path data-mk-dt-tick d="M5 12.5l4.5 4.5L19 7.5" pathLength="1"/><path data-mk-dt-dash d="M6 12h12"/></svg>';
154
+ '<svg aria-hidden="true" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="3.2" stroke-linecap="round" stroke-linejoin="round"><path data-mk-dt-tick d="M5.5 12l4 4L18.5 7.5" pathLength="1"/><path data-mk-dt-dash d="M6.5 12h11"/></svg>';
153
155
  const plural = (n: number, one: string, many: string) => `${n} ${n === 1 ? one : many}`;
154
156
  let sequence = 0;
155
157
 
@@ -343,21 +345,18 @@ export function createDataTable<T extends DataRow = DataRow>(root: HTMLElement |
343
345
 
344
346
  const foot = el('div', 'data-mk-dt-foot');
345
347
  const range = el('span', 'data-mk-dt-range');
346
- const sizeField = el('label', 'data-mk-dt-size');
348
+ const sizeField = el('div', 'data-mk-dt-size');
347
349
  sizeField.append(el('span', undefined, 'Filas'));
348
- const sizeSelect = el('select');
349
- for (const size of pageSizes) {
350
- const option = el('option', undefined, String(size));
351
- option.value = String(size);
352
- sizeSelect.append(option);
353
- }
354
- if (!pageSizes.includes(pageSize) && pageSize) {
355
- const option = el('option', undefined, String(pageSize));
356
- option.value = String(pageSize);
357
- sizeSelect.prepend(option);
358
- }
359
- sizeSelect.value = String(pageSize);
360
- sizeField.append(sizeSelect);
350
+ const sizeButton = el('button', 'data-mk-dt-size-button');
351
+ sizeButton.type = 'button';
352
+ sizeButton.setAttribute('aria-haspopup', 'menu');
353
+ sizeButton.setAttribute('aria-expanded', 'false');
354
+ const sizeValue = el('span', 'data-mk-dt-size-value', String(pageSize));
355
+ sizeButton.append(sizeValue);
356
+ sizeButton.insertAdjacentHTML('beforeend', ICONS.chevron);
357
+ sizeButton.setAttribute('aria-label', `Filas por página: ${pageSize}`);
358
+ sizeField.append(sizeButton);
359
+ const sizes = pageSize && !pageSizes.includes(pageSize) ? [pageSize, ...pageSizes] : [...pageSizes];
361
360
  const pager = el('nav', 'data-mk-dt-pager');
362
361
  pager.setAttribute('aria-label', 'Páginas');
363
362
  const previous = el('button', 'data-mk-dt-previous');
@@ -400,7 +399,7 @@ export function createDataTable<T extends DataRow = DataRow>(root: HTMLElement |
400
399
  const status = el('span');
401
400
  status.className = 'mk-sr-only';
402
401
  status.setAttribute('role', 'status');
403
- root.append(bar, active, scroll, emptyHost, foot, bulk, status);
402
+ root.append(bar, bulk, active, scroll, emptyHost, foot, status);
404
403
  scope.attr(root, 'data-mk-ready', '');
405
404
  scope.add(() => {
406
405
  for (const effect of effects) effect.kill();
@@ -610,15 +609,17 @@ export function createDataTable<T extends DataRow = DataRow>(root: HTMLElement |
610
609
  gsap.killTweensOf(bulk);
611
610
  if (show) {
612
611
  bulk.hidden = false;
613
- if (animate && !reduced()) keep(gsap.fromTo(bulk, { opacity: 0, y: 16, filter: 'blur(8px)' }, { opacity: 1, y: 0, filter: 'blur(0px)', duration: 0.35, ease: EASE.pop, clearProps: 'opacity,transform,filter' }));
612
+ if (animate && !reduced()) keep(gsap.fromTo(bulk, { opacity: 0, y: -8, filter: 'blur(8px)' }, { opacity: 1, y: 0, filter: 'blur(0px)', duration: 0.35, ease: EASE.pop, clearProps: 'opacity,transform,filter' }));
614
613
  } else if (animate && !reduced()) {
615
- keep(gsap.to(bulk, { opacity: 0, y: 16, filter: 'blur(6px)', duration: 0.22, ease: 'power2.in', onComplete: () => {
614
+ keep(gsap.to(bulk, { opacity: 0, y: -8, filter: 'blur(6px)', duration: 0.22, ease: 'power2.in', onComplete: () => {
616
615
  bulk.hidden = true;
617
616
  gsap.set(bulk, { clearProps: 'opacity,transform,filter' });
618
617
  } }));
619
618
  } else bulk.hidden = true;
620
619
  };
621
620
 
621
+ let slides = 0;
622
+ scope.add(() => scroll.style.removeProperty('overflow-x'));
622
623
  /** Draw the page; `motion`: how the rows change (glide, slide from a side, or nothing). */
623
624
  const render = (motion: 'reflow' | 'forward' | 'back' | 'none' = 'none') => {
624
625
  const all = loading ? [] : results();
@@ -645,7 +646,19 @@ export function createDataTable<T extends DataRow = DataRow>(root: HTMLElement |
645
646
  }),
646
647
  );
647
648
  } else if (motion === 'forward' || motion === 'back') {
648
- keep(gsap.fromTo(fresh, { opacity: 0, x: motion === 'forward' ? 24 : -24 }, { opacity: 1, x: 0, duration: 0.32, stagger: 0.015, ease: 'power2.out', clearProps: 'opacity,transform' }));
649
+ // Sliding rows stick out past the side: a table that fits its panel hides the overflow while they travel
650
+ // (no scrollbar blinks); a wider one, which already scrolls, fades them in place instead.
651
+ const fits = scroll.scrollWidth <= scroll.clientWidth + 1;
652
+ const token = ++slides;
653
+ if (fits) scroll.style.overflowX = 'hidden';
654
+ const release = () => {
655
+ if (token === slides) scroll.style.removeProperty('overflow-x');
656
+ };
657
+ keep(
658
+ gsap.fromTo(fresh, fits ? { opacity: 0, x: motion === 'forward' ? 24 : -24 } : { opacity: 0, y: 6 }, {
659
+ opacity: 1, x: 0, y: 0, duration: 0.32, stagger: 0.015, ease: 'power2.out', clearProps: 'opacity,transform', onComplete: release, onInterrupt: release,
660
+ }),
661
+ );
649
662
  } else if (wasLoading) keep(gsap.fromTo(fresh, { opacity: 0, y: 6 }, { opacity: 1, y: 0, duration: 0.35, stagger: 0.025, ease: 'power2.out', clearProps: 'opacity,transform' }));
650
663
  }
651
664
  options.onRender?.(body);
@@ -793,11 +806,7 @@ export function createDataTable<T extends DataRow = DataRow>(root: HTMLElement |
793
806
  keep(press(next));
794
807
  setPage(page + 1);
795
808
  });
796
- scope.on(sizeSelect, 'change', () => {
797
- pageSize = Number(sizeSelect.value) || 0;
798
- page = 0;
799
- render('reflow');
800
- });
809
+
801
810
  scope.on(clearButton, 'click', clearAll);
802
811
  scope.on(bulkClose, 'click', () => setSelection([]));
803
812
  scope.on(bulkAll, 'click', () => setSelection(results().map(idOf)));
@@ -821,11 +830,57 @@ export function createDataTable<T extends DataRow = DataRow>(root: HTMLElement |
821
830
  return dialog;
822
831
  };
823
832
 
833
+ const sizeDialog = panelDialog('size', 'Filas por página');
834
+ sizeDialog.className = 'mk-dialog mk-menu mk-dt-panel mk-dt-menu';
835
+ const sizeItems = sizes.map((size) => {
836
+ const item = el('button', 'data-mk-item');
837
+ item.type = 'button';
838
+ item.dataset.mkValue = String(size);
839
+ item.append(el('span', undefined, String(size)));
840
+ item.insertAdjacentHTML('beforeend', ICONS.check);
841
+ sizeDialog.append(item);
842
+ return item;
843
+ });
844
+ const paintSize = () => {
845
+ for (const item of sizeItems) {
846
+ const on = Number(item.dataset.mkValue) === pageSize;
847
+ item.setAttribute('role', 'menuitemradio');
848
+ item.setAttribute('aria-checked', String(on));
849
+ }
850
+ };
851
+ /** A new page size from the menu (animated) or the API. */
852
+ const changePageSize = (size: number, animate: boolean) => {
853
+ const next = Math.max(0, Math.floor(size));
854
+ const text = String(next);
855
+ if (animate && next !== pageSize && !reduced()) keep(rollDigits(sizeValue, text, next > pageSize ? 1 : -1, 0.3));
856
+ else sizeValue.textContent = text;
857
+ sizeButton.setAttribute('aria-label', `Filas por página: ${next}`);
858
+ pageSize = next;
859
+ page = 0;
860
+ paintSize();
861
+ render('reflow');
862
+ };
863
+ const sizeMenu = createMenu(sizeDialog, {
864
+ triggers: false,
865
+ // The menu marks every item a plain menuitem when it opens: these are a choice.
866
+ onOpen: paintSize,
867
+ onClose: () => sizeButton.setAttribute('aria-expanded', 'false'),
868
+ onSelect: (value) => changePageSize(Number(value) || 0, true),
869
+ });
870
+ paintSize();
871
+ scope.on(sizeDialog, 'mk:select', (event) => event.stopPropagation());
872
+ scope.add(() => sizeMenu.destroy());
873
+ scope.on(sizeButton, 'click', () => {
874
+ keep(press(sizeButton));
875
+ sizeButton.setAttribute('aria-expanded', 'true');
876
+ sizeMenu.open(sizeButton);
877
+ });
878
+
824
879
  let rowMenu: ReturnType<typeof createMenu> | null = null;
825
880
  let menuRow: T | null = null;
826
881
  if (options.actions?.length) {
827
882
  const dialog = panelDialog('menu', 'Acciones de la fila');
828
- dialog.className = 'mk-dialog mk-menu mk-dt-menu';
883
+ dialog.className = 'mk-dialog mk-menu mk-dt-panel mk-dt-menu';
829
884
  for (const action of options.actions) {
830
885
  const item = el('button', 'data-mk-item', action.label);
831
886
  item.type = 'button';
@@ -1005,10 +1060,7 @@ export function createDataTable<T extends DataRow = DataRow>(root: HTMLElement |
1005
1060
  sortBy,
1006
1061
  setPage,
1007
1062
  setPageSize(size) {
1008
- pageSize = Math.max(0, Math.floor(size));
1009
- sizeSelect.value = String(pageSize);
1010
- page = 0;
1011
- render('reflow');
1063
+ changePageSize(size, false);
1012
1064
  },
1013
1065
  setQuery(next) {
1014
1066
  query = next;
@@ -2,6 +2,7 @@ import { gsap, EASE, registerMotion } from '../../core/gsap';
2
2
  import { reduced } from '../../core/reduced';
3
3
  import { createScope } from '../../core/scope';
4
4
  import { inert, type Instance } from '../../core/types';
5
+ import { createMenu, type MenuInstance } from '../menu/engine';
5
6
  import { fromJson } from '../../core/dates';
6
7
  import { toTime } from '../../core/relative-time';
7
8
  import { press, rollDigits, shake } from '../../motion/micro';
@@ -395,18 +396,13 @@ export function createEditableTable<T extends DataRow = DataRow>(root: HTMLEleme
395
396
  setValue(id, column!.key, !value);
396
397
  return;
397
398
  }
398
- let editor: HTMLInputElement | HTMLSelectElement;
399
+ // A choice opens the kit's menu on the cell (like every other select of the kit), not a system list.
399
400
  if (kind === 'select') {
400
- const select = document.createElement('select');
401
- for (const option of column!.options ?? []) {
402
- const element = document.createElement('option');
403
- element.value = option.value;
404
- element.textContent = option.label;
405
- select.append(element);
406
- }
407
- select.value = String(value ?? '');
408
- editor = select;
409
- } else {
401
+ openChoices(cell, id, column!, String(value ?? ''));
402
+ return;
403
+ }
404
+ let editor: HTMLInputElement | HTMLSelectElement;
405
+ {
410
406
  const input = document.createElement('input');
411
407
  input.type = kind === 'number' ? 'text' : kind === 'date' ? 'date' : 'text';
412
408
  if (kind === 'number') input.inputMode = 'decimal';
@@ -479,6 +475,66 @@ export function createEditableTable<T extends DataRow = DataRow>(root: HTMLEleme
479
475
  };
480
476
  const finishEdit = (save: boolean) => editing?.done(save);
481
477
 
478
+ // ── Choices: one menu, filled with the column's options when a choice cell is edited ──────────────
479
+ const CHECK = '<svg aria-hidden="true" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round"><path d="M5 12.5l4.5 4.5L19 7.5"/></svg>';
480
+ let choices: { menu: MenuInstance; dialog: HTMLDialogElement } | null = null;
481
+ let choosing: { id: RowId; key: string; value: string } | null = null;
482
+ const markChoices = (dialog: HTMLElement) => {
483
+ for (const item of dialog.querySelectorAll<HTMLElement>('[data-mk-item]')) {
484
+ item.setAttribute('role', 'menuitemradio');
485
+ item.setAttribute('aria-checked', String(item.dataset.mkValue === choosing?.value));
486
+ }
487
+ };
488
+ const choiceMenu = () => {
489
+ if (choices) return choices;
490
+ const dialog = document.createElement('dialog');
491
+ dialog.className = 'mk-dialog mk-menu mk-select-menu';
492
+ dialog.dataset.mkEtChoices = '';
493
+ root.append(dialog);
494
+ scope.add(() => dialog.remove());
495
+ const menu = createMenu(dialog, {
496
+ triggers: false,
497
+ onOpen: () => markChoices(dialog),
498
+ onSelect: (value) => {
499
+ const target = choosing;
500
+ if (target && value !== target.value) setValue(target.id, target.key, value);
501
+ },
502
+ onClose: () => {
503
+ const target = choosing;
504
+ choosing = null;
505
+ // Focus comes back to the cell (drawn again after a change).
506
+ const fresh = target ? cellOf(target.id, target.key) : null;
507
+ if (fresh) focusCell(fresh, false);
508
+ },
509
+ });
510
+ // The menu's selection stays inside: listeners on the table hear the table's events.
511
+ scope.on(dialog, 'mk:select', (event) => event.stopPropagation());
512
+ scope.add(() => menu.destroy());
513
+ choices = { menu, dialog };
514
+ return choices;
515
+ };
516
+ const openChoices = (cell: HTMLTableCellElement, id: RowId, column: EditableColumn<T>, value: string) => {
517
+ const { menu, dialog } = choiceMenu();
518
+ dialog.setAttribute('aria-label', column.label);
519
+ dialog.replaceChildren(
520
+ ...(column.options ?? []).map((option) => {
521
+ const item = document.createElement('button');
522
+ item.type = 'button';
523
+ item.dataset.mkItem = '';
524
+ item.dataset.mkValue = option.value;
525
+ const text = document.createElement('span');
526
+ text.textContent = option.label;
527
+ item.append(text);
528
+ item.insertAdjacentHTML('beforeend', CHECK);
529
+ return item;
530
+ }),
531
+ );
532
+ choosing = { id, key: column.key, value };
533
+ markChoices(dialog);
534
+ dialog.style.setProperty('--mk-select-width', `${cell.offsetWidth}px`);
535
+ menu.open(cell);
536
+ };
537
+
482
538
  const moveFrom = (cell: HTMLTableCellElement, move: 'down' | 'right' | 'left' | 'up') => {
483
539
  const { row, column, rows, cells } = positionOf(cell);
484
540
  if (move === 'right' || move === 'left') {
@@ -7,6 +7,7 @@ import {
7
7
  type FilterField, type FilterGroup, type FilterNode, type FilterRule,
8
8
  } from '../../core/filter';
9
9
  import { blurIn, collapse, press, rollDigits, shake } from '../../motion/micro';
10
+ import { createSelect, type SelectInstance } from '../select/engine';
10
11
  import { slideIndicator } from '../../motion/indicator';
11
12
 
12
13
  /*
@@ -102,6 +103,28 @@ export function createFilterBuilder(root: HTMLElement | null, options: FilterBui
102
103
  root.dispatchEvent(new CustomEvent('mk:change', { bubbles: true, detail: { value: cloneFilter(model), summary: describeFilter(model, fields) } }));
103
104
  };
104
105
 
106
+ // ── Choices: every <select> is the kit's select (it opens the kit's menu), kept in step with the select itself, so
107
+ // the change handlers below read it as always. Each one lives as long as its row.
108
+ const selects = new Map<HTMLElement, SelectInstance>();
109
+ const choice = (select: HTMLSelectElement, label: string) => {
110
+ const box = el('span', { class: 'mk-select', 'data-mk-fb-select': '' });
111
+ box.append(select);
112
+ selects.set(box, createSelect(box, { label }));
113
+ return box;
114
+ };
115
+ /** The selects inside `container` go (before it is emptied or removed). */
116
+ const dropChoices = (container: Element) => {
117
+ for (const [box, select] of selects) {
118
+ if (!container.contains(box)) continue;
119
+ select.destroy();
120
+ selects.delete(box);
121
+ }
122
+ };
123
+ scope.add(() => {
124
+ for (const select of selects.values()) select.destroy();
125
+ selects.clear();
126
+ });
127
+
105
128
  // ── Value controls ───────────────────────────────────────────────────────
106
129
  const valueControl = (rule: FilterRule, index: 0 | 1) => {
107
130
  const field = fieldOf(rule.field);
@@ -111,6 +134,9 @@ export function createFilterBuilder(root: HTMLElement | null, options: FilterBui
111
134
  if (type === 'select') {
112
135
  control = el('select');
113
136
  control.append(el('option', { value: '' }, 'Elige…'), ...(field?.options ?? []).map((option) => el('option', { value: option.value }, option.label)));
137
+ control.dataset.mkFbValue = String(index);
138
+ control.value = current === null || current === undefined ? '' : String(current);
139
+ return choice(control, index === 1 ? 'Hasta' : operatorOf(field, rule.operator).arity === 2 ? 'Desde' : 'Valor');
114
140
  } else {
115
141
  control = el('input');
116
142
  control.type = type === 'number' ? 'text' : type === 'date' ? 'date' : 'text';
@@ -126,6 +152,7 @@ export function createFilterBuilder(root: HTMLElement | null, options: FilterBui
126
152
  const fillValues = (row: HTMLElement, rule: FilterRule) => {
127
153
  const host = row.querySelector<HTMLElement>('[data-mk-fb-values]')!;
128
154
  const { arity } = operatorOf(fieldOf(rule.field), rule.operator);
155
+ dropChoices(host);
129
156
  host.replaceChildren();
130
157
  if (arity === 0) return;
131
158
  host.append(valueControl(rule, 0));
@@ -147,7 +174,7 @@ export function createFilterBuilder(root: HTMLElement | null, options: FilterBui
147
174
  const values = el('span', { 'data-mk-fb-values': '' });
148
175
  const remove = el('button', { type: 'button', 'data-mk-fb-remove': '', 'aria-label': 'Quitar condición' });
149
176
  remove.innerHTML = CLOSE;
150
- row.append(field, operator, values, remove);
177
+ row.append(choice(field, 'Campo'), choice(operator, 'Operador'), values, remove);
151
178
  fillOperators(row, rule);
152
179
  fillValues(row, rule);
153
180
  return row;
@@ -195,6 +222,7 @@ export function createFilterBuilder(root: HTMLElement | null, options: FilterBui
195
222
  };
196
223
 
197
224
  const render = () => {
225
+ dropChoices(tree);
198
226
  tree.replaceChildren(groupBox(model, 0));
199
227
  placeIndicators(true);
200
228
  describe();
@@ -220,7 +248,7 @@ export function createFilterBuilder(root: HTMLElement | null, options: FilterBui
220
248
  parts.push(element);
221
249
  if (isGroup(node)) placeIndicators(true);
222
250
  keep(blurIn(parts, { from: 'rise', stagger: 0.04 }));
223
- element.querySelector<HTMLElement>('select, input')?.focus();
251
+ element.querySelector<HTMLElement>('[data-mk-select-trigger], input')?.focus();
224
252
  emit();
225
253
  };
226
254
 
@@ -236,6 +264,7 @@ export function createFilterBuilder(root: HTMLElement | null, options: FilterBui
236
264
  const box = boxOf(parent.id)!;
237
265
  const focusAfter = box.querySelector<HTMLElement>(':scope > [data-mk-fb-actions] button');
238
266
  const gone = () => {
267
+ dropChoices(element);
239
268
  element.remove();
240
269
  link?.remove();
241
270
  };
@@ -292,6 +321,8 @@ export function createFilterBuilder(root: HTMLElement | null, options: FilterBui
292
321
  setCombinator(group, next);
293
322
  option.parentElement!.querySelector<HTMLElement>(`[data-combinator="${next}"]`)!.focus();
294
323
  });
324
+ // The selects' own events stay inside: listeners on the builder hear the builder's mk:change.
325
+ scope.on(tree, 'mk:change', (event) => event.stopPropagation());
295
326
  scope.on(tree, 'change', (event) => {
296
327
  const target = event.target as HTMLElement;
297
328
  const row = target.closest<HTMLElement>('[data-mk-fb-rule]');
@@ -88,7 +88,13 @@ mk-filter-builder {
88
88
  min-width: 0;
89
89
  }
90
90
 
91
- [data-mk-fb-rule] :is(select, input) {
91
+ /* Choices are the kit's select (select.css draws the button); inputs share its look. */
92
+ [data-mk-fb-rule] .mk-select {
93
+ min-width: 8rem;
94
+ max-width: 100%;
95
+ }
96
+
97
+ [data-mk-fb-rule] input {
92
98
  box-sizing: border-box;
93
99
  min-width: 0;
94
100
  height: 2.5rem;
@@ -103,22 +109,11 @@ mk-filter-builder {
103
109
  transition: box-shadow 0.2s ease;
104
110
  }
105
111
 
106
- [data-mk-fb-rule] select {
107
- max-width: 100%;
108
- padding-right: 1.9rem;
109
- appearance: none;
110
- background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Cpath d='M6 9l6 6 6-6' fill='none' stroke='%23888' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E");
111
- background-position: right 0.55rem center;
112
- background-repeat: no-repeat;
113
- background-size: 1rem;
114
- cursor: pointer;
115
- }
116
-
117
112
  [data-mk-fb-rule] input {
118
113
  width: 9rem;
119
114
  }
120
115
 
121
- [data-mk-fb-rule] :is(select, input):focus-visible {
116
+ [data-mk-fb-rule] input:focus-visible {
122
117
  box-shadow: 0 0 0 3px color-mix(in srgb, var(--mk-accent) 35%, transparent);
123
118
  }
124
119
 
@@ -217,7 +212,7 @@ mk-filter-builder {
217
212
  }
218
213
 
219
214
  @media (max-width: 480px) {
220
- [data-mk-fb-rule] :is(select, input) {
215
+ [data-mk-fb-rule] :is(.mk-select, input) {
221
216
  flex: 1 1 8rem;
222
217
  }
223
218
  }
@@ -225,7 +220,7 @@ mk-filter-builder {
225
220
  @media (prefers-reduced-motion: reduce) {
226
221
  [data-mk-fb-combinator] [role='radio'],
227
222
  [data-mk-fb-actions] button,
228
- [data-mk-fb-rule] :is(select, input) {
223
+ [data-mk-fb-rule] input {
229
224
  transition: none;
230
225
  }
231
226
  }
@@ -0,0 +1,48 @@
1
+ # heatmap-chart
2
+
3
+ Calendario de actividad tipo GitHub con intensidad por día, tooltips y desplazamiento horizontal.
4
+
5
+ ## Instalación
6
+
7
+ ```tsx
8
+ import '@yewdiel21/motion-kit/css';
9
+ import { HeatmapChart } from '@yewdiel21/motion-kit/react';
10
+ ```
11
+
12
+ ```sh
13
+ npx @yewdiel21/motion-kit add heatmap-chart --react --elements
14
+ ```
15
+
16
+ ## React
17
+
18
+ ```tsx
19
+ <HeatmapChart end="2026-10-31" weeks={26} data={{ "2026-10-30": 4, "2026-10-31": 8 }} unit="sesiones" locale="es-MX" />
20
+ ```
21
+
22
+ Los callbacks del ejemplo (`ask`, `run`, `setTool`, `setVolume`) pertenecen a tu aplicación. React acepta `className`, `style` y atributos HTML. Los componentes con estado exponen `instanceRef` (excepto PillButton).
23
+
24
+ ## HTML / Astro
25
+
26
+ ```html
27
+ <mk-heatmap-chart end="2026-10-31" weeks="26" data='{"2026-10-30":4,"2026-10-31":8}'></mk-heatmap-chart>
28
+ <script type="module">
29
+ import { defineElements } from '@yewdiel21/motion-kit/elements';
30
+ defineElements(['mk-heatmap-chart']);
31
+ </script>
32
+ ```
33
+
34
+ En Astro usa un `<script>` procesado por Astro. En HTML directo el import requiere bundler o import map. También puedes usar `import { createHeatmapChart } from '@yewdiel21/motion-kit'` y llamar a la factory con el contenedor y las mismas opciones. Al desmontar, llama a `instance.destroy()`.
35
+
36
+ ## Opciones
37
+
38
+ data: {date, value}[] o mapa YYYY-MM-DD a valor. end: hoy. weeks: 53. firstDay: 1 (lunes). levels: 4. color: acento. locale, unit, legend. onSelect(date, value). Instancia: select(date | null), update(options), selected.
39
+
40
+ Todos los gráficos aceptan `label`, `emptyLabel` y `formatValue`. Se actualizan con `update(options)` sin desmontarse; `select(index | null)` y `selected` controlan la selección (en heatmap se usa una fecha). `onSelect` también emite `mk:select`.
41
+
42
+ Los nombres camelCase se escriben como atributos kebab-case. Los arrays y objetos se pasan como JSON. Los booleanos aceptan atributo vacío o `"false"`.
43
+
44
+ ## Estilos y accesibilidad
45
+
46
+ Variables: --mk-heatmap-color, --mk-heatmap-empty; --mk-chart-surface.
47
+
48
+ Respeta los colores del tema, foco visible y `prefers-reduced-motion`. Los gráficos incluyen una tabla accesible y selección con flechas, Home, End y Escape. Heatmap usa arriba/abajo para un día e izquierda/derecha para una semana. Toolbar usa flechas y Home/End; OrbitMenu cierra con Escape y devuelve el foco. Los controles conservan nombres accesibles; VoiceDots anuncia los estados sin leer cada fotograma.
@@ -0,0 +1,21 @@
1
+ import { MkElement, defineElement, type OptionType } from '../../elements/base';
2
+ import { createHeatmapChart, type HeatmapChartInstance, type HeatmapChartOptions } from './engine';
3
+
4
+ /** <mk-heatmap-chart></mk-heatmap-chart> */
5
+ export class MkHeatmapChartElement extends MkElement<HeatmapChartInstance> {
6
+ static options: Record<string, OptionType> = { data: 'json', end: 'string', weeks: 'number', firstDay: 'number', locale: 'string', levels: 'number', color: 'string', unit: 'string', label: 'string', legend: 'boolean', emptyLabel: 'string' };
7
+
8
+ protected create(options: Record<string, unknown>) {
9
+ return createHeatmapChart(this, options);
10
+ }
11
+
12
+ attributeChangedCallback(_name: string, previous: string | null, next: string | null) {
13
+ if (previous === next || !this.instance) return;
14
+ this.instance.update({ legend: undefined, color: undefined, ...this.options });
15
+ }
16
+
17
+ update(options: HeatmapChartOptions) { this.instance?.update(options); }
18
+ select(date: string | null) { this.instance?.select(date); }
19
+ }
20
+
21
+ export const defineHeatmapChart = () => defineElement('mk-heatmap-chart', MkHeatmapChartElement);