@yewdiel21/motion-kit 0.15.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 (48) hide show
  1. package/README.md +4 -0
  2. package/dist/auto/index.d.ts +3 -3
  3. package/dist/auto/index.js +2 -2
  4. package/dist/{chunk-6IRJYXJU.js → chunk-NFZIX3IA.js} +2 -2
  5. package/dist/{chunk-JXRS3OEA.js → chunk-RBEKR5UZ.js} +29 -7
  6. package/dist/chunk-RBEKR5UZ.js.map +1 -0
  7. package/dist/{chunk-A6QQCF6V.js → chunk-SDQ776LG.js} +129 -17
  8. package/dist/chunk-SDQ776LG.js.map +1 -0
  9. package/dist/css/components/chart.css +7 -0
  10. package/dist/css/components/heatmap-chart.css +7 -0
  11. package/dist/css/components/pie-chart.css +7 -0
  12. package/dist/css/components/radar-chart.css +7 -0
  13. package/dist/css/components/radial-chart.css +16 -2
  14. package/dist/css/components/sankey-chart.css +7 -0
  15. package/dist/css/components/scroll-island.css +3 -0
  16. package/dist/elements/index.d.ts +3 -3
  17. package/dist/elements/index.js +11 -6
  18. package/dist/elements/index.js.map +1 -1
  19. package/dist/index.css +19 -2
  20. package/dist/index.d.ts +3 -3
  21. package/dist/index.js +3 -3
  22. package/dist/react/index.d.ts +9 -9
  23. package/dist/react/index.js +16 -10
  24. package/dist/react/index.js.map +1 -1
  25. package/dist/{reduced-6X_bns60.d.ts → reduced-DQwVXQYb.d.ts} +1 -1
  26. package/dist/{reveal-BfyKKsw4.d.ts → reveal-DRpNO4KP.d.ts} +12 -0
  27. package/dist/{store-CA2Qcun_.d.ts → store-Da3zSh8Z.d.ts} +5 -1
  28. package/package.json +1 -1
  29. package/src/components/add-to-cart/README.md +49 -34
  30. package/src/components/add-to-cart/react.tsx +4 -0
  31. package/src/components/chart/README.md +49 -34
  32. package/src/components/chart/chart.css +7 -0
  33. package/src/components/chart/element.ts +2 -1
  34. package/src/components/chart/engine.ts +105 -13
  35. package/src/components/chart/react.tsx +2 -2
  36. package/src/components/radial-chart/README.md +10 -0
  37. package/src/components/radial-chart/element.ts +2 -2
  38. package/src/components/radial-chart/engine.ts +33 -6
  39. package/src/components/radial-chart/radial-chart.css +9 -2
  40. package/src/components/radial-chart/react.tsx +2 -2
  41. package/src/components/scroll-island/README.md +44 -34
  42. package/src/components/scroll-island/element.ts +1 -1
  43. package/src/components/scroll-island/engine.ts +25 -3
  44. package/src/components/scroll-island/react.tsx +3 -3
  45. package/src/components/scroll-island/scroll-island.css +3 -0
  46. package/dist/chunk-A6QQCF6V.js.map +0 -1
  47. package/dist/chunk-JXRS3OEA.js.map +0 -1
  48. /package/dist/{chunk-6IRJYXJU.js.map → chunk-NFZIX3IA.js.map} +0 -0
@@ -1,4 +1,4 @@
1
- import { bY as MenuOptions, bX as MenuInstance, bd as DialogOptions, bb as DialogInstance, cv as RevealSpeed } from './reveal-BfyKKsw4.js';
1
+ import { bY as MenuOptions, bX as MenuInstance, bd as DialogOptions, bb as DialogInstance, cv as RevealSpeed } from './reveal-DRpNO4KP.js';
2
2
  import { I as Instance } from './types-BkS_raL8.js';
3
3
  import { SplitText } from 'gsap/SplitText';
4
4
 
@@ -780,6 +780,16 @@ interface ChartOptions {
780
780
  points?: ChartPoint[];
781
781
  /** Bars only: stack the series. */
782
782
  stacked?: boolean;
783
+ /** Rounded ends (default) or a full capsule, including stacked columns. */
784
+ barStyle?: 'rounded' | 'pill';
785
+ /** Show the available capacity behind each column. */
786
+ barTrack?: boolean;
787
+ /** Solid (default), gradient, or a gradient selection over muted striped columns. */
788
+ barFill?: 'solid' | 'gradient' | 'spotlight';
789
+ /** Maximum column width in px (default 24; pill demos use 40). */
790
+ barWidth?: number;
791
+ /** Smooth monotone curves preserve the data's extrema (default linear). */
792
+ curve?: 'linear' | 'smooth';
783
793
  formatValue?: (value: number) => string;
784
794
  /** Accessible name (and table caption). */
785
795
  label?: string;
@@ -2102,6 +2112,8 @@ interface ScrollIslandOptions {
2102
2112
  reach?: number;
2103
2113
  /** ms the bar stays after scrolling stops (default 900). */
2104
2114
  idle?: number;
2115
+ /** Keep the thin bar visible while idle (default false). */
2116
+ persistent?: boolean;
2105
2117
  /** Follow the page's scroll (default: when the root is <html> or <body>). */
2106
2118
  page?: boolean;
2107
2119
  /** The current section changed (its heading's text, or null before the first). */
@@ -1,6 +1,6 @@
1
1
  import { I as Instance } from './types-BkS_raL8.js';
2
2
  import { Flip } from 'gsap/Flip';
3
- import { a3 as RevealHandle, a4 as DateInput } from './reveal-BfyKKsw4.js';
3
+ import { a3 as RevealHandle, a4 as DateInput } from './reveal-DRpNO4KP.js';
4
4
 
5
5
  interface ThemePickerOptions {
6
6
  /** Theme ids to show, in order. Default: visible presets, or all with showHidden. */
@@ -301,6 +301,10 @@ interface RadialChartOptions {
301
301
  size?: number;
302
302
  /** Ring thickness in px (default: what fits, up to 16). */
303
303
  thickness?: number;
304
+ /** Discrete sectors per ring (0: continuous, default). */
305
+ segments?: number;
306
+ /** Show values (default) or progress toward the goals in the middle. */
307
+ center?: 'value' | 'percent';
304
308
  label?: string;
305
309
  /** The middle when nothing is chosen (default 'Total'). */
306
310
  totalLabel?: string;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@yewdiel21/motion-kit",
3
- "version": "0.15.0",
3
+ "version": "0.16.0",
4
4
  "description": "Motion toolkit for vanilla, Astro and React: GSAP recipes, accessible components, 19 themes, CSS controls and optional WebGL effects.",
5
5
  "type": "module",
6
6
  "license": "UNLICENSED",
@@ -1,34 +1,49 @@
1
- # add-to-cart
2
-
3
- Qué hace, en una frase.
4
-
5
- Origen: archivo del kit y parámetros (tiempos, curvas).
6
-
7
- ## HTML / Astro
8
-
9
- ```html
10
- <mk-add-to-cart></mk-add-to-cart>
11
- <script>
12
- import { defineElements } from '@yewdiel21/motion-kit/elements';
13
- defineElements(['mk-add-to-cart']);
14
- </script>
15
- ```
16
-
17
- ## React
18
-
19
- ```tsx
20
- <AddToCart />
21
- ```
22
-
23
- ## Opciones
24
-
25
- | Opción | Atributo | Por defecto | Qué hace |
26
- |---|---|---|---|
27
-
28
- ## Variables CSS
29
-
30
- `--mk-add-to-cart-radius`
31
-
32
- ## Accesibilidad
33
-
34
- Roles, foco y teclado. Con movimiento reducido…
1
+ # add-to-cart
2
+
3
+ El producto se empaca en una caja y vuela hasta un carrito; al llegar, el contador cambia y el carrito responde con un pulso. Cada clic tiene su propio vuelo. Con movimiento reducido, el contador se actualiza al instante.
4
+
5
+ ## React
6
+
7
+ ```tsx
8
+ import '@yewdiel21/motion-kit/css';
9
+ import { AddToCart } from '@yewdiel21/motion-kit/react';
10
+
11
+ <div id="my-cart">Carrito <span data-mk-cart-count>0</span></div>
12
+ <article data-mk-cart-item>
13
+ <img src="/product.webp" alt="Producto" data-mk-cart-image />
14
+ <AddToCart target="#my-cart" onLand={count => console.log(count)}>
15
+ <button type="button">Agregar al carrito</button>
16
+ </AddToCart>
17
+ </article>
18
+ ```
19
+
20
+ **El botón es obligatorio.** Para ver el recorrido necesitas un destino visible y una imagen. Usa un destino único por demo o carrito para no mezclar instancias. El componente anima la interfaz; la aplicación gestiona el carrito y el inventario reales.
21
+
22
+ ## HTML / Astro
23
+
24
+ ```html
25
+ <div id="my-cart">Carrito <span data-mk-cart-count>0</span></div>
26
+ <article data-mk-cart-item>
27
+ <img src="/product.webp" alt="Producto" data-mk-cart-image>
28
+ <mk-add-to-cart target="#my-cart"><button type="button">Agregar</button></mk-add-to-cart>
29
+ </article>
30
+ <script>
31
+ import { defineElements } from '@yewdiel21/motion-kit/elements';
32
+ defineElements(['mk-add-to-cart']);
33
+ </script>
34
+ ```
35
+
36
+ En vanilla puedes llamar a `createAddToCart(root, options)`. `destroy()` limpia los vuelos y listeners.
37
+
38
+ ## Opciones y eventos
39
+
40
+ - `target`: HTMLElement o selector del carrito; por defecto busca `[data-mk-cart-target]` visible.
41
+ - `image`: HTMLElement o selector; por defecto `[data-mk-cart-image]` dentro del producto.
42
+ - `quantity`: unidades por clic, 1 por defecto.
43
+ - `count`: contador inicial; en React también sincroniza cambios posteriores.
44
+ - `label`: anuncio al llegar, por defecto “Añadido al carrito”.
45
+ - `onAdd(count)`: al pulsar; `onLand(count)`: al terminar el vuelo.
46
+ - Eventos DOM: `mk:add` y `mk:land`, con `detail.count`.
47
+ - Instancia: `add()`, `setCount(count)`, `count`, `destroy()`; React expone `instanceRef`.
48
+
49
+ El estado final se anuncia con `role="status"`. Usa un botón nativo con nombre accesible. Los vuelos son decorativos, no capturan el puntero y se eliminan al desmontar.
@@ -12,15 +12,19 @@ export function AddToCart({ target, image, quantity, count, label, onAdd, onLand
12
12
  const ref = useRef<HTMLDivElement>(null);
13
13
  const add = useLatest(onAdd);
14
14
  const landed = useLatest(onLand);
15
+ const instance = useRef<AddToCartInstance | null>(null);
15
16
  useIsomorphicLayoutEffect(() => {
16
17
  const created = createAddToCart(ref.current, { target, image, quantity, count, label, onAdd: (n) => add.current?.(n), onLand: (n) => landed.current?.(n) });
18
+ instance.current = created;
17
19
  if (instanceRef) instanceRef.current = created;
18
20
  return () => {
19
21
  created.destroy();
22
+ instance.current = null;
20
23
  if (instanceRef) instanceRef.current = null;
21
24
  };
22
25
  // eslint-disable-next-line react-hooks/exhaustive-deps
23
26
  }, [typeof target === 'string' ? target : null, typeof image === 'string' ? image : null, quantity, label]);
27
+ useIsomorphicLayoutEffect(() => { if (count !== undefined) instance.current?.setCount(count); }, [count]);
24
28
  return (
25
29
  <div {...props} ref={ref} className={['mk-add-to-cart', className].filter(Boolean).join(' ')}>
26
30
  {children}
@@ -1,34 +1,49 @@
1
- # chart
2
-
3
- Desde 0.15 también puedes mezclar series con `type="composed"`. Cada serie declara `type: 'line' | 'bar' | 'area'`. Un área acepta `lowerValues` para rellenar el intervalo entre la frontera inferior y `values`; los huecos `null` interrumpen el relleno. La tabla y el tooltip muestran ambos límites.
4
-
5
- ```tsx
6
- <Chart type="composed" labels={['Lun', 'Mar', 'Mié']} series={[
7
- { name: 'Ventas', type: 'bar', values: [20, 32, 28] },
8
- { name: 'Meta', type: 'line', values: [25, 30, 35] },
9
- { name: 'Rango', type: 'area', values: [24, 36, 32], lowerValues: [16, 26, 22] },
10
- ]} axis endLabel={false} />
11
- ```
12
-
13
- Gráfica SVG reutilizable de línea, área y barras (agrupadas o apiladas) sin motor canvas ni dependencias. Sigue las gráficas de métricas de Melon Mind: base en cero, línea de 2 px, área que se desvanece de .25 a 0, barras de hasta 24 px con extremo redondeado de 4 px y huecos de 2 px, rejilla fina y tooltip con superficie invertida.
14
-
15
- ```html
16
- <mk-chart class="mk-chart" type="bar" stacked label="Sesiones por semana"
17
- labels="S1,S2,S3,S4"
18
- series='[{"name":"Ana","values":[4,6,5,7]},{"name":"Luis","values":[2,3,4,1]}]'></mk-chart>
19
- ```
20
-
21
- ```ts
22
- const chart = createChart(root, { type: 'area', labels: days, series: [{ name: 'Ingresos', values }], formatValue: money, label: 'Ingresos de octubre' });
23
- chart.update({ series: [{ name: 'Ingresos', values: next }] });
24
- ```
25
-
26
- React: `<Chart type="line" labels={days} series={series} formatValue={money} label="Ingresos" onSelect={setDay} />`.
27
-
28
- Datos: `labels` + `series: ChartSeries[]` (`{name, values: (number | null)[], color?, dash?}`; `dash` es un patrón SVG para líneas, p. ej. `'5 5'` para un ritmo o `'1 6'` para una proyección) o `points: ChartPoint[]` (`{label, value}`) para una serie. `null` es un hueco sin dato: la línea se corta y un punto aislado se muestra con marcador. Opciones: `type='line'|'area'|'bar'`, `stacked` (barras), `formatValue(n)` (ejes, tooltip y tabla), `label` (nombre accesible y título de la tabla), `height=240`, `emptyLabel`, `legend` (por defecto con dos o más series), `axis` (valores y líneas guía en el eje y; por defecto sólo la línea base), `endLabel` (líneas: el último valor de la primera serie en una píldora al final; por defecto `true`), `tooltip` (por defecto `true`; con `false` la gráfica deja la guía y los marcadores y la tarjeta la pone `chart-tooltip`), `onSelect(index | null)`. API: `update(options)` (combina y redibuja), `select(index | null)`, `selected`, `destroy()`. Evento `mk:select` con `{index, label, values, x, y, series}`: `x`/`y` son el punto señalado en píxeles del área de la gráfica y `series` trae `{name, color, value, previous}` por serie.
29
-
30
- Puntero y toque eligen el punto más cercano (el toque mantiene la selección; el ratón la quita al salir). Con foco en la gráfica, flechas/Inicio/Fin recorren los valores y Escape los oculta; un estado educado anuncia el valor elegido. La gráfica tiene un resumen como nombre accesible y una tabla para lectores de pantalla con todos los valores (y el total en barras apiladas). Sin datos muestra `emptyLabel`.
31
-
32
- Movimiento: el primer dibujo traza la línea de izquierda a derecha (barras: crecen desde la base) y cada `update` desliza los valores nuevos desde los anteriores; el tooltip aparece con un pop de cristal y se desliza entre puntos. Con movimiento reducido todo cambia sin transición.
33
-
34
- Colores: una sola serie es monocroma (`--mk-chart-mono`, el color del texto, al estilo de los indicadores de Melon Mind); con varias, cada serie usa su `color` o la ranura `--mk-chart-N` en orden fijo (paleta comprobada para daltonismo, con pasos propios en oscuro); a partir de la novena serie todas comparten `--mk-chart-other`: agrúpalas en «Otros». Los textos usan los colores de texto del tema; el color identifica la marca. Se redimensiona con su contenedor (ResizeObserver) y `destroy()` quita todo lo creado. Variables: `--mk-chart-mono`, `--mk-chart-1…8`, `--mk-chart-other`, `--mk-chart-surface` (anillo de marcadores), `--mk-chart-grid`.
1
+ # chart
2
+
3
+ Desde 0.15 también puedes mezclar series con `type="composed"`. Cada serie declara `type: 'line' | 'bar' | 'area'`. Un área acepta `lowerValues` para rellenar el intervalo entre la frontera inferior y `values`; los huecos `null` interrumpen el relleno. La tabla y el tooltip muestran ambos límites.
4
+
5
+ ```tsx
6
+ <Chart type="composed" labels={['Lun', 'Mar', 'Mié']} series={[
7
+ { name: 'Ventas', type: 'bar', values: [20, 32, 28] },
8
+ { name: 'Meta', type: 'line', values: [25, 30, 35] },
9
+ { name: 'Rango', type: 'area', values: [24, 36, 32], lowerValues: [16, 26, 22] },
10
+ ]} axis endLabel={false} />
11
+ ```
12
+
13
+ Gráfica SVG reutilizable de línea, área y barras (agrupadas o apiladas) sin motor canvas ni dependencias. Sigue las gráficas de métricas de Melon Mind: base en cero, línea de 2 px, área que se desvanece de .25 a 0, barras de hasta 24 px con extremo redondeado de 4 px y huecos de 2 px, rejilla fina y tooltip con superficie invertida.
14
+
15
+ ```html
16
+ <mk-chart class="mk-chart" type="bar" stacked label="Sesiones por semana"
17
+ labels="S1,S2,S3,S4"
18
+ series='[{"name":"Ana","values":[4,6,5,7]},{"name":"Luis","values":[2,3,4,1]}]'></mk-chart>
19
+ ```
20
+
21
+ ```ts
22
+ const chart = createChart(root, { type: 'area', labels: days, series: [{ name: 'Ingresos', values }], formatValue: money, label: 'Ingresos de octubre' });
23
+ chart.update({ series: [{ name: 'Ingresos', values: next }] });
24
+ ```
25
+
26
+ React: `<Chart type="line" labels={days} series={series} formatValue={money} label="Ingresos" onSelect={setDay} />`.
27
+
28
+ Datos: `labels` + `series: ChartSeries[]` (`{name, values: (number | null)[], color?, dash?}`; `dash` es un patrón SVG para líneas, p. ej. `'5 5'` para un ritmo o `'1 6'` para una proyección) o `points: ChartPoint[]` (`{label, value}`) para una serie. `null` es un hueco sin dato: la línea se corta y un punto aislado se muestra con marcador. Opciones: `type='line'|'area'|'bar'`, `stacked` (barras), `formatValue(n)` (ejes, tooltip y tabla), `label` (nombre accesible y título de la tabla), `height=240`, `emptyLabel`, `legend` (por defecto con dos o más series), `axis` (valores y líneas guía en el eje y; por defecto sólo la línea base), `endLabel` (líneas: el último valor de la primera serie en una píldora al final; por defecto `true`), `tooltip` (por defecto `true`; con `false` la gráfica deja la guía y los marcadores y la tarjeta la pone `chart-tooltip`), `onSelect(index | null)`. API: `update(options)` (combina y redibuja), `select(index | null)`, `selected`, `destroy()`. Evento `mk:select` con `{index, label, values, x, y, series}`: `x`/`y` son el punto señalado en píxeles del área de la gráfica y `series` trae `{name, color, value, previous}` por serie.
29
+
30
+ Puntero y toque eligen el punto más cercano (el toque mantiene la selección; el ratón la quita al salir). Con foco en la gráfica, flechas/Inicio/Fin recorren los valores y Escape los oculta; un estado educado anuncia el valor elegido. La gráfica tiene un resumen como nombre accesible y una tabla para lectores de pantalla con todos los valores (y el total en barras apiladas). Sin datos muestra `emptyLabel`.
31
+
32
+ Movimiento: el primer dibujo traza la línea de izquierda a derecha (barras: crecen desde la base) y cada `update` desliza los valores nuevos desde los anteriores; el tooltip aparece con un pop de cristal y se desliza entre puntos. Con movimiento reducido todo cambia sin transición.
33
+
34
+ Colores: una sola serie es monocroma (`--mk-chart-mono`, el color del texto, al estilo de los indicadores de Melon Mind); con varias, cada serie usa su `color` o la ranura `--mk-chart-N` en orden fijo (paleta comprobada para daltonismo, con pasos propios en oscuro); a partir de la novena serie todas comparten `--mk-chart-other`: agrúpalas en «Otros». Los textos usan los colores de texto del tema; el color identifica la marca. Se redimensiona con su contenedor (ResizeObserver) y `destroy()` quita todo lo creado. Variables: `--mk-chart-mono`, `--mk-chart-1…8`, `--mk-chart-other`, `--mk-chart-surface` (anillo de marcadores), `--mk-chart-grid`.
35
+
36
+ ## Variantes visuales · 0.16
37
+
38
+ ```tsx
39
+ <Chart type="bar" barStyle="pill" barTrack barFill="spotlight" barWidth={40}
40
+ labels={['Lun', 'Mar', 'Mié']} series={[{ name: 'Enfoque', values: [32, 56, 44] }]} />
41
+ ```
42
+
43
+ - `barStyle`: `rounded` (compatibilidad) o `pill` (ambos extremos redondos). Las pilas comparten un recorte exterior por columna.
44
+ - `barTrack`: fondo de capacidad, false por defecto.
45
+ - `barFill`: `solid`, `gradient` o `spotlight` (selección con degradado sobre columnas rayadas). Sin selección, spotlight destaca la última columna.
46
+ - `barWidth`: ancho máximo, 24 px por defecto; se adapta al espacio disponible.
47
+ - `curve`: `linear` o `smooth`. La interpolación suave conserva los extremos reales y también funciona en áreas por rangos.
48
+
49
+ La selección de una píldora muestra un valor flotante. Pointer, tacto y teclado usan la misma selección. La entrada se escalona por columnas y los cambios de datos continúan desde los valores dibujados, también al interrumpir una transición.
@@ -15,6 +15,7 @@ mk-chart {
15
15
  --mk-chart-other: var(--mk-muted);
16
16
  --mk-chart-surface: var(--mk-surface-1);
17
17
  --mk-chart-grid: color-mix(in srgb, var(--mk-text) 14%, transparent);
18
+ --mk-chart-track: color-mix(in srgb, var(--mk-text) 5%, transparent);
18
19
  position: relative;
19
20
  display: grid;
20
21
  gap: 0.75rem;
@@ -120,6 +121,12 @@ mk-chart {
120
121
  fill: color-mix(in srgb, var(--mk-text) 6%, transparent);
121
122
  }
122
123
 
124
+ :is(.mk-chart, mk-chart) [data-mk-part='bar-track'] { fill: var(--mk-chart-track); }
125
+ :is(.mk-chart, mk-chart) [data-mk-part='bar'] { transition: opacity .22s ease, filter .22s ease; }
126
+ :is(.mk-chart, mk-chart) [data-mk-part='bar'][data-mk-active] { filter: brightness(1.12); }
127
+ :is(.mk-chart, mk-chart) [data-mk-part='bar-value'] rect { fill: var(--mk-text); }
128
+ :is(.mk-chart, mk-chart) [data-mk-part='bar-value'] text { fill: var(--mk-bg); font-size: 11px; font-weight: 600; }
129
+
123
130
  :is(.mk-chart, mk-chart) [data-mk-part='series'] path[data-mk-index] {
124
131
  transition: opacity 0.2s ease;
125
132
  }
@@ -13,6 +13,7 @@ export class MkChartElement extends MkElement<ChartInstance> {
13
13
  series: 'json',
14
14
  points: 'json',
15
15
  stacked: 'boolean',
16
+ barStyle: 'string', barTrack: 'boolean', barFill: 'string', barWidth: 'number', curve: 'string',
16
17
  label: 'string',
17
18
  height: 'number',
18
19
  emptyLabel: 'string',
@@ -28,7 +29,7 @@ export class MkChartElement extends MkElement<ChartInstance> {
28
29
 
29
30
  attributeChangedCallback(_name: string, previous: string | null, next: string | null) {
30
31
  if (previous === next || !this.instance) return;
31
- this.instance.update({ stacked: false, legend: undefined, ...(this.options as ChartOptions) });
32
+ this.instance.update({ stacked: false, barTrack: false, barStyle: undefined, barFill: undefined, barWidth: undefined, curve: undefined, legend: undefined, ...(this.options as ChartOptions) });
32
33
  }
33
34
 
34
35
  update(options: ChartOptions) {
@@ -47,6 +47,16 @@ export interface ChartOptions {
47
47
  points?: ChartPoint[];
48
48
  /** Bars only: stack the series. */
49
49
  stacked?: boolean;
50
+ /** Rounded ends (default) or a full capsule, including stacked columns. */
51
+ barStyle?: 'rounded' | 'pill';
52
+ /** Show the available capacity behind each column. */
53
+ barTrack?: boolean;
54
+ /** Solid (default), gradient, or a gradient selection over muted striped columns. */
55
+ barFill?: 'solid' | 'gradient' | 'spotlight';
56
+ /** Maximum column width in px (default 24; pill demos use 40). */
57
+ barWidth?: number;
58
+ /** Smooth monotone curves preserve the data's extrema (default linear). */
59
+ curve?: 'linear' | 'smooth';
50
60
  formatValue?: (value: number) => string;
51
61
  /** Accessible name (and table caption). */
52
62
  label?: string;
@@ -102,6 +112,25 @@ function barPath(x: number, width: number, base: number, end: number, rounded: b
102
112
  return `M${round(x)},${round(base)}V${round(end + dir * r)}Q${round(x)},${round(end)} ${round(x + r)},${round(end)}H${round(x + width - r)}Q${round(x + width)},${round(end)} ${round(x + width)},${round(end + dir * r)}V${round(base)}Z`;
103
113
  }
104
114
 
115
+ function pillPath(x: number, width: number, a: number, b: number) {
116
+ const top = Math.min(a, b), bottom = Math.max(a, b);
117
+ const r = Math.min(width / 2, (bottom - top) / 2);
118
+ return `M${round(x + r)},${round(top)}H${round(x + width - r)}Q${round(x + width)},${round(top)} ${round(x + width)},${round(top + r)}V${round(bottom - r)}Q${round(x + width)},${round(bottom)} ${round(x + width - r)},${round(bottom)}H${round(x + r)}Q${round(x)},${round(bottom)} ${round(x)},${round(bottom - r)}V${round(top + r)}Q${round(x)},${round(top)} ${round(x + r)},${round(top)}Z`;
119
+ }
120
+
121
+ /** Cubic Hermite interpolation with bounded tangents: no invented peaks between samples. */
122
+ function linePath(points: number[][], smooth: boolean) {
123
+ if (!points.length) return '';
124
+ let path = `M${round(points[0][0])},${round(points[0][1])}`;
125
+ const slopes = points.slice(1).map((point, i) => (point[1] - points[i][1]) / (point[0] - points[i][0] || 1));
126
+ const tangents = points.map((_, i) => i === 0 ? slopes[0] : i === points.length - 1 ? slopes[i - 1] : slopes[i - 1] * slopes[i] <= 0 ? 0 : 2 * slopes[i - 1] * slopes[i] / (slopes[i - 1] + slopes[i]));
127
+ for (let i = 1; i < points.length; i++) {
128
+ const [x0, y0] = points[i - 1], [x, y] = points[i], dx = (x - x0) / 3;
129
+ path += smooth ? `C${round(x0 + dx)},${round(y0 + tangents[i - 1] * dx)} ${round(x - dx)},${round(y - tangents[i] * dx)} ${round(x)},${round(y)}` : `L${round(x)},${round(y)}`;
130
+ }
131
+ return path;
132
+ }
133
+
105
134
  interface Model {
106
135
  labels: string[];
107
136
  series: Array<ChartSeries & { color: string }>;
@@ -357,6 +386,7 @@ export function createChart(root: HTMLElement | null, initial: ChartOptions = {}
357
386
  const slot = plotW / n;
358
387
  const xs = model.labels.map((_, i) => (bar ? left + slot * (i + 0.5) : n === 1 ? left + plotW / 2 : left + (plotW * i) / (n - 1)));
359
388
  geometry = { width, height, top, bottom: top + plotH, left, right: width - right, xs, y, base };
389
+ shown = { values, lower, lo, hi, type: type(), stacked: stacked() };
360
390
 
361
391
  // Baseline (and, with `axis`, hairlines with their values).
362
392
  const grid = svg('g', { 'data-mk-part': 'grid' });
@@ -401,10 +431,46 @@ export function createChart(root: HTMLElement | null, initial: ChartOptions = {}
401
431
  if (bar) {
402
432
  const barIndices = model.series.flatMap((_, index) => seriesType(index) === 'bar' ? [index] : []);
403
433
  const groups = stacked() ? 1 : barIndices.length;
404
- const barWidth = Math.max(2, Math.min(BAR_MAX, (slot * 0.62 - GAP * (groups - 1)) / groups));
434
+ const maxWidth = finite(options.barWidth) ? Math.max(2, Math.min(96, options.barWidth)) : BAR_MAX;
435
+ const barWidth = Math.max(2, Math.min(maxWidth, (slot * 0.62 - GAP * (groups - 1)) / groups));
436
+ const pill = options.barStyle === 'pill';
437
+ const trackLayer = svg('g', { 'data-mk-part': 'bar-tracks' });
438
+ canvas.append(trackLayer);
439
+ if (options.barFill && options.barFill !== 'solid') {
440
+ model.series.forEach((series, k) => {
441
+ const gradient = svg('linearGradient', { id: `${uid}-bar-${k}`, x1: 0, y1: 0, x2: 0, y2: 1 });
442
+ for (const [offset, opacity] of [['0', '1'], ['1', '0.4']]) {
443
+ const stop = svg('stop', { offset }); stop.style.stopColor = series.color; stop.style.stopOpacity = opacity; gradient.append(stop);
444
+ }
445
+ defs.append(gradient);
446
+ });
447
+ const hatch = svg('pattern', { id: `${uid}-hatch`, patternUnits: 'userSpaceOnUse', width: 6, height: 6, patternTransform: 'rotate(35)' });
448
+ const ground = svg('rect', { width: 6, height: 6 }); ground.style.fill = 'var(--mk-chart-track)';
449
+ const stripe = svg('line', { x1: 0, x2: 0, y1: 0, y2: 6 }); stripe.style.stroke = 'var(--mk-chart-grid)'; stripe.style.strokeWidth = '2';
450
+ hatch.append(ground, stripe); defs.append(hatch);
451
+ }
452
+ const fillBar = (path: SVGPathElement, k: number, index: number) => {
453
+ const fill = options.barFill && options.barFill !== 'solid' ? `url(#${uid}-bar-${k})` : model.series[k]!.color;
454
+ path.style.setProperty('--mk-bar-fill', fill);
455
+ path.style.fill = options.barFill === 'spotlight' && index !== (selected ?? n - 1) ? `url(#${uid}-hatch)` : fill;
456
+ };
457
+ const addTrack = (x: number) => {
458
+ if (!options.barTrack) return;
459
+ const path = svg('path', { d: pillPath(x, barWidth, top, base), 'data-mk-part': 'bar-track' });
460
+ trackLayer.append(path);
461
+ };
405
462
  const totalsLayer = svg('g', { 'data-mk-part': 'totals' });
406
463
  model.labels.forEach((_, i) => {
407
464
  if (stacked()) {
465
+ const clipId = `${uid}-stack-${i}`;
466
+ if (pill) {
467
+ const sum = totalsOf(values, i);
468
+ const clip = svg('clipPath', { id: clipId });
469
+ if (sum.up) clip.append(svg('path', { d: pillPath(xs[i]! - barWidth / 2, barWidth, base, y(sum.up)) }));
470
+ if (sum.down) clip.append(svg('path', { d: pillPath(xs[i]! - barWidth / 2, barWidth, base, y(sum.down)) }));
471
+ defs.append(clip);
472
+ }
473
+ addTrack(xs[i]! - barWidth / 2);
408
474
  let up = 0;
409
475
  let down = 0;
410
476
  const outer = { up: -1, down: -1 };
@@ -424,8 +490,9 @@ export function createChart(root: HTMLElement | null, initial: ChartOptions = {}
424
490
  const start = y(from) + (from !== 0 ? (value > 0 ? -GAP : GAP) : 0);
425
491
  const end = y(to);
426
492
  if ((value > 0 && end >= start) || (value < 0 && end <= start)) return;
427
- const path = svg('path', { d: barPath(xs[i]! - barWidth / 2, barWidth, start, end, k === (value > 0 ? outer.up : outer.down)), 'data-mk-index': i, 'data-mk-series': k });
428
- path.style.fill = model.series[k]!.color;
493
+ const path = svg('path', { d: barPath(xs[i]! - barWidth / 2, barWidth, start, end, !pill && k === (value > 0 ? outer.up : outer.down)), 'data-mk-part': 'bar', 'data-mk-index': i, 'data-mk-series': k });
494
+ if (pill) path.setAttribute('clip-path', `url(#${clipId})`);
495
+ fillBar(path, k, i);
429
496
  marks.append(path);
430
497
  });
431
498
  // The stack total above it, when the columns have room.
@@ -440,10 +507,11 @@ export function createChart(root: HTMLElement | null, initial: ChartOptions = {}
440
507
  values.forEach((list, k) => {
441
508
  if (seriesType(k) !== 'bar') return;
442
509
  const value = list[i];
443
- if (!finite(value) || value === 0) return;
444
510
  const x = xs[i]! - groupWidth / 2 + barIndices.indexOf(k) * (barWidth + GAP);
445
- const path = svg('path', { d: barPath(x, barWidth, base, y(value), true), 'data-mk-index': i, 'data-mk-series': k });
446
- path.style.fill = model.series[k]!.color;
511
+ addTrack(x);
512
+ if (!finite(value) || value === 0) return;
513
+ const path = svg('path', { d: pill ? pillPath(x, barWidth, base, y(value)) : barPath(x, barWidth, base, y(value), true), 'data-mk-part': 'bar', 'data-mk-index': i, 'data-mk-series': k });
514
+ fillBar(path, k, i);
447
515
  marks.append(path);
448
516
  });
449
517
  }
@@ -477,7 +545,9 @@ export function createChart(root: HTMLElement | null, initial: ChartOptions = {}
477
545
  defs.append(gradient);
478
546
  for (const points of runs) {
479
547
  if (points.length < 2) continue;
480
- const d = `M${points.map(([px, py]) => `${round(px)},${round(py)}`).join('L')}L${[...points].reverse().map(([px, , py]) => `${round(px)},${round(py)}`).join('L')}Z`;
548
+ const upper = linePath(points, options.curve === 'smooth');
549
+ const bottom = linePath([...points].reverse().map(([px, , py]) => [px, py]), options.curve === 'smooth');
550
+ const d = `${upper}${bottom.replace(/^M/, 'L')}Z`;
481
551
  const area = svg('path', { d, 'data-mk-part': 'area', 'data-mk-series': k });
482
552
  area.style.fill = `url(#${uid}-fill-${k})`;
483
553
  marks.append(area);
@@ -491,7 +561,7 @@ export function createChart(root: HTMLElement | null, initial: ChartOptions = {}
491
561
  marks.append(dot);
492
562
  continue;
493
563
  }
494
- const line = svg('path', { d: `M${points.map(([px, py]) => `${round(px)},${round(py)}`).join('L')}`, 'data-mk-part': 'line' });
564
+ const line = svg('path', { d: linePath(points, options.curve === 'smooth'), 'data-mk-part': 'line' });
495
565
  line.style.stroke = series.color;
496
566
  // Dotted patterns (a first dash of 1 or less) draw round dots; dashes stay thin.
497
567
  const dotted = Boolean(series.dash) && Number(series.dash!.trim().split(/[\s,]+/)[0]) <= 1;
@@ -531,6 +601,11 @@ export function createChart(root: HTMLElement | null, initial: ChartOptions = {}
531
601
  cursor.replaceChildren();
532
602
  if (band) band.style.display = 'none';
533
603
  for (const mark of canvas.querySelectorAll('[data-mk-index]')) mark.removeAttribute('data-mk-dim');
604
+ for (const mark of canvas.querySelectorAll<SVGPathElement>('[data-mk-part="bar"]')) {
605
+ const index = Number(mark.dataset.mkIndex);
606
+ mark.toggleAttribute('data-mk-active', index === selected);
607
+ if (options.barFill === 'spotlight') mark.style.fill = index === (selected ?? model.labels.length - 1) ? mark.style.getPropertyValue('--mk-bar-fill') : `url(#${uid}-hatch)`;
608
+ }
534
609
  if (selected === null || !hasData()) {
535
610
  if (!tooltip.hidden && !reduced()) {
536
611
  gsap.killTweensOf(tooltip, 'opacity,scale');
@@ -555,6 +630,20 @@ export function createChart(root: HTMLElement | null, initial: ChartOptions = {}
555
630
  for (const mark of canvas.querySelectorAll<SVGElement>('[data-mk-index]')) if (mark.getAttribute('data-mk-index') !== String(i)) mark.setAttribute('data-mk-dim', '');
556
631
  const { up, down } = totals(i);
557
632
  anchorY = stacked() ? geometry.y(up || down) : Math.min(...model.series.map((series) => (finite(series.values[i]) ? geometry.y(series.values[i]!) : geometry.bottom)));
633
+ if (options.barStyle === 'pill') {
634
+ const amount = stacked() ? up + down : model.series[0]?.values[i];
635
+ if (finite(amount)) {
636
+ const label = format(amount), width = Math.max(40, label.length * 7 + 18);
637
+ const bx = Math.max(0, Math.min(geometry.width - width, x - width / 2));
638
+ const by = Math.max(0, anchorY - 32);
639
+ const group = svg('g', { 'data-mk-part': 'bar-value' });
640
+ group.append(svg('rect', { x: round(bx), y: round(by), width, height: 24, rx: 12 }));
641
+ const text = svg('text', { x: round(bx + width / 2), y: round(by + 12), 'text-anchor': 'middle', 'dominant-baseline': 'middle' });
642
+ text.textContent = label; group.append(text); cursor.append(group);
643
+ const marker = svg('circle', { cx: round(x), cy: round(anchorY), r: 4, 'data-mk-part': 'marker' }); marker.style.fill = model.series[0]?.color ?? 'var(--mk-text)'; cursor.append(marker);
644
+ if (fresh && !reduced()) keep(gsap.fromTo(group, { opacity: 0, y: 6 }, { opacity: 1, y: 0, duration: 0.3, ease: EASE.pop }));
645
+ }
646
+ }
558
647
  } else {
559
648
  cursor.append(svg('line', { x1: round(x), x2: round(x), y1: geometry.top, y2: geometry.bottom, 'data-mk-part': 'guide' }));
560
649
  model.series.forEach((series) => {
@@ -644,6 +733,7 @@ export function createChart(root: HTMLElement | null, initial: ChartOptions = {}
644
733
  glide?.kill();
645
734
  canvas.setAttribute('aria-label', describe());
646
735
  scope.attr(root, 'data-mk-chart-type', type());
736
+ scope.attr(root, 'data-mk-bar-style', options.barStyle ?? 'rounded');
647
737
  renderTable();
648
738
  renderLegend();
649
739
  const ok = hasData();
@@ -700,12 +790,14 @@ export function createChart(root: HTMLElement | null, initial: ChartOptions = {}
700
790
  // First appearance: bars grow from the baseline, lines wipe in from the left.
701
791
  finish();
702
792
  if (type() === 'bar') {
703
- const state = { t: 0 };
793
+ const stagger = Math.min(0.045, 0.22 / Math.max(1, model.labels.length - 1));
794
+ const length = 0.7 + stagger * (model.labels.length - 1);
795
+ const state = { t: 0 }, ease = gsap.parseEase(EASE.swatch);
704
796
  glide = gsap.to(state, {
705
- t: 1,
706
- duration: 0.7,
707
- ease: EASE.swatch,
708
- onUpdate: () => paint(target.map((list) => list.map((value) => (finite(value) ? value * state.t : value))), domain.lo, domain.hi, domain.step),
797
+ t: length,
798
+ duration: length,
799
+ ease: 'none',
800
+ onUpdate: () => paint(target.map((list) => list.map((value, i) => (finite(value) ? value * Math.max(0, ease(Math.min(1, Math.max(0, (state.t - i * stagger) / 0.7)))) : value))), domain.lo, domain.hi, domain.step),
709
801
  onComplete: () => {
710
802
  finish();
711
803
  settle();
@@ -11,13 +11,13 @@ export interface ChartProps extends Omit<HTMLAttributes<HTMLDivElement>, 'onSele
11
11
  * <Chart type="bar" stacked labels={weeks} series={[{ name: 'Ana', values }]} formatValue={money} label="Sesiones por semana" />.
12
12
  * Data changes redraw in place; the selection survives when its point still exists.
13
13
  */
14
- export function Chart({ type, labels, series, points, stacked, formatValue, label, height, emptyLabel, legend, tooltip, axis, endLabel, onSelect, instanceRef, className, ...props }: ChartProps) {
14
+ export function Chart({ type, labels, series, points, stacked, barStyle, barTrack, barFill, barWidth, curve, formatValue, label, height, emptyLabel, legend, tooltip, axis, endLabel, onSelect, instanceRef, className, ...props }: ChartProps) {
15
15
  const ref = useRef<HTMLDivElement>(null);
16
16
  const instance = useRef<ChartInstance | null>(null);
17
17
  const format = useLatest(formatValue);
18
18
  const select = useLatest(onSelect);
19
19
  const formatted = formatValue !== undefined;
20
- const data = { type, labels, series, points, stacked, label, height, emptyLabel, legend, tooltip, axis, endLabel };
20
+ const data = { type, labels, series, points, stacked, barStyle, barTrack, barFill, barWidth, curve, label, height, emptyLabel, legend, tooltip, axis, endLabel };
21
21
  const key = JSON.stringify(data);
22
22
  const latest = useLatest(data);
23
23
  const options = (): ChartOptions => ({
@@ -46,3 +46,13 @@ Los nombres camelCase se escriben como atributos kebab-case. Los arrays y objeto
46
46
  Variables: --mk-chart-1…8, --mk-chart-surface.
47
47
 
48
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.
49
+
50
+ ## Medidor segmentado · 0.16
51
+
52
+ ```tsx
53
+ <RadialChart sweep={180} segments={18} thickness={30} center="percent"
54
+ totalLabel="Crecimiento" legend={false}
55
+ data={[{ label: 'Ventas', value: 70.8, max: 100 }]} />
56
+ ```
57
+
58
+ `segments` divide cada anillo en sectores (0 conserva el trazo continuo). `center="percent"` muestra el avance hacia la meta; por defecto muestra el valor. En HTML se usan los mismos atributos. Los datos se interpolan sobre los segmentos y conservan los valores exactos en la tabla accesible.
@@ -7,7 +7,7 @@ import { createRadialChart, type RadialChartInstance, type RadialChartOptions }
7
7
  * sweep="270" draws a gauge. Attribute changes glide in place. Emits mk:select ({index, label, value}).
8
8
  */
9
9
  export class MkRadialChartElement extends MkElement<RadialChartInstance> {
10
- static options: Record<string, OptionType> = { data: 'json', max: 'number', sweep: 'number', size: 'number', thickness: 'number', label: 'string', totalLabel: 'string', legend: 'boolean', emptyLabel: 'string' };
10
+ static options: Record<string, OptionType> = { data: 'json', max: 'number', sweep: 'number', size: 'number', thickness: 'number', segments: 'number', center: 'string', label: 'string', totalLabel: 'string', legend: 'boolean', emptyLabel: 'string' };
11
11
 
12
12
  protected create(options: Record<string, unknown>) {
13
13
  return createRadialChart(this, options as RadialChartOptions);
@@ -15,7 +15,7 @@ export class MkRadialChartElement extends MkElement<RadialChartInstance> {
15
15
 
16
16
  attributeChangedCallback(_name: string, previous: string | null, next: string | null) {
17
17
  if (previous === next || !this.instance) return;
18
- this.instance.update({ legend: undefined, max: undefined, ...(this.options as RadialChartOptions) });
18
+ this.instance.update({ legend: undefined, max: undefined, segments: 0, center: undefined, ...(this.options as RadialChartOptions) });
19
19
  }
20
20
 
21
21
  update(options: RadialChartOptions) {