@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.
- package/README.md +174 -155
- package/bin/motion-kit.mjs +3 -3
- package/dist/auto/index.d.ts +6 -3
- package/dist/auto/index.js +8 -2
- package/dist/auto/index.js.map +1 -1
- package/dist/{chunk-EY4BJKC4.js → chunk-6IRJYXJU.js} +2 -2
- package/dist/{chunk-CU6SXW3E.js → chunk-A6QQCF6V.js} +1309 -310
- package/dist/chunk-A6QQCF6V.js.map +1 -0
- package/dist/chunk-JXRS3OEA.js +3638 -0
- package/dist/chunk-JXRS3OEA.js.map +1 -0
- package/dist/css/components/add-to-cart.css +72 -0
- package/dist/css/components/bidirectional-slider.css +168 -0
- package/dist/css/components/data-table.css +121 -26
- package/dist/css/components/filter-builder.css +10 -15
- package/dist/css/components/heatmap-chart.css +391 -0
- package/dist/css/components/hover-card.css +8 -2
- package/dist/css/components/image-preview.css +90 -2
- package/dist/css/components/json-viewer.css +33 -1
- package/dist/css/components/nav.css +102 -102
- package/dist/css/components/orbit-menu.css +130 -0
- package/dist/css/components/pagination.css +10 -5
- package/dist/css/components/pie-chart.css +390 -0
- package/dist/css/components/pill-button.css +135 -0
- package/dist/css/components/radar-chart.css +370 -0
- package/dist/css/components/radial-chart.css +398 -0
- package/dist/css/components/sankey-chart.css +358 -0
- package/dist/css/components/scroll-island.css +111 -0
- package/dist/css/components/select.css +136 -0
- package/dist/css/components/toolbar.css +122 -0
- package/dist/css/components/tree-view.css +79 -0
- package/dist/css/components/voice-dots.css +70 -0
- package/dist/elements/index.d.ts +180 -7
- package/dist/elements/index.js +287 -6
- package/dist/elements/index.js.map +1 -1
- package/dist/{engine-Cn6IhE3z.d.ts → engine-5zRIssTO.d.ts} +11 -1
- package/dist/index.css +1832 -168
- package/dist/index.d.ts +4 -4
- package/dist/index.js +33 -3
- package/dist/index.js.map +1 -1
- package/dist/react/index.d.ts +117 -11
- package/dist/react/index.js +314 -13
- package/dist/react/index.js.map +1 -1
- package/dist/{reduced-BBUusld6.d.ts → reduced-6X_bns60.d.ts} +1 -1
- package/dist/{reveal-DEvp6U6T.d.ts → reveal-BfyKKsw4.d.ts} +97 -4
- package/dist/store-CA2Qcun_.d.ts +453 -0
- package/package.json +9 -16
- package/registry.json +1096 -948
- package/src/auto/auto-init.ts +6 -0
- package/src/components/add-to-cart/README.md +34 -0
- package/src/components/add-to-cart/add-to-cart.css +71 -0
- package/src/components/add-to-cart/element.ts +20 -0
- package/src/components/add-to-cart/engine.ts +206 -0
- package/src/components/add-to-cart/react.tsx +29 -0
- package/src/components/bidirectional-slider/README.md +46 -0
- package/src/components/bidirectional-slider/bidirectional-slider.css +167 -0
- package/src/components/bidirectional-slider/element.ts +42 -0
- package/src/components/bidirectional-slider/engine.ts +289 -0
- package/src/components/bidirectional-slider/react.tsx +45 -0
- package/src/components/chart/README.md +10 -0
- package/src/components/chart/element.ts +2 -0
- package/src/components/chart/engine.ts +50 -23
- package/src/components/chart/parts.ts +237 -0
- package/src/components/chart/react.tsx +2 -2
- package/src/components/chart/use-chart.ts +55 -0
- package/src/components/data-table/README.md +3 -3
- package/src/components/data-table/data-table.css +121 -26
- package/src/components/data-table/engine.ts +81 -29
- package/src/components/editable-table/engine.ts +67 -11
- package/src/components/filter-builder/engine.ts +33 -2
- package/src/components/filter-builder/filter-builder.css +10 -15
- package/src/components/heatmap-chart/README.md +48 -0
- package/src/components/heatmap-chart/element.ts +21 -0
- package/src/components/heatmap-chart/engine.ts +281 -0
- package/src/components/heatmap-chart/heatmap-chart.css +93 -0
- package/src/components/heatmap-chart/react.tsx +14 -0
- package/src/components/hover-card/engine.ts +30 -1
- package/src/components/hover-card/hover-card.css +8 -2
- package/src/components/image-preview/element.ts +1 -1
- package/src/components/image-preview/engine.ts +95 -15
- package/src/components/image-preview/image-preview.css +90 -2
- package/src/components/image-preview/react.tsx +3 -3
- package/src/components/json-viewer/engine.ts +11 -2
- package/src/components/json-viewer/json-viewer.css +33 -1
- package/src/components/nav/README.md +12 -10
- package/src/components/nav/element.ts +5 -5
- package/src/components/nav/engine.ts +21 -14
- package/src/components/nav/nav.css +102 -102
- package/src/components/nav/react.tsx +4 -4
- package/src/components/orbit-menu/README.md +46 -0
- package/src/components/orbit-menu/element.ts +27 -0
- package/src/components/orbit-menu/engine.ts +179 -0
- package/src/components/orbit-menu/orbit-menu.css +129 -0
- package/src/components/orbit-menu/react.tsx +44 -0
- package/src/components/pagination/README.md +1 -1
- package/src/components/pagination/engine.ts +50 -4
- package/src/components/pagination/pagination.css +10 -5
- package/src/components/pie-chart/README.md +48 -0
- package/src/components/pie-chart/element.ts +30 -0
- package/src/components/pie-chart/engine.ts +283 -0
- package/src/components/pie-chart/pie-chart.css +92 -0
- package/src/components/pie-chart/react.tsx +14 -0
- package/src/components/pill-button/README.md +46 -0
- package/src/components/pill-button/element.ts +16 -0
- package/src/components/pill-button/engine.ts +77 -0
- package/src/components/pill-button/pill-button.css +134 -0
- package/src/components/pill-button/react.tsx +32 -0
- package/src/components/radar-chart/README.md +48 -0
- package/src/components/radar-chart/element.ts +30 -0
- package/src/components/radar-chart/engine.ts +327 -0
- package/src/components/radar-chart/radar-chart.css +72 -0
- package/src/components/radar-chart/react.tsx +14 -0
- package/src/components/radial-chart/README.md +48 -0
- package/src/components/radial-chart/element.ts +30 -0
- package/src/components/radial-chart/engine.ts +290 -0
- package/src/components/radial-chart/radial-chart.css +100 -0
- package/src/components/radial-chart/react.tsx +14 -0
- package/src/components/sankey-chart/README.md +48 -0
- package/src/components/sankey-chart/element.ts +31 -0
- package/src/components/sankey-chart/engine.ts +401 -0
- package/src/components/sankey-chart/react.tsx +14 -0
- package/src/components/sankey-chart/sankey-chart.css +60 -0
- package/src/components/scroll-island/README.md +34 -0
- package/src/components/scroll-island/element.ts +16 -0
- package/src/components/scroll-island/engine.ts +252 -0
- package/src/components/scroll-island/react.tsx +26 -0
- package/src/components/scroll-island/scroll-island.css +110 -0
- package/src/components/select/README.md +34 -0
- package/src/components/select/element.ts +26 -0
- package/src/components/select/engine.ts +267 -0
- package/src/components/select/react.tsx +33 -0
- package/src/components/select/select.css +135 -0
- package/src/components/toolbar/README.md +46 -0
- package/src/components/toolbar/element.ts +30 -0
- package/src/components/toolbar/engine.ts +230 -0
- package/src/components/toolbar/react.tsx +34 -0
- package/src/components/toolbar/toolbar.css +121 -0
- package/src/components/tree-view/element.ts +1 -1
- package/src/components/tree-view/engine.ts +174 -0
- package/src/components/tree-view/react.tsx +7 -3
- package/src/components/tree-view/tree-view.css +79 -0
- package/src/components/voice-dots/README.md +48 -0
- package/src/components/voice-dots/element.ts +45 -0
- package/src/components/voice-dots/engine.ts +243 -0
- package/src/components/voice-dots/react.tsx +52 -0
- package/src/components/voice-dots/voice-dots.css +69 -0
- package/src/elements/index.ts +39 -0
- package/src/index.ts +16 -0
- package/src/motion/liquid.ts +14 -4
- package/src/react/index.ts +14 -1
- package/dist/chunk-6P4P7NU6.js +0 -1401
- package/dist/chunk-6P4P7NU6.js.map +0 -1
- package/dist/chunk-CU6SXW3E.js.map +0 -1
- package/dist/store-DjkTBX7f.d.ts +0 -195
- /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
|
|
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('
|
|
348
|
+
const sizeField = el('div', 'data-mk-dt-size');
|
|
347
349
|
sizeField.append(el('span', undefined, 'Filas'));
|
|
348
|
-
const
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
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,
|
|
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:
|
|
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:
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
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
|
-
|
|
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]
|
|
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]
|
|
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);
|