@yewdiel21/motion-kit 0.14.0 → 0.16.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 (156) hide show
  1. package/README.md +178 -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-NFZIX3IA.js} +2 -2
  7. package/dist/chunk-RBEKR5UZ.js +3660 -0
  8. package/dist/chunk-RBEKR5UZ.js.map +1 -0
  9. package/dist/{chunk-CU6SXW3E.js → chunk-SDQ776LG.js} +1596 -485
  10. package/dist/chunk-SDQ776LG.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/chart.css +7 -0
  14. package/dist/css/components/data-table.css +121 -26
  15. package/dist/css/components/filter-builder.css +10 -15
  16. package/dist/css/components/heatmap-chart.css +398 -0
  17. package/dist/css/components/hover-card.css +8 -2
  18. package/dist/css/components/image-preview.css +90 -2
  19. package/dist/css/components/json-viewer.css +33 -1
  20. package/dist/css/components/nav.css +102 -102
  21. package/dist/css/components/orbit-menu.css +130 -0
  22. package/dist/css/components/pagination.css +10 -5
  23. package/dist/css/components/pie-chart.css +397 -0
  24. package/dist/css/components/pill-button.css +135 -0
  25. package/dist/css/components/radar-chart.css +377 -0
  26. package/dist/css/components/radial-chart.css +412 -0
  27. package/dist/css/components/sankey-chart.css +365 -0
  28. package/dist/css/components/scroll-island.css +114 -0
  29. package/dist/css/components/select.css +136 -0
  30. package/dist/css/components/toolbar.css +122 -0
  31. package/dist/css/components/tree-view.css +79 -0
  32. package/dist/css/components/voice-dots.css +70 -0
  33. package/dist/elements/index.d.ts +180 -7
  34. package/dist/elements/index.js +293 -7
  35. package/dist/elements/index.js.map +1 -1
  36. package/dist/{engine-Cn6IhE3z.d.ts → engine-5zRIssTO.d.ts} +11 -1
  37. package/dist/index.css +1849 -168
  38. package/dist/index.d.ts +4 -4
  39. package/dist/index.js +33 -3
  40. package/dist/index.js.map +1 -1
  41. package/dist/react/index.d.ts +117 -11
  42. package/dist/react/index.js +320 -13
  43. package/dist/react/index.js.map +1 -1
  44. package/dist/{reduced-BBUusld6.d.ts → reduced-DQwVXQYb.d.ts} +1 -1
  45. package/dist/{reveal-DEvp6U6T.d.ts → reveal-DRpNO4KP.d.ts} +109 -4
  46. package/dist/store-Da3zSh8Z.d.ts +457 -0
  47. package/package.json +9 -16
  48. package/registry.json +1096 -948
  49. package/src/auto/auto-init.ts +6 -0
  50. package/src/components/add-to-cart/README.md +49 -0
  51. package/src/components/add-to-cart/add-to-cart.css +71 -0
  52. package/src/components/add-to-cart/element.ts +20 -0
  53. package/src/components/add-to-cart/engine.ts +206 -0
  54. package/src/components/add-to-cart/react.tsx +33 -0
  55. package/src/components/bidirectional-slider/README.md +46 -0
  56. package/src/components/bidirectional-slider/bidirectional-slider.css +167 -0
  57. package/src/components/bidirectional-slider/element.ts +42 -0
  58. package/src/components/bidirectional-slider/engine.ts +289 -0
  59. package/src/components/bidirectional-slider/react.tsx +45 -0
  60. package/src/components/chart/README.md +49 -24
  61. package/src/components/chart/chart.css +7 -0
  62. package/src/components/chart/element.ts +4 -1
  63. package/src/components/chart/engine.ts +153 -34
  64. package/src/components/chart/parts.ts +237 -0
  65. package/src/components/chart/react.tsx +2 -2
  66. package/src/components/chart/use-chart.ts +55 -0
  67. package/src/components/data-table/README.md +3 -3
  68. package/src/components/data-table/data-table.css +121 -26
  69. package/src/components/data-table/engine.ts +81 -29
  70. package/src/components/editable-table/engine.ts +67 -11
  71. package/src/components/filter-builder/engine.ts +33 -2
  72. package/src/components/filter-builder/filter-builder.css +10 -15
  73. package/src/components/heatmap-chart/README.md +48 -0
  74. package/src/components/heatmap-chart/element.ts +21 -0
  75. package/src/components/heatmap-chart/engine.ts +281 -0
  76. package/src/components/heatmap-chart/heatmap-chart.css +93 -0
  77. package/src/components/heatmap-chart/react.tsx +14 -0
  78. package/src/components/hover-card/engine.ts +30 -1
  79. package/src/components/hover-card/hover-card.css +8 -2
  80. package/src/components/image-preview/element.ts +1 -1
  81. package/src/components/image-preview/engine.ts +95 -15
  82. package/src/components/image-preview/image-preview.css +90 -2
  83. package/src/components/image-preview/react.tsx +3 -3
  84. package/src/components/json-viewer/engine.ts +11 -2
  85. package/src/components/json-viewer/json-viewer.css +33 -1
  86. package/src/components/nav/README.md +12 -10
  87. package/src/components/nav/element.ts +5 -5
  88. package/src/components/nav/engine.ts +21 -14
  89. package/src/components/nav/nav.css +102 -102
  90. package/src/components/nav/react.tsx +4 -4
  91. package/src/components/orbit-menu/README.md +46 -0
  92. package/src/components/orbit-menu/element.ts +27 -0
  93. package/src/components/orbit-menu/engine.ts +179 -0
  94. package/src/components/orbit-menu/orbit-menu.css +129 -0
  95. package/src/components/orbit-menu/react.tsx +44 -0
  96. package/src/components/pagination/README.md +1 -1
  97. package/src/components/pagination/engine.ts +50 -4
  98. package/src/components/pagination/pagination.css +10 -5
  99. package/src/components/pie-chart/README.md +48 -0
  100. package/src/components/pie-chart/element.ts +30 -0
  101. package/src/components/pie-chart/engine.ts +283 -0
  102. package/src/components/pie-chart/pie-chart.css +92 -0
  103. package/src/components/pie-chart/react.tsx +14 -0
  104. package/src/components/pill-button/README.md +46 -0
  105. package/src/components/pill-button/element.ts +16 -0
  106. package/src/components/pill-button/engine.ts +77 -0
  107. package/src/components/pill-button/pill-button.css +134 -0
  108. package/src/components/pill-button/react.tsx +32 -0
  109. package/src/components/radar-chart/README.md +48 -0
  110. package/src/components/radar-chart/element.ts +30 -0
  111. package/src/components/radar-chart/engine.ts +327 -0
  112. package/src/components/radar-chart/radar-chart.css +72 -0
  113. package/src/components/radar-chart/react.tsx +14 -0
  114. package/src/components/radial-chart/README.md +58 -0
  115. package/src/components/radial-chart/element.ts +30 -0
  116. package/src/components/radial-chart/engine.ts +317 -0
  117. package/src/components/radial-chart/radial-chart.css +107 -0
  118. package/src/components/radial-chart/react.tsx +14 -0
  119. package/src/components/sankey-chart/README.md +48 -0
  120. package/src/components/sankey-chart/element.ts +31 -0
  121. package/src/components/sankey-chart/engine.ts +401 -0
  122. package/src/components/sankey-chart/react.tsx +14 -0
  123. package/src/components/sankey-chart/sankey-chart.css +60 -0
  124. package/src/components/scroll-island/README.md +44 -0
  125. package/src/components/scroll-island/element.ts +16 -0
  126. package/src/components/scroll-island/engine.ts +274 -0
  127. package/src/components/scroll-island/react.tsx +26 -0
  128. package/src/components/scroll-island/scroll-island.css +113 -0
  129. package/src/components/select/README.md +34 -0
  130. package/src/components/select/element.ts +26 -0
  131. package/src/components/select/engine.ts +267 -0
  132. package/src/components/select/react.tsx +33 -0
  133. package/src/components/select/select.css +135 -0
  134. package/src/components/toolbar/README.md +46 -0
  135. package/src/components/toolbar/element.ts +30 -0
  136. package/src/components/toolbar/engine.ts +230 -0
  137. package/src/components/toolbar/react.tsx +34 -0
  138. package/src/components/toolbar/toolbar.css +121 -0
  139. package/src/components/tree-view/element.ts +1 -1
  140. package/src/components/tree-view/engine.ts +174 -0
  141. package/src/components/tree-view/react.tsx +7 -3
  142. package/src/components/tree-view/tree-view.css +79 -0
  143. package/src/components/voice-dots/README.md +48 -0
  144. package/src/components/voice-dots/element.ts +45 -0
  145. package/src/components/voice-dots/engine.ts +243 -0
  146. package/src/components/voice-dots/react.tsx +52 -0
  147. package/src/components/voice-dots/voice-dots.css +69 -0
  148. package/src/elements/index.ts +39 -0
  149. package/src/index.ts +16 -0
  150. package/src/motion/liquid.ts +14 -4
  151. package/src/react/index.ts +14 -1
  152. package/dist/chunk-6P4P7NU6.js +0 -1401
  153. package/dist/chunk-6P4P7NU6.js.map +0 -1
  154. package/dist/chunk-CU6SXW3E.js.map +0 -1
  155. package/dist/store-DjkTBX7f.d.ts +0 -195
  156. /package/dist/{chunk-EY4BJKC4.js.map → chunk-NFZIX3IA.js.map} +0 -0
@@ -0,0 +1,55 @@
1
+ 'use client';
2
+ import { useRef, type RefObject } from 'react';
3
+ import { useIsomorphicLayoutEffect, useLatest } from '../../react/use-recipe';
4
+ import type { Instance } from '../../core/types';
5
+
6
+ interface Updatable<O> extends Instance {
7
+ update(options: O): void;
8
+ }
9
+
10
+ /**
11
+ * The React side of the round and flow charts: the engine is created once and
12
+ * data changes are pushed with update(), so the chart glides instead of being
13
+ * drawn again. `data` is compared as JSON; callbacks always call the latest.
14
+ */
15
+ export function useChart<O extends object, I extends Updatable<O>>(
16
+ create: (root: HTMLElement | null, options: O) => I,
17
+ data: O,
18
+ callbacks: { formatValue?: (value: number) => string; onSelect?: (...args: never[]) => void },
19
+ instanceRef?: { current: I | null },
20
+ ): RefObject<HTMLDivElement | null> {
21
+ const ref = useRef<HTMLDivElement>(null);
22
+ const instance = useRef<I | null>(null);
23
+ const format = useLatest(callbacks.formatValue);
24
+ const select = useLatest(callbacks.onSelect);
25
+ const formatted = callbacks.formatValue !== undefined;
26
+ const key = JSON.stringify(data);
27
+ const latest = useLatest(data);
28
+ const options = (): O => ({
29
+ ...latest.current,
30
+ formatValue: formatted ? (value: number) => format.current?.(value) ?? String(value) : undefined,
31
+ onSelect: (...args: never[]) => (select.current as ((...a: never[]) => void) | undefined)?.(...args),
32
+ });
33
+ useIsomorphicLayoutEffect(() => {
34
+ const created = create(ref.current, options());
35
+ instance.current = created;
36
+ if (instanceRef) instanceRef.current = created;
37
+ return () => {
38
+ created.destroy();
39
+ instance.current = null;
40
+ if (instanceRef) instanceRef.current = null;
41
+ };
42
+ // Created once; data is pushed below.
43
+ // eslint-disable-next-line react-hooks/exhaustive-deps
44
+ }, []);
45
+ const first = useRef(true);
46
+ useIsomorphicLayoutEffect(() => {
47
+ if (first.current) {
48
+ first.current = false;
49
+ return;
50
+ }
51
+ instance.current?.update(options());
52
+ // eslint-disable-next-line react-hooks/exhaustive-deps
53
+ }, [key, formatted]);
54
+ return ref;
55
+ }
@@ -2,7 +2,7 @@
2
2
 
3
3
  Tabla avanzada con ordenamiento, búsqueda, filtros, selección, paginación, acciones por fila y columnas configurables. Al ordenar, las filas se deslizan a su nuevo lugar (el resorte de layout del kit, .62 s) y la flecha del encabezado gira. La búsqueda resalta lo que coincide (sin importar tildes ni mayúsculas) y las filas que quedan se reacomodan juntas. Los filtros usan `filter-builder` dentro de un panel que crece desde su botón: la tabla responde mientras editas y una frase dice qué está filtrado («Estado es Enviado y Monto mayor que 500»).
4
4
 
5
- Las casillas se dibujan solas y hacen pop; la del encabezado queda a medias cuando solo hay algunas filas elegidas. Al elegir filas, sube desde un blur una barra con el conteo (que rueda) y las acciones masivas, y ofrece elegir todos los resultados cuando la página entera está marcada. Al cambiar de página, las filas entran por el lado hacia el que vas. Las filas eliminadas salen deslizándose y las demás cierran el hueco. Mientras carga hay *skeletons* que brillan, y sin nada que mostrar aparece un estado vacío que explica por qué y ofrece limpiar los filtros.
5
+ Las casillas se dibujan solas y hacen pop; la del encabezado queda a medias cuando solo hay algunas filas elegidas. Una fila elegida se tiñe apenas y lleva un borde de acento a la izquierda, sin tapar sus datos. Al elegir filas, la barra con el conteo (que rueda) y las acciones masivas aparece con un blur en el lugar de la barra de herramientas, así nunca cubre filas, y ofrece elegir todos los resultados cuando la página entera está marcada. Al cambiar de página, las filas entran por el lado hacia el que vas; si la tabla cabe en su panel, el desborde se oculta mientras viajan para que no parpadee una barra de desplazamiento (si ya es más ancha, aparecen con un fundido). Las filas eliminadas salen deslizándose y las demás cierran el hueco. Mientras carga hay *skeletons* que brillan, y sin nada que mostrar aparece un estado vacío que explica por qué y ofrece limpiar los filtros.
6
6
 
7
7
  ## HTML / Astro
8
8
 
@@ -56,7 +56,7 @@ Unas `rows` nuevas entran en su sitio y se conservan el orden, los filtros y la
56
56
  | `rowKey` | `row-key` | `'id'` | Clave (o función) del id de cada fila. |
57
57
  | `caption` | `caption` | `'Tabla'` | Nombre accesible. |
58
58
  | `pageSize` | `page-size` | `10` | Filas por página (`0`: todas). |
59
- | `pageSizes` | `page-sizes` (JSON) | `[10, 25, 50]` | Opciones de filas por página. |
59
+ | `pageSizes` | `page-sizes` (JSON) | `[10, 25, 50]` | Opciones del menú «Filas» (un botón que abre un menú como Filtros y Columnas, con la opción actual marcada). |
60
60
  | `selectable` | `selectable` | `false` | Casillas para elegir filas. |
61
61
  | `search` | `search` | `true` | Campo de búsqueda (un texto es su marcador). |
62
62
  | `filters` | `filters` | `true` | Botón «Filtros» con `filter-builder`. |
@@ -80,4 +80,4 @@ Unas `rows` nuevas entran en su sitio y se conservan el orden, los filtros y la
80
80
 
81
81
  ## Accesibilidad
82
82
 
83
- Una tabla real con `caption`, encabezados `th scope="col"` con `aria-sort` y botones para ordenar; casillas con etiqueta («Seleccionar Ana Gómez»); el botón de acciones dice de qué fila es y abre un menú de WAI-ARIA; la barra de selección es un `toolbar` con nombre. Si la tabla es más ancha que su contenedor, se desplaza de lado dentro de una región enfocable. Un estado discreto anuncia el orden, los resultados, la página y la selección. Con movimiento reducido los cambios son inmediatos.
83
+ Una tabla real con `caption`, encabezados `th scope="col"` con `aria-sort` y botones para ordenar; casillas con etiqueta («Seleccionar Ana Gómez»); el botón de acciones dice de qué fila es y abre un menú de WAI-ARIA; «Filas» es un botón con `aria-haspopup="menu"` y sus opciones son `menuitemradio` con `aria-checked`; la barra de selección es un `toolbar` con nombre. Si la tabla es más ancha que su contenedor, se desplaza de lado dentro de una región enfocable. Un estado discreto anuncia el orden, los resultados, la página y la selección. Con movimiento reducido los cambios son inmediatos.
@@ -1,9 +1,11 @@
1
1
  /* data-table — a toolbar (a 12 px search on surface-3, 12 px tool buttons with an accent count), the filter sentence
2
2
  on accent-soft, then the table on a 20 px panel: a sticky muted header with sort buttons whose arrow turns, 3rem
3
- rows on hairlines, chosen rows tinted, 18 px checks that draw themselves, an 8 px row menu button and shimmering
4
- bones while loading; the foot with the range, rows per page and the pager; the bar of the chosen rows on inverse
5
- glass. Panels (columns, filters) are anchored .mk-dialog (dialog.css) surfaces with 20 px corners. The table
6
- scrolls sideways inside its panel when it is wider; --mk-dt-max-height lets it scroll down too. */
3
+ rows on hairlines, chosen rows softly tinted with an accent edge (their data stays readable), 18 px checks that
4
+ draw themselves, an 8 px row menu button and shimmering bones while loading; the foot with the range, the rows
5
+ per page menu and the pager; the bar of the chosen rows takes the toolbar's place, so it never covers a row.
6
+ Panels and menus (columns, filters, rows per page, row actions) are anchored .mk-dialog (dialog.css) surfaces with
7
+ 20 px corners and 8 px items. The table scrolls sideways inside its panel when it is wider; --mk-dt-max-height
8
+ lets it scroll down too. */
7
9
  .mk-data-table,
8
10
  mk-data-table {
9
11
  --mk-dt-max-height: none;
@@ -17,6 +19,8 @@ mk-data-table {
17
19
 
18
20
  /* ── The toolbar ─────────────────────────────────────────────────────────── */
19
21
  [data-mk-dt-bar] {
22
+ grid-row: 1;
23
+ grid-column: 1;
20
24
  display: flex;
21
25
  flex-wrap: wrap;
22
26
  align-items: center;
@@ -86,6 +90,7 @@ mk-data-table {
86
90
  }
87
91
 
88
92
  [data-mk-dt-tool]:focus-visible,
93
+ [data-mk-dt-size-button]:focus-visible,
89
94
  [data-mk-dt-sort]:focus-visible,
90
95
  [data-mk-dt-row-menu]:focus-visible,
91
96
  [data-mk-dt-pager] button:focus-visible,
@@ -223,8 +228,17 @@ mk-data-table {
223
228
  background: color-mix(in srgb, var(--mk-text) 3%, transparent);
224
229
  }
225
230
 
231
+ /* A chosen row: a light tint and an accent edge, never a fill that hides the data (on OLED the accent is white). */
226
232
  [data-mk-dt-table] tbody tr[data-selected] {
227
- background: color-mix(in srgb, var(--mk-accent-soft) 60%, transparent);
233
+ background: color-mix(in srgb, var(--mk-accent) 7%, transparent);
234
+ }
235
+
236
+ [data-mk-dt-table] tbody tr[data-selected]:hover {
237
+ background: color-mix(in srgb, var(--mk-accent) 11%, transparent);
238
+ }
239
+
240
+ [data-mk-dt-table] tbody tr[data-selected] > :first-child {
241
+ box-shadow: inset 3px 0 0 var(--mk-accent);
228
242
  }
229
243
 
230
244
  [data-mk-dt-table] td mark {
@@ -296,7 +310,7 @@ th[data-direction='desc'] [data-mk-dt-sort] svg {
296
310
  width: 1.125rem;
297
311
  height: 1.125rem;
298
312
  box-sizing: border-box;
299
- border: 1.5px solid var(--mk-outline);
313
+ border: 2px solid var(--mk-outline);
300
314
  border-radius: 5px;
301
315
  color: transparent;
302
316
  vertical-align: middle;
@@ -317,6 +331,9 @@ th[data-direction='desc'] [data-mk-dt-sort] svg {
317
331
  }
318
332
 
319
333
  [data-mk-dt-check] svg {
334
+ display: block;
335
+ width: 100%;
336
+ height: 100%;
320
337
  pointer-events: none;
321
338
  }
322
339
 
@@ -422,14 +439,35 @@ th[data-direction='desc'] [data-mk-dt-sort] svg {
422
439
  margin-left: auto;
423
440
  }
424
441
 
425
- [data-mk-dt-size] select {
442
+ [data-mk-dt-size-button] {
443
+ display: inline-flex;
444
+ align-items: center;
445
+ gap: 0.25rem;
426
446
  height: 2rem;
427
- padding: 0 0.5rem;
447
+ padding: 0 0.375rem 0 0.75rem;
428
448
  border: 1px solid var(--mk-outline-soft);
429
449
  border-radius: 8px;
430
450
  background: var(--mk-surface-1);
431
451
  color: var(--mk-text);
432
452
  font: inherit;
453
+ font-variant-numeric: tabular-nums;
454
+ cursor: pointer;
455
+ transition: background-color 0.15s;
456
+ }
457
+
458
+ [data-mk-dt-size-button]:hover,
459
+ [data-mk-dt-size-button][aria-expanded='true'] {
460
+ background: var(--mk-surface-2);
461
+ }
462
+
463
+ [data-mk-dt-size-button] svg {
464
+ flex: none;
465
+ color: var(--mk-muted);
466
+ transition: transform 0.3s cubic-bezier(0.38, 0.49, 0, 1.5);
467
+ }
468
+
469
+ [data-mk-dt-size-button][aria-expanded='true'] svg {
470
+ transform: rotate(180deg);
433
471
  }
434
472
 
435
473
  [data-mk-dt-pager] {
@@ -463,24 +501,28 @@ th[data-direction='desc'] [data-mk-dt-sort] svg {
463
501
  }
464
502
 
465
503
  /* ── The bar of the chosen rows ──────────────────────────────────────────── */
504
+ /* It shares the toolbar's grid cell and covers it while rows are chosen: the rows stay in view. */
466
505
  [data-mk-dt-bulk] {
467
- position: absolute;
468
- right: 0;
469
- bottom: 3.25rem;
470
- left: 0;
471
- z-index: 3;
506
+ position: relative;
507
+ z-index: 2;
508
+ grid-row: 1;
509
+ grid-column: 1;
472
510
  display: flex;
473
511
  flex-wrap: wrap;
474
512
  align-items: center;
513
+ align-self: stretch;
475
514
  gap: 0.25rem 0.5rem;
476
- width: max-content;
477
- max-width: calc(100% - 1rem);
478
- margin-inline: auto;
479
- padding: 0.375rem 0.375rem 0.375rem 1rem;
480
- border-radius: 20px;
481
- background: var(--mk-glass-inverse, var(--mk-text));
482
- box-shadow: 0 14px 32px -12px color-mix(in srgb, var(--mk-shadow-color, #000) 45%, transparent);
483
- color: var(--mk-on-glass-inverse, var(--mk-surface-1));
515
+ min-width: 0;
516
+ min-height: 2.5rem;
517
+ padding: 0.25rem 0.25rem 0.25rem 1rem;
518
+ border-radius: 12px;
519
+ background: var(--mk-surface-3);
520
+ color: var(--mk-text);
521
+ }
522
+
523
+ .mk-data-table:has(> [data-mk-dt-bulk]:not([hidden])) > [data-mk-dt-bar],
524
+ mk-data-table:has(> [data-mk-dt-bulk]:not([hidden])) > [data-mk-dt-bar] {
525
+ visibility: hidden;
484
526
  }
485
527
 
486
528
  [data-mk-dt-bulk-label] {
@@ -504,12 +546,17 @@ th[data-direction='desc'] [data-mk-dt-sort] svg {
504
546
  height: 2rem;
505
547
  padding: 0 0.75rem;
506
548
  border: 0;
507
- border-radius: 12px;
508
- background: color-mix(in srgb, currentColor 14%, transparent);
549
+ border-radius: 8px;
550
+ background: var(--mk-surface-1);
509
551
  color: inherit;
510
552
  font: inherit;
511
553
  font-weight: 500;
512
554
  cursor: pointer;
555
+ transition: background-color 0.15s;
556
+ }
557
+
558
+ [data-mk-dt-bulk] button:hover {
559
+ background: var(--mk-surface-2);
513
560
  }
514
561
 
515
562
  [data-mk-dt-bulk] button[data-danger] {
@@ -517,23 +564,31 @@ th[data-direction='desc'] [data-mk-dt-sort] svg {
517
564
  color: var(--mk-on-error, #fff);
518
565
  }
519
566
 
520
- [data-mk-dt-select-all] {
567
+ [data-mk-dt-bulk] [data-mk-dt-select-all] {
568
+ background: none;
521
569
  text-decoration: underline;
522
570
  text-underline-offset: 3px;
523
571
  }
524
572
 
525
- [data-mk-dt-bulk-close] {
573
+ /* The close button is a square with its cross in the middle (the bar's button padding must not reach it). */
574
+ [data-mk-dt-bulk] [data-mk-dt-bulk-close] {
526
575
  display: inline-grid;
527
576
  place-items: center;
528
577
  width: 2rem;
578
+ margin-left: auto;
529
579
  padding: 0;
530
580
  }
531
581
 
582
+ [data-mk-dt-bulk-close] svg {
583
+ display: block;
584
+ }
585
+
532
586
  /* ── Panels and the row menu ─────────────────────────────────────────────── */
533
587
  .mk-dialog.mk-dt-panel {
534
588
  --mk-dialog-radius: 20px;
535
589
  --mk-dialog-bg: var(--mk-surface-1);
536
- --mk-dialog-shadow: 0 16px 48px -12px color-mix(in srgb, var(--mk-shadow-color, #000) 35%, transparent);
590
+ /* A hairline keeps the panel apart from the table under it (on OLED both are near black). */
591
+ --mk-dialog-shadow: 0 0 0 1px var(--mk-outline-soft), 0 16px 48px -12px color-mix(in srgb, var(--mk-shadow-color, #000) 35%, transparent);
537
592
  gap: 0.5rem;
538
593
  width: max-content;
539
594
  min-width: min(14rem, calc(100vw - 32px));
@@ -574,6 +629,45 @@ th[data-direction='desc'] [data-mk-dt-sort] svg {
574
629
  background: var(--mk-surface-2);
575
630
  }
576
631
 
632
+ /* The row menu and the rows per page menu look like the panels: 20 px surface, 8 px items, the same hover. */
633
+ .mk-dialog.mk-dt-panel.mk-dt-menu {
634
+ gap: 0.125rem;
635
+ min-width: 9rem;
636
+ padding: 0.375rem;
637
+ -webkit-backdrop-filter: none;
638
+ backdrop-filter: none;
639
+ }
640
+
641
+ .mk-dialog.mk-dt-menu [data-mk-item] {
642
+ gap: 0.625rem;
643
+ min-height: 2.25rem;
644
+ padding: 0 0.625rem;
645
+ border-radius: 8px;
646
+ font-family: inherit;
647
+ font-size: 0.875rem;
648
+ font-weight: 500;
649
+ letter-spacing: normal;
650
+ }
651
+
652
+ .mk-dialog.mk-dt-menu [data-mk-item]:hover,
653
+ .mk-dialog.mk-dt-menu [data-mk-item]:focus-visible {
654
+ background: var(--mk-surface-2);
655
+ }
656
+
657
+ .mk-dialog.mk-dt-menu [data-mk-item] svg {
658
+ flex: none;
659
+ margin-left: auto;
660
+ opacity: 0;
661
+ }
662
+
663
+ .mk-dialog.mk-dt-menu [data-mk-item][aria-checked='true'] {
664
+ font-weight: 600;
665
+ }
666
+
667
+ .mk-dialog.mk-dt-menu [data-mk-item][aria-checked='true'] svg {
668
+ opacity: 1;
669
+ }
670
+
577
671
  .mk-dialog.mk-dt-menu [data-mk-item][data-danger] {
578
672
  color: var(--mk-error);
579
673
  }
@@ -584,6 +678,7 @@ th[data-direction='desc'] [data-mk-dt-sort] svg {
584
678
  }
585
679
 
586
680
  [data-mk-dt-sort] svg,
681
+ [data-mk-dt-size-button] svg,
587
682
  [data-mk-dt-check] {
588
683
  transition: none;
589
684
  }
@@ -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') {