@yewdiel21/motion-kit 0.10.0 → 0.14.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 +155 -93
- package/dist/auto/index.d.ts +53 -3
- package/dist/auto/index.js +101 -3
- package/dist/auto/index.js.map +1 -1
- package/dist/{chunk-IKQ5FODN.js → chunk-6P4P7NU6.js} +2 -218
- package/dist/chunk-6P4P7NU6.js.map +1 -0
- package/dist/chunk-CU6SXW3E.js +20334 -0
- package/dist/chunk-CU6SXW3E.js.map +1 -0
- package/dist/{chunk-XAKC2QY7.js → chunk-EY4BJKC4.js} +3 -28
- package/dist/chunk-EY4BJKC4.js.map +1 -0
- package/dist/chunk-QP4QJOEN.js +72 -0
- package/dist/chunk-QP4QJOEN.js.map +1 -0
- package/dist/css/components/activity-feed.css +240 -0
- package/dist/css/components/attachment.css +266 -0
- package/dist/css/components/breadcrumb.css +110 -0
- package/dist/css/components/chart-tooltip.css +168 -0
- package/dist/css/components/chart.css +296 -0
- package/dist/css/components/code-block.css +264 -0
- package/dist/css/components/color-area.css +82 -0
- package/dist/css/components/color-field.css +67 -0
- package/dist/css/components/color-picker.css +131 -0
- package/dist/css/components/color-slider.css +113 -0
- package/dist/css/components/command-search.css +252 -0
- package/dist/css/components/context-menu.css +23 -0
- package/dist/css/components/credit-card.css +288 -0
- package/dist/css/components/data-table.css +591 -0
- package/dist/css/components/date-range-picker.css +168 -0
- package/dist/css/components/date-time-picker.css +178 -0
- package/dist/css/components/drawer.css +58 -0
- package/dist/css/components/editable-table.css +201 -0
- package/dist/css/components/empty-state.css +61 -0
- package/dist/css/components/file-explorer.css +633 -0
- package/dist/css/components/filter-builder.css +232 -0
- package/dist/css/components/hold-confirm.css +87 -0
- package/dist/css/components/hover-card.css +13 -7
- package/dist/css/components/image-preview.css +195 -0
- package/dist/css/components/json-viewer.css +272 -0
- package/dist/css/components/like-button.css +78 -0
- package/dist/css/components/link-preview.css +275 -0
- package/dist/css/components/loading-button.css +77 -0
- package/dist/css/components/multi-select.css +267 -0
- package/dist/css/components/notification-bell.css +127 -0
- package/dist/css/components/password-strength.css +183 -0
- package/dist/css/components/popover.css +25 -0
- package/dist/css/components/progress.css +140 -0
- package/dist/css/components/questionnaire.css +217 -0
- package/dist/css/components/range-calendar.css +186 -0
- package/dist/css/components/range-slider.css +192 -0
- package/dist/css/components/resizable-panel.css +243 -0
- package/dist/css/components/rich-text-editor.css +284 -0
- package/dist/css/components/scratch-card.css +75 -0
- package/dist/css/components/skeleton.css +108 -0
- package/dist/css/components/split-view.css +19 -0
- package/dist/css/components/star-rating.css +80 -0
- package/dist/css/components/stat-card.css +164 -0
- package/dist/css/components/swipe-actions.css +105 -0
- package/dist/css/components/tag-input.css +244 -0
- package/dist/css/components/time-field.css +88 -0
- package/dist/css/components/timeline.css +219 -0
- package/dist/css/components/tooltip.css +79 -0
- package/dist/css/components/tree-view.css +210 -0
- package/dist/css/components/virtual-list.css +195 -0
- package/dist/css/tokens.css +185 -181
- package/dist/elements/index.d.ts +674 -5
- package/dist/elements/index.js +1361 -8
- package/dist/elements/index.js.map +1 -1
- package/dist/{engine-GDGwQnJE.d.ts → engine-Cn6IhE3z.d.ts} +1 -1
- package/dist/engine-QECQnGz1.d.ts +33 -0
- package/dist/index.css +9104 -189
- package/dist/index.d.ts +89 -5
- package/dist/index.js +243 -9
- package/dist/index.js.map +1 -1
- package/dist/react/index.d.ts +579 -23
- package/dist/react/index.js +2042 -7
- package/dist/react/index.js.map +1 -1
- package/dist/{reduced-BmBzKH94.d.ts → reduced-BBUusld6.d.ts} +9 -2
- package/dist/reveal-DEvp6U6T.d.ts +2381 -0
- package/dist/{store-DYb3tTsZ.d.ts → store-DjkTBX7f.d.ts} +2 -2
- package/package.json +1 -1
- package/registry.json +406 -1
- package/src/auto/auto-init.ts +98 -0
- package/src/components/activity-feed/README.md +56 -0
- package/src/components/activity-feed/activity-feed.css +239 -0
- package/src/components/activity-feed/element.ts +36 -0
- package/src/components/activity-feed/engine.ts +351 -0
- package/src/components/activity-feed/react.tsx +32 -0
- package/src/components/attachment/README.md +27 -0
- package/src/components/attachment/attachment.css +265 -0
- package/src/components/attachment/element.ts +50 -0
- package/src/components/attachment/engine.ts +469 -0
- package/src/components/attachment/react.tsx +65 -0
- package/src/components/breadcrumb/README.md +38 -0
- package/src/components/breadcrumb/breadcrumb.css +109 -0
- package/src/components/breadcrumb/element.ts +27 -0
- package/src/components/breadcrumb/engine.ts +249 -0
- package/src/components/breadcrumb/react.tsx +30 -0
- package/src/components/chart/README.md +24 -0
- package/src/components/chart/chart.css +295 -0
- package/src/components/chart/element.ts +41 -0
- package/src/components/chart/engine.ts +811 -0
- package/src/components/chart/react.tsx +50 -0
- package/src/components/chart-tooltip/README.md +53 -0
- package/src/components/chart-tooltip/chart-tooltip.css +167 -0
- package/src/components/chart-tooltip/element.ts +37 -0
- package/src/components/chart-tooltip/engine.ts +442 -0
- package/src/components/chart-tooltip/react.tsx +47 -0
- package/src/components/code-block/README.md +19 -0
- package/src/components/code-block/code-block.css +263 -0
- package/src/components/code-block/element.ts +34 -0
- package/src/components/code-block/engine.ts +254 -0
- package/src/components/code-block/highlight.ts +244 -0
- package/src/components/code-block/react.tsx +94 -0
- package/src/components/color-area/README.md +50 -0
- package/src/components/color-area/color-area.css +81 -0
- package/src/components/color-area/element.ts +26 -0
- package/src/components/color-area/engine.ts +246 -0
- package/src/components/color-area/react.tsx +42 -0
- package/src/components/color-field/README.md +43 -0
- package/src/components/color-field/color-field.css +66 -0
- package/src/components/color-field/element.ts +26 -0
- package/src/components/color-field/engine.ts +174 -0
- package/src/components/color-field/react.tsx +42 -0
- package/src/components/color-picker/README.md +45 -0
- package/src/components/color-picker/color-picker.css +130 -0
- package/src/components/color-picker/element.ts +45 -0
- package/src/components/color-picker/engine.ts +314 -0
- package/src/components/color-picker/react.tsx +47 -0
- package/src/components/color-slider/README.md +55 -0
- package/src/components/color-slider/color-slider.css +112 -0
- package/src/components/color-slider/element.ts +26 -0
- package/src/components/color-slider/engine.ts +234 -0
- package/src/components/color-slider/react.tsx +45 -0
- package/src/components/command-search/README.md +15 -0
- package/src/components/command-search/command-search.css +251 -0
- package/src/components/command-search/element.ts +36 -0
- package/src/components/command-search/engine.ts +356 -0
- package/src/components/command-search/match.ts +53 -0
- package/src/components/command-search/react.tsx +66 -0
- package/src/components/context-menu/README.md +31 -0
- package/src/components/context-menu/context-menu.css +22 -0
- package/src/components/context-menu/element.ts +40 -0
- package/src/components/context-menu/engine.ts +153 -0
- package/src/components/context-menu/react.tsx +57 -0
- package/src/components/credit-card/README.md +20 -0
- package/src/components/credit-card/card.ts +77 -0
- package/src/components/credit-card/credit-card.css +287 -0
- package/src/components/credit-card/element.ts +33 -0
- package/src/components/credit-card/engine.ts +410 -0
- package/src/components/credit-card/react.tsx +36 -0
- package/src/components/data-table/README.md +83 -0
- package/src/components/data-table/data-table.css +590 -0
- package/src/components/data-table/element.ts +54 -0
- package/src/components/data-table/engine.ts +1059 -0
- package/src/components/data-table/react.tsx +94 -0
- package/src/components/date-range-picker/README.md +51 -0
- package/src/components/date-range-picker/date-range-picker.css +167 -0
- package/src/components/date-range-picker/element.ts +52 -0
- package/src/components/date-range-picker/engine.ts +352 -0
- package/src/components/date-range-picker/react.tsx +57 -0
- package/src/components/date-time-picker/README.md +48 -0
- package/src/components/date-time-picker/date-time-picker.css +177 -0
- package/src/components/date-time-picker/element.ts +47 -0
- package/src/components/date-time-picker/engine.ts +350 -0
- package/src/components/date-time-picker/react.tsx +54 -0
- package/src/components/dialog/README.md +103 -102
- package/src/components/dialog/element.ts +123 -118
- package/src/components/dialog/engine.ts +449 -432
- package/src/components/drawer/README.md +28 -0
- package/src/components/drawer/drawer.css +57 -0
- package/src/components/drawer/element.ts +21 -0
- package/src/components/drawer/engine.ts +56 -0
- package/src/components/drawer/react.tsx +85 -0
- package/src/components/editable-table/README.md +67 -0
- package/src/components/editable-table/editable-table.css +200 -0
- package/src/components/editable-table/element.ts +45 -0
- package/src/components/editable-table/engine.ts +775 -0
- package/src/components/editable-table/react.tsx +79 -0
- package/src/components/empty-state/README.md +41 -0
- package/src/components/empty-state/element.ts +23 -0
- package/src/components/empty-state/empty-state.css +60 -0
- package/src/components/empty-state/engine.ts +232 -0
- package/src/components/empty-state/react.tsx +34 -0
- package/src/components/file-explorer/README.md +67 -0
- package/src/components/file-explorer/element.ts +39 -0
- package/src/components/file-explorer/engine.ts +1307 -0
- package/src/components/file-explorer/file-explorer.css +632 -0
- package/src/components/file-explorer/react.tsx +77 -0
- package/src/components/filter-builder/README.md +47 -0
- package/src/components/filter-builder/element.ts +34 -0
- package/src/components/filter-builder/engine.ts +365 -0
- package/src/components/filter-builder/filter-builder.css +231 -0
- package/src/components/filter-builder/react.tsx +36 -0
- package/src/components/hold-confirm/README.md +13 -0
- package/src/components/hold-confirm/element.ts +24 -0
- package/src/components/hold-confirm/engine.ts +216 -0
- package/src/components/hold-confirm/hold-confirm.css +86 -0
- package/src/components/hold-confirm/react.tsx +28 -0
- package/src/components/hover-card/README.md +6 -2
- package/src/components/hover-card/engine.ts +68 -6
- package/src/components/hover-card/hover-card.css +13 -7
- package/src/components/image-preview/README.md +16 -0
- package/src/components/image-preview/element.ts +26 -0
- package/src/components/image-preview/engine.ts +325 -0
- package/src/components/image-preview/image-preview.css +194 -0
- package/src/components/image-preview/react.tsx +43 -0
- package/src/components/json-viewer/README.md +56 -0
- package/src/components/json-viewer/element.ts +37 -0
- package/src/components/json-viewer/engine.ts +440 -0
- package/src/components/json-viewer/json-viewer.css +271 -0
- package/src/components/json-viewer/react.tsx +56 -0
- package/src/components/like-button/README.md +13 -0
- package/src/components/like-button/element.ts +47 -0
- package/src/components/like-button/engine.ts +145 -0
- package/src/components/like-button/like-button.css +77 -0
- package/src/components/like-button/react.tsx +39 -0
- package/src/components/link-preview/README.md +19 -0
- package/src/components/link-preview/element.ts +29 -0
- package/src/components/link-preview/engine.ts +192 -0
- package/src/components/link-preview/link-preview.css +274 -0
- package/src/components/link-preview/react.tsx +101 -0
- package/src/components/loading-button/README.md +45 -0
- package/src/components/loading-button/element.ts +34 -0
- package/src/components/loading-button/engine.ts +201 -0
- package/src/components/loading-button/loading-button.css +76 -0
- package/src/components/loading-button/react.tsx +43 -0
- package/src/components/multi-select/README.md +48 -0
- package/src/components/multi-select/element.ts +28 -0
- package/src/components/multi-select/engine.ts +294 -0
- package/src/components/multi-select/multi-select.css +266 -0
- package/src/components/multi-select/react.tsx +39 -0
- package/src/components/notification-bell/README.md +14 -0
- package/src/components/notification-bell/element.ts +48 -0
- package/src/components/notification-bell/engine.ts +202 -0
- package/src/components/notification-bell/notification-bell.css +126 -0
- package/src/components/notification-bell/react.tsx +57 -0
- package/src/components/password-strength/README.md +17 -0
- package/src/components/password-strength/element.ts +22 -0
- package/src/components/password-strength/engine.ts +212 -0
- package/src/components/password-strength/password-strength.css +182 -0
- package/src/components/password-strength/react.tsx +36 -0
- package/src/components/password-strength/score.ts +62 -0
- package/src/components/popover/README.md +44 -0
- package/src/components/popover/element.ts +31 -0
- package/src/components/popover/engine.ts +114 -0
- package/src/components/popover/popover.css +24 -0
- package/src/components/popover/react.tsx +50 -0
- package/src/components/progress/README.md +17 -0
- package/src/components/progress/element.ts +30 -0
- package/src/components/progress/engine.ts +173 -0
- package/src/components/progress/progress.css +139 -0
- package/src/components/progress/react.tsx +35 -0
- package/src/components/questionnaire/README.md +32 -0
- package/src/components/questionnaire/element.ts +118 -0
- package/src/components/questionnaire/engine.ts +255 -0
- package/src/components/questionnaire/questionnaire.css +216 -0
- package/src/components/questionnaire/react.tsx +140 -0
- package/src/components/range-calendar/README.md +58 -0
- package/src/components/range-calendar/element.ts +42 -0
- package/src/components/range-calendar/engine.ts +390 -0
- package/src/components/range-calendar/range-calendar.css +185 -0
- package/src/components/range-calendar/react.tsx +56 -0
- package/src/components/range-slider/README.md +23 -0
- package/src/components/range-slider/element.ts +53 -0
- package/src/components/range-slider/engine.ts +316 -0
- package/src/components/range-slider/range-slider.css +191 -0
- package/src/components/range-slider/react.tsx +66 -0
- package/src/components/resizable-panel/README.md +61 -0
- package/src/components/resizable-panel/element.ts +36 -0
- package/src/components/resizable-panel/engine.ts +551 -0
- package/src/components/resizable-panel/react.tsx +74 -0
- package/src/components/resizable-panel/resizable-panel.css +242 -0
- package/src/components/rich-text-editor/README.md +57 -0
- package/src/components/rich-text-editor/element.ts +34 -0
- package/src/components/rich-text-editor/engine.ts +673 -0
- package/src/components/rich-text-editor/react.tsx +60 -0
- package/src/components/rich-text-editor/rich-text-editor.css +283 -0
- package/src/components/scratch-card/README.md +15 -0
- package/src/components/scratch-card/element.ts +30 -0
- package/src/components/scratch-card/engine.ts +257 -0
- package/src/components/scratch-card/react.tsx +40 -0
- package/src/components/scratch-card/scratch-card.css +74 -0
- package/src/components/skeleton/README.md +15 -0
- package/src/components/skeleton/element.ts +32 -0
- package/src/components/skeleton/engine.ts +104 -0
- package/src/components/skeleton/react.tsx +41 -0
- package/src/components/skeleton/skeleton.css +107 -0
- package/src/components/split-view/README.md +50 -0
- package/src/components/split-view/element.ts +34 -0
- package/src/components/split-view/engine.ts +114 -0
- package/src/components/split-view/react.tsx +63 -0
- package/src/components/split-view/split-view.css +18 -0
- package/src/components/star-rating/README.md +14 -0
- package/src/components/star-rating/element.ts +26 -0
- package/src/components/star-rating/engine.ts +217 -0
- package/src/components/star-rating/react.tsx +33 -0
- package/src/components/star-rating/star-rating.css +79 -0
- package/src/components/stat-card/README.md +42 -0
- package/src/components/stat-card/element.ts +40 -0
- package/src/components/stat-card/engine.ts +283 -0
- package/src/components/stat-card/react.tsx +36 -0
- package/src/components/stat-card/stat-card.css +163 -0
- package/src/components/swipe-actions/README.md +19 -0
- package/src/components/swipe-actions/element.ts +26 -0
- package/src/components/swipe-actions/engine.ts +260 -0
- package/src/components/swipe-actions/react.tsx +80 -0
- package/src/components/swipe-actions/swipe-actions.css +104 -0
- package/src/components/tag-input/README.md +42 -0
- package/src/components/tag-input/element.ts +39 -0
- package/src/components/tag-input/engine.ts +313 -0
- package/src/components/tag-input/react.tsx +54 -0
- package/src/components/tag-input/tag-input.css +243 -0
- package/src/components/time-field/README.md +54 -0
- package/src/components/time-field/element.ts +38 -0
- package/src/components/time-field/engine.ts +110 -0
- package/src/components/time-field/react.tsx +45 -0
- package/src/components/time-field/time-field.css +87 -0
- package/src/components/timeline/README.md +57 -0
- package/src/components/timeline/element.ts +31 -0
- package/src/components/timeline/engine.ts +300 -0
- package/src/components/timeline/react.tsx +59 -0
- package/src/components/timeline/timeline.css +218 -0
- package/src/components/tooltip/README.md +46 -0
- package/src/components/tooltip/element.ts +22 -0
- package/src/components/tooltip/engine.ts +288 -0
- package/src/components/tooltip/react.tsx +30 -0
- package/src/components/tooltip/tooltip.css +78 -0
- package/src/components/tree-view/README.md +61 -0
- package/src/components/tree-view/element.ts +32 -0
- package/src/components/tree-view/engine.ts +569 -0
- package/src/components/tree-view/react.tsx +78 -0
- package/src/components/tree-view/tree-view.css +209 -0
- package/src/components/virtual-list/README.md +81 -0
- package/src/components/virtual-list/element.ts +42 -0
- package/src/components/virtual-list/engine.ts +678 -0
- package/src/components/virtual-list/react.tsx +101 -0
- package/src/components/virtual-list/virtual-list.css +194 -0
- package/src/core/chips.ts +196 -0
- package/src/core/color.ts +304 -0
- package/src/core/date-range.ts +67 -0
- package/src/core/dates.ts +11 -1
- package/src/core/files.ts +107 -0
- package/src/core/filter.ts +229 -0
- package/src/core/gsap.ts +80 -65
- package/src/core/listbox.ts +332 -0
- package/src/core/mark.ts +29 -0
- package/src/core/position.ts +89 -0
- package/src/core/relative-time.ts +77 -0
- package/src/core/segment-field.ts +373 -0
- package/src/core/segments.ts +153 -0
- package/src/core/tree.ts +116 -0
- package/src/core/virtual.ts +79 -0
- package/src/css/tokens.css +186 -182
- package/src/elements/index.ts +147 -0
- package/src/index.ts +72 -0
- package/src/motion/micro.ts +248 -0
- package/src/motion/sheet.ts +88 -65
- package/src/motion/surface.ts +9 -0
- package/src/react/index.ts +57 -0
- package/dist/chunk-IKQ5FODN.js.map +0 -1
- package/dist/chunk-J6OCFNGU.js +0 -177
- package/dist/chunk-J6OCFNGU.js.map +0 -1
- package/dist/chunk-TFNRIZHU.js +0 -3977
- package/dist/chunk-TFNRIZHU.js.map +0 -1
- package/dist/chunk-XAKC2QY7.js.map +0 -1
- package/dist/reveal-jsX-uwUz.d.ts +0 -625
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
# filter-builder
|
|
2
|
+
|
|
3
|
+
Constructor de filtros combinados: condiciones (campo · operador · valor) en grupos que se cumplen con **Y** (todas) u **O** (alguna), anidables hasta `maxDepth` niveles.
|
|
4
|
+
- **Combinador:** el interruptor Y/O es un control segmentado con un indicador que se desliza (.45 s), y los conectores entre condiciones ruedan a la letra nueva.
|
|
5
|
+
- **Añadir y quitar:** una condición o un grupo nuevo entra con blur, con su campo enfocado; uno que se quita se pliega.
|
|
6
|
+
- **Valor según el tipo:** el control sigue el tipo del campo (texto, número, fecha, lista, sí/no) y el operador decide cuántos valores pide («entre» pide dos). Un número que no lo es se sacude.
|
|
7
|
+
|
|
8
|
+
Cada cambio entrega una copia del filtro (`onChange`) y la frase que lo lee («Mostrar donde Estado es Activo y (Monto mayor que 100 o VIP es sí).»).
|
|
9
|
+
|
|
10
|
+
La lógica pura está en `core/filter.ts` y se exporta: `matchesFilter(registro, filtro, campos)` aplica el filtro a tus datos (texto sin mayúsculas ni tildes, fechas por día, condiciones incompletas no filtran) y `describeFilter(filtro, campos)` lo escribe en español.
|
|
11
|
+
|
|
12
|
+
## HTML / Astro
|
|
13
|
+
|
|
14
|
+
```html
|
|
15
|
+
<mk-filter-builder class="mk-filter-builder" fields='[
|
|
16
|
+
{"key":"estado","label":"Estado","type":"select","options":[{"value":"activo","label":"Activo"},{"value":"pausado","label":"Pausado"}]},
|
|
17
|
+
{"key":"monto","label":"Monto","type":"number"},
|
|
18
|
+
{"key":"alta","label":"Fecha de alta","type":"date"},
|
|
19
|
+
{"key":"cliente","label":"Cliente"}]'></mk-filter-builder>
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
## React
|
|
23
|
+
|
|
24
|
+
```tsx
|
|
25
|
+
const [filter, setFilter] = useState<FilterGroup>();
|
|
26
|
+
<FilterBuilder fields={FIELDS} value={filter} onChange={setFilter} />
|
|
27
|
+
const visible = rows.filter((row) => !filter || matchesFilter(row, filter, FIELDS));
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
## Opciones
|
|
31
|
+
|
|
32
|
+
| Opción | Atributo | Por defecto | Qué hace |
|
|
33
|
+
|---|---|---|---|
|
|
34
|
+
| `fields` | `fields` (JSON) | — | `{ key, label, type?, options? }[]`; `type`: `text`, `number`, `date`, `select`, `boolean`. |
|
|
35
|
+
| `value` | `value` (JSON) | grupo vacío | `{ id, combinator: 'and' \| 'or', rules: [...] }`. |
|
|
36
|
+
| `maxDepth` | `max-depth` | `2` | Niveles de grupos (2: grupos dentro del principal). |
|
|
37
|
+
| `label` | `label` | `'Filtros'` | Nombre accesible. |
|
|
38
|
+
| `summary` | `summary` | `true` | La frase bajo el constructor. |
|
|
39
|
+
| `onChange(filter)` | — | — | Cada cambio (una copia). |
|
|
40
|
+
|
|
41
|
+
## Operadores
|
|
42
|
+
|
|
43
|
+
Texto: contiene, no contiene, es, no es, empieza por, termina en, está vacío, no está vacío. Número: igual a, distinto de, mayor que, mayor o igual que, menor que, menor o igual que, entre, vacío. Fecha: es el, antes del, después del, entre, vacío. Lista: es, no es, vacío. Sí/no: es sí, es no.
|
|
44
|
+
|
|
45
|
+
## API y eventos
|
|
46
|
+
|
|
47
|
+
`setValue(filter)`, `clear()`, `value`, `summary`, `destroy()`. Evento `mk:change` con `{ value, summary }`. Con movimiento reducido todo cambia sin animación.
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { MkElement, defineElement, type OptionType } from '../../elements/base';
|
|
2
|
+
import { createFilterBuilder, type FilterBuilderInstance } from './engine';
|
|
3
|
+
import type { FilterField, FilterGroup } from '../../core/filter';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* <mk-filter-builder class="mk-filter-builder"
|
|
7
|
+
* fields='[{"key":"estado","label":"Estado","type":"select","options":[{"value":"activo","label":"Activo"}]},{"key":"monto","label":"Monto","type":"number"}]'>
|
|
8
|
+
* </mk-filter-builder>
|
|
9
|
+
* Emits mk:change ({value, summary}).
|
|
10
|
+
*/
|
|
11
|
+
export class MkFilterBuilderElement extends MkElement<FilterBuilderInstance> {
|
|
12
|
+
static options: Record<string, OptionType> = { fields: 'json', value: 'json', maxDepth: 'number', label: 'string', summary: 'boolean' };
|
|
13
|
+
|
|
14
|
+
protected create(options: Record<string, unknown>) {
|
|
15
|
+
return createFilterBuilder(this, options as { fields?: FilterField[] });
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
attributeChangedCallback(name: string, previous: string | null, next: string | null) {
|
|
19
|
+
if (previous === next || !this.instance) return;
|
|
20
|
+
if (name === 'value' && next) {
|
|
21
|
+
try {
|
|
22
|
+
this.instance.setValue(JSON.parse(next) as FilterGroup);
|
|
23
|
+
} catch {
|
|
24
|
+
// Malformed JSON: keep the current filter.
|
|
25
|
+
}
|
|
26
|
+
} else super.attributeChangedCallback(name, previous, next);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
get value() {
|
|
30
|
+
return this.instance?.value ?? null;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export const defineFilterBuilder = () => defineElement('mk-filter-builder', MkFilterBuilderElement);
|
|
@@ -0,0 +1,365 @@
|
|
|
1
|
+
import { gsap, registerMotion } from '../../core/gsap';
|
|
2
|
+
import { reduced } from '../../core/reduced';
|
|
3
|
+
import { createScope } from '../../core/scope';
|
|
4
|
+
import { inert, type Instance } from '../../core/types';
|
|
5
|
+
import {
|
|
6
|
+
cloneFilter, describeFilter, emptyFilter, filterId, isGroup, newRule, operatorOf, operatorsFor, typeOf,
|
|
7
|
+
type FilterField, type FilterGroup, type FilterNode, type FilterRule,
|
|
8
|
+
} from '../../core/filter';
|
|
9
|
+
import { blurIn, collapse, press, rollDigits, shake } from '../../motion/micro';
|
|
10
|
+
import { slideIndicator } from '../../motion/indicator';
|
|
11
|
+
|
|
12
|
+
/*
|
|
13
|
+
* Filter builder: conditions (field · operator · value) in groups combined
|
|
14
|
+
* with Y (all) or O (any), nested up to maxDepth. The Y/O switch is a
|
|
15
|
+
* segmented control whose indicator slides (.45 s, signature) while the
|
|
16
|
+
* connectors between the conditions roll to the new letter; a new condition or
|
|
17
|
+
* group drops in out of a blur (y −20, blur 16) with its field focused; a
|
|
18
|
+
* removed one folds away (height to 0, blur). The value control follows the
|
|
19
|
+
* field's type (text, number, date, a list, yes/no) and the operator's arity
|
|
20
|
+
* ("entre" takes two); a number that is not one shakes. Every change reports
|
|
21
|
+
* a copy of the filter (onChange) and the sentence that reads it.
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
export interface FilterBuilderOptions {
|
|
25
|
+
/** The fields to filter by (without them the builder stays inert). */
|
|
26
|
+
fields?: FilterField[];
|
|
27
|
+
value?: FilterGroup;
|
|
28
|
+
/** Levels of groups (default 2: groups inside the main one). */
|
|
29
|
+
maxDepth?: number;
|
|
30
|
+
/** Accessible name (default 'Filtros'). */
|
|
31
|
+
label?: string;
|
|
32
|
+
/** The sentence under the builder (default true). */
|
|
33
|
+
summary?: boolean;
|
|
34
|
+
onChange?: (filter: FilterGroup) => void;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export interface FilterBuilderInstance extends Instance {
|
|
38
|
+
setValue(filter: FilterGroup): void;
|
|
39
|
+
clear(): void;
|
|
40
|
+
readonly value: FilterGroup;
|
|
41
|
+
readonly summary: string;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const CLOSE = '<svg aria-hidden="true" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round"><path d="M7 7l10 10M17 7L7 17"/></svg>';
|
|
45
|
+
const COMBINATOR = { and: { short: 'Y', long: 'todas las condiciones', connector: 'y' }, or: { short: 'O', long: 'alguna condición', connector: 'o' } } as const;
|
|
46
|
+
|
|
47
|
+
const el = <K extends keyof HTMLElementTagNameMap>(tag: K, attrs: Record<string, string> = {}, text?: string) => {
|
|
48
|
+
const element = document.createElement(tag);
|
|
49
|
+
for (const [name, value] of Object.entries(attrs)) element.setAttribute(name, value);
|
|
50
|
+
if (text !== undefined) element.textContent = text;
|
|
51
|
+
return element;
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
export function createFilterBuilder(root: HTMLElement | null, options: FilterBuilderOptions = {}): FilterBuilderInstance {
|
|
55
|
+
if (!root || !options.fields?.length) return inert({ setValue() {}, clear() {}, value: emptyFilter(), summary: '' });
|
|
56
|
+
registerMotion();
|
|
57
|
+
const scope = createScope();
|
|
58
|
+
const fields = options.fields;
|
|
59
|
+
const maxDepth = Number.isFinite(options.maxDepth) && options.maxDepth! >= 1 ? Math.floor(options.maxDepth!) : 2;
|
|
60
|
+
let model: FilterGroup = options.value ? cloneFilter(options.value) : emptyFilter();
|
|
61
|
+
const effects = new Set<gsap.core.Animation>();
|
|
62
|
+
const keep = (animation: gsap.core.Animation | null) => {
|
|
63
|
+
if (!animation) return;
|
|
64
|
+
effects.add(animation);
|
|
65
|
+
// Finished ones leave as new ones arrive: the animation's own onComplete stays untouched.
|
|
66
|
+
for (const effect of effects) if (effect.totalProgress() === 1) effects.delete(effect);
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
const tree = el('div', { 'data-mk-fb-tree': '', role: 'group', 'aria-label': options.label ?? 'Filtros' });
|
|
70
|
+
const summary = el('p', { 'data-mk-fb-summary': '', 'aria-live': 'polite' });
|
|
71
|
+
summary.hidden = options.summary === false;
|
|
72
|
+
root.append(tree, summary);
|
|
73
|
+
scope.add(() => {
|
|
74
|
+
for (const effect of effects) effect.kill();
|
|
75
|
+
effects.clear();
|
|
76
|
+
tree.remove();
|
|
77
|
+
summary.remove();
|
|
78
|
+
});
|
|
79
|
+
scope.attr(root, 'data-mk-ready', '');
|
|
80
|
+
|
|
81
|
+
const fieldOf = (key: string) => fields.find((field) => field.key === key);
|
|
82
|
+
/** The node with `id` and the group holding it. */
|
|
83
|
+
const find = (id: string, group: FilterGroup = model, parent: FilterGroup | null = null): { node: FilterNode; parent: FilterGroup | null } | null => {
|
|
84
|
+
if (group.id === id) return { node: group, parent };
|
|
85
|
+
for (const node of group.rules) {
|
|
86
|
+
if (node.id === id) return { node, parent: group };
|
|
87
|
+
if (isGroup(node)) {
|
|
88
|
+
const found = find(id, node, group);
|
|
89
|
+
if (found) return found;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
return null;
|
|
93
|
+
};
|
|
94
|
+
|
|
95
|
+
const describe = () => {
|
|
96
|
+
const text = describeFilter(model, fields);
|
|
97
|
+
summary.textContent = text ? `Mostrar donde ${text}.` : 'Sin filtros: se muestra todo.';
|
|
98
|
+
};
|
|
99
|
+
const emit = () => {
|
|
100
|
+
describe();
|
|
101
|
+
options.onChange?.(cloneFilter(model));
|
|
102
|
+
root.dispatchEvent(new CustomEvent('mk:change', { bubbles: true, detail: { value: cloneFilter(model), summary: describeFilter(model, fields) } }));
|
|
103
|
+
};
|
|
104
|
+
|
|
105
|
+
// ── Value controls ───────────────────────────────────────────────────────
|
|
106
|
+
const valueControl = (rule: FilterRule, index: 0 | 1) => {
|
|
107
|
+
const field = fieldOf(rule.field);
|
|
108
|
+
const type = typeOf(field);
|
|
109
|
+
const current = Array.isArray(rule.value) ? rule.value[index] : rule.value;
|
|
110
|
+
let control: HTMLInputElement | HTMLSelectElement;
|
|
111
|
+
if (type === 'select') {
|
|
112
|
+
control = el('select');
|
|
113
|
+
control.append(el('option', { value: '' }, 'Elige…'), ...(field?.options ?? []).map((option) => el('option', { value: option.value }, option.label)));
|
|
114
|
+
} else {
|
|
115
|
+
control = el('input');
|
|
116
|
+
control.type = type === 'number' ? 'text' : type === 'date' ? 'date' : 'text';
|
|
117
|
+
if (type === 'number') control.inputMode = 'decimal';
|
|
118
|
+
control.placeholder = type === 'number' ? '0' : 'Valor';
|
|
119
|
+
}
|
|
120
|
+
control.dataset.mkFbValue = String(index);
|
|
121
|
+
control.setAttribute('aria-label', index === 1 ? 'Hasta' : operatorOf(field, rule.operator).arity === 2 ? 'Desde' : 'Valor');
|
|
122
|
+
control.value = current === null || current === undefined ? '' : String(current);
|
|
123
|
+
return control;
|
|
124
|
+
};
|
|
125
|
+
|
|
126
|
+
const fillValues = (row: HTMLElement, rule: FilterRule) => {
|
|
127
|
+
const host = row.querySelector<HTMLElement>('[data-mk-fb-values]')!;
|
|
128
|
+
const { arity } = operatorOf(fieldOf(rule.field), rule.operator);
|
|
129
|
+
host.replaceChildren();
|
|
130
|
+
if (arity === 0) return;
|
|
131
|
+
host.append(valueControl(rule, 0));
|
|
132
|
+
if (arity === 2) host.append(el('span', { 'data-mk-fb-and': '', 'aria-hidden': 'true' }, 'y'), valueControl(rule, 1));
|
|
133
|
+
};
|
|
134
|
+
|
|
135
|
+
const fillOperators = (row: HTMLElement, rule: FilterRule) => {
|
|
136
|
+
const select = row.querySelector<HTMLSelectElement>('[data-mk-fb-operator]')!;
|
|
137
|
+
select.replaceChildren(...operatorsFor(fieldOf(rule.field)).map((operator) => el('option', { value: operator.id }, operator.label)));
|
|
138
|
+
select.value = rule.operator;
|
|
139
|
+
};
|
|
140
|
+
|
|
141
|
+
const ruleRow = (rule: FilterRule) => {
|
|
142
|
+
const row = el('div', { 'data-mk-fb-rule': '', 'data-id': rule.id, role: 'group', 'aria-label': 'Condición' });
|
|
143
|
+
const field = el('select', { 'data-mk-fb-field': '', 'aria-label': 'Campo' });
|
|
144
|
+
field.append(...fields.map((candidate) => el('option', { value: candidate.key }, candidate.label)));
|
|
145
|
+
field.value = rule.field;
|
|
146
|
+
const operator = el('select', { 'data-mk-fb-operator': '', 'aria-label': 'Operador' });
|
|
147
|
+
const values = el('span', { 'data-mk-fb-values': '' });
|
|
148
|
+
const remove = el('button', { type: 'button', 'data-mk-fb-remove': '', 'aria-label': 'Quitar condición' });
|
|
149
|
+
remove.innerHTML = CLOSE;
|
|
150
|
+
row.append(field, operator, values, remove);
|
|
151
|
+
fillOperators(row, rule);
|
|
152
|
+
fillValues(row, rule);
|
|
153
|
+
return row;
|
|
154
|
+
};
|
|
155
|
+
|
|
156
|
+
const connector = (group: FilterGroup) => el('span', { 'data-mk-fb-connector': '', 'aria-hidden': 'true' }, COMBINATOR[group.combinator].connector);
|
|
157
|
+
|
|
158
|
+
const groupBox = (group: FilterGroup, depth: number): HTMLElement => {
|
|
159
|
+
const box = el('div', { 'data-mk-fb-group': '', 'data-id': group.id, 'data-depth': String(depth), role: 'group', 'aria-label': depth ? 'Grupo de condiciones' : 'Condiciones' });
|
|
160
|
+
const head = el('div', { 'data-mk-fb-head': '' });
|
|
161
|
+
const switcher = el('div', { 'data-mk-fb-combinator': '', role: 'radiogroup', 'aria-label': 'Cumplir' });
|
|
162
|
+
const indicator = el('span', { 'data-mk-fb-indicator': '', 'aria-hidden': 'true' });
|
|
163
|
+
switcher.append(indicator);
|
|
164
|
+
for (const kind of ['and', 'or'] as const) {
|
|
165
|
+
const option = el('button', { type: 'button', role: 'radio', 'data-combinator': kind, 'aria-checked': String(group.combinator === kind), 'aria-label': COMBINATOR[kind].long }, COMBINATOR[kind].short);
|
|
166
|
+
option.tabIndex = group.combinator === kind ? 0 : -1;
|
|
167
|
+
switcher.append(option);
|
|
168
|
+
}
|
|
169
|
+
const caption = el('span', { 'data-mk-fb-caption': '' }, `Cumplir ${COMBINATOR[group.combinator].long}`);
|
|
170
|
+
head.append(switcher, caption);
|
|
171
|
+
if (depth > 0) {
|
|
172
|
+
const removeGroup = el('button', { type: 'button', 'data-mk-fb-remove-group': '', 'aria-label': 'Quitar grupo' });
|
|
173
|
+
removeGroup.innerHTML = CLOSE;
|
|
174
|
+
head.append(removeGroup);
|
|
175
|
+
}
|
|
176
|
+
const rules = el('div', { 'data-mk-fb-rules': '' });
|
|
177
|
+
group.rules.forEach((node, i) => {
|
|
178
|
+
if (i) rules.append(connector(group));
|
|
179
|
+
rules.append(isGroup(node) ? groupBox(node, depth + 1) : ruleRow(node));
|
|
180
|
+
});
|
|
181
|
+
const actions = el('div', { 'data-mk-fb-actions': '' });
|
|
182
|
+
actions.append(el('button', { type: 'button', 'data-mk-fb-add-rule': '' }, '+ Condición'));
|
|
183
|
+
// A group at depth d holds another while it stays within maxDepth levels.
|
|
184
|
+
if (depth + 1 < maxDepth) actions.append(el('button', { type: 'button', 'data-mk-fb-add-group': '' }, '+ Grupo'));
|
|
185
|
+
box.append(head, rules, actions);
|
|
186
|
+
return box;
|
|
187
|
+
};
|
|
188
|
+
|
|
189
|
+
/** The indicator of every switch sits under its chosen option. */
|
|
190
|
+
const placeIndicators = (immediate: boolean) => {
|
|
191
|
+
for (const switcher of tree.querySelectorAll<HTMLElement>('[data-mk-fb-combinator]')) {
|
|
192
|
+
const chosen = switcher.querySelector<HTMLElement>('[aria-checked="true"]');
|
|
193
|
+
keep(slideIndicator(switcher.querySelector<HTMLElement>('[data-mk-fb-indicator]')!, chosen, { immediate }));
|
|
194
|
+
}
|
|
195
|
+
};
|
|
196
|
+
|
|
197
|
+
const render = () => {
|
|
198
|
+
tree.replaceChildren(groupBox(model, 0));
|
|
199
|
+
placeIndicators(true);
|
|
200
|
+
describe();
|
|
201
|
+
};
|
|
202
|
+
|
|
203
|
+
const boxOf = (id: string) => tree.querySelector<HTMLElement>(`[data-mk-fb-group][data-id="${id}"]`);
|
|
204
|
+
const rowOf = (id: string) => tree.querySelector<HTMLElement>(`[data-mk-fb-rule][data-id="${id}"]`);
|
|
205
|
+
const ruleOf = (element: Element) => find(element.closest<HTMLElement>('[data-mk-fb-rule]')!.dataset.id!)!.node as FilterRule;
|
|
206
|
+
|
|
207
|
+
const add = (group: FilterGroup, node: FilterNode) => {
|
|
208
|
+
const box = boxOf(group.id)!;
|
|
209
|
+
const rules = box.querySelector<HTMLElement>(':scope > [data-mk-fb-rules]')!;
|
|
210
|
+
group.rules.push(node);
|
|
211
|
+
const depth = Number(box.dataset.depth);
|
|
212
|
+
const element = isGroup(node) ? groupBox(node, depth + 1) : ruleRow(node);
|
|
213
|
+
const parts: HTMLElement[] = [];
|
|
214
|
+
if (group.rules.length > 1) {
|
|
215
|
+
const link = connector(group);
|
|
216
|
+
rules.append(link);
|
|
217
|
+
parts.push(link);
|
|
218
|
+
}
|
|
219
|
+
rules.append(element);
|
|
220
|
+
parts.push(element);
|
|
221
|
+
if (isGroup(node)) placeIndicators(true);
|
|
222
|
+
keep(blurIn(parts, { from: 'rise', stagger: 0.04 }));
|
|
223
|
+
element.querySelector<HTMLElement>('select, input')?.focus();
|
|
224
|
+
emit();
|
|
225
|
+
};
|
|
226
|
+
|
|
227
|
+
const removeNode = (id: string) => {
|
|
228
|
+
const found = find(id);
|
|
229
|
+
if (!found?.parent) return;
|
|
230
|
+
const { parent } = found;
|
|
231
|
+
const element = tree.querySelector<HTMLElement>(`[data-id="${id}"]`)!;
|
|
232
|
+
const index = parent.rules.findIndex((node) => node.id === id);
|
|
233
|
+
parent.rules.splice(index, 1);
|
|
234
|
+
// The connector before it (or after it, for the first) goes too.
|
|
235
|
+
const link = (element.previousElementSibling?.matches('[data-mk-fb-connector]') ? element.previousElementSibling : element.nextElementSibling?.matches('[data-mk-fb-connector]') ? element.nextElementSibling : null) as HTMLElement | null;
|
|
236
|
+
const box = boxOf(parent.id)!;
|
|
237
|
+
const focusAfter = box.querySelector<HTMLElement>(':scope > [data-mk-fb-actions] button');
|
|
238
|
+
const gone = () => {
|
|
239
|
+
element.remove();
|
|
240
|
+
link?.remove();
|
|
241
|
+
};
|
|
242
|
+
if (reduced()) gone();
|
|
243
|
+
else {
|
|
244
|
+
keep(collapse(element, { onComplete: gone }));
|
|
245
|
+
if (link) keep(collapse(link));
|
|
246
|
+
}
|
|
247
|
+
focusAfter?.focus();
|
|
248
|
+
emit();
|
|
249
|
+
};
|
|
250
|
+
|
|
251
|
+
const setCombinator = (group: FilterGroup, kind: 'and' | 'or') => {
|
|
252
|
+
if (group.combinator === kind) return;
|
|
253
|
+
group.combinator = kind;
|
|
254
|
+
const box = boxOf(group.id)!;
|
|
255
|
+
const switcher = box.querySelector<HTMLElement>(':scope > [data-mk-fb-head] [data-mk-fb-combinator]')!;
|
|
256
|
+
for (const option of switcher.querySelectorAll<HTMLElement>('[role="radio"]')) {
|
|
257
|
+
const on = option.dataset.combinator === kind;
|
|
258
|
+
option.setAttribute('aria-checked', String(on));
|
|
259
|
+
option.tabIndex = on ? 0 : -1;
|
|
260
|
+
}
|
|
261
|
+
keep(slideIndicator(switcher.querySelector<HTMLElement>('[data-mk-fb-indicator]')!, switcher.querySelector<HTMLElement>('[aria-checked="true"]')));
|
|
262
|
+
box.querySelector<HTMLElement>(':scope > [data-mk-fb-head] [data-mk-fb-caption]')!.textContent = `Cumplir ${COMBINATOR[kind].long}`;
|
|
263
|
+
const links = [...box.querySelectorAll<HTMLElement>(':scope > [data-mk-fb-rules] > [data-mk-fb-connector]')];
|
|
264
|
+
links.forEach((link, i) => {
|
|
265
|
+
if (reduced()) link.textContent = COMBINATOR[kind].connector;
|
|
266
|
+
else keep(gsap.delayedCall(i * 0.03, () => keep(rollDigits(link, COMBINATOR[kind].connector, kind === 'or' ? 1 : -1, 0.3))));
|
|
267
|
+
});
|
|
268
|
+
emit();
|
|
269
|
+
};
|
|
270
|
+
|
|
271
|
+
scope.on(tree, 'click', (event) => {
|
|
272
|
+
const target = event.target as Element | null;
|
|
273
|
+
const button = target?.closest<HTMLElement>('button');
|
|
274
|
+
if (!button || !tree.contains(button)) return;
|
|
275
|
+
const box = button.closest<HTMLElement>('[data-mk-fb-group]')!;
|
|
276
|
+
const group = find(box.dataset.id!)!.node as FilterGroup;
|
|
277
|
+
if (button.matches('[data-combinator]')) return setCombinator(group, button.dataset.combinator as 'and' | 'or');
|
|
278
|
+
keep(press(button));
|
|
279
|
+
if (button.matches('[data-mk-fb-add-rule]')) add(group, newRule(fields[0]!));
|
|
280
|
+
else if (button.matches('[data-mk-fb-add-group]')) add(group, { id: filterId(), combinator: 'and', rules: [newRule(fields[0]!)] });
|
|
281
|
+
else if (button.matches('[data-mk-fb-remove]')) removeNode(button.closest<HTMLElement>('[data-mk-fb-rule]')!.dataset.id!);
|
|
282
|
+
else if (button.matches('[data-mk-fb-remove-group]')) removeNode(box.dataset.id!);
|
|
283
|
+
});
|
|
284
|
+
// Arrows move the Y/O choice, like radios.
|
|
285
|
+
scope.on(tree, 'keydown', (event) => {
|
|
286
|
+
const key = event as KeyboardEvent;
|
|
287
|
+
const option = (key.target as Element | null)?.closest<HTMLElement>('[data-combinator]');
|
|
288
|
+
if (!option || !['ArrowLeft', 'ArrowRight', 'ArrowUp', 'ArrowDown'].includes(key.key)) return;
|
|
289
|
+
key.preventDefault();
|
|
290
|
+
const group = find(option.closest<HTMLElement>('[data-mk-fb-group]')!.dataset.id!)!.node as FilterGroup;
|
|
291
|
+
const next = group.combinator === 'and' ? 'or' : 'and';
|
|
292
|
+
setCombinator(group, next);
|
|
293
|
+
option.parentElement!.querySelector<HTMLElement>(`[data-combinator="${next}"]`)!.focus();
|
|
294
|
+
});
|
|
295
|
+
scope.on(tree, 'change', (event) => {
|
|
296
|
+
const target = event.target as HTMLElement;
|
|
297
|
+
const row = target.closest<HTMLElement>('[data-mk-fb-rule]');
|
|
298
|
+
if (!row) return;
|
|
299
|
+
const rule = ruleOf(row);
|
|
300
|
+
if (target.matches('[data-mk-fb-field]')) {
|
|
301
|
+
const fresh = newRule(fieldOf((target as HTMLSelectElement).value)!);
|
|
302
|
+
rule.field = fresh.field;
|
|
303
|
+
rule.operator = fresh.operator;
|
|
304
|
+
rule.value = fresh.value;
|
|
305
|
+
fillOperators(row, rule);
|
|
306
|
+
fillValues(row, rule);
|
|
307
|
+
const values = row.querySelector<HTMLElement>('[data-mk-fb-values]')!;
|
|
308
|
+
if (values.children.length && !reduced()) keep(blurIn([...values.children], { from: 'soft', stagger: 0.03 }));
|
|
309
|
+
} else if (target.matches('[data-mk-fb-operator]')) {
|
|
310
|
+
const before = operatorOf(fieldOf(rule.field), rule.operator).arity;
|
|
311
|
+
rule.operator = (target as HTMLSelectElement).value;
|
|
312
|
+
const after = operatorOf(fieldOf(rule.field), rule.operator).arity;
|
|
313
|
+
if (before !== after) {
|
|
314
|
+
rule.value = after === 2 ? [Array.isArray(rule.value) ? rule.value[0] : (rule.value ?? null), null] : after === 1 ? (Array.isArray(rule.value) ? rule.value[0] : (rule.value ?? null)) : null;
|
|
315
|
+
fillValues(row, rule);
|
|
316
|
+
const values = row.querySelector<HTMLElement>('[data-mk-fb-values]')!;
|
|
317
|
+
if (values.children.length && !reduced()) keep(blurIn([...values.children], { from: 'soft', stagger: 0.03 }));
|
|
318
|
+
}
|
|
319
|
+
} else if (target.matches('[data-mk-fb-value]') && target instanceof HTMLSelectElement) {
|
|
320
|
+
rule.value = target.value || null;
|
|
321
|
+
} else return;
|
|
322
|
+
emit();
|
|
323
|
+
});
|
|
324
|
+
scope.on(tree, 'input', (event) => {
|
|
325
|
+
const target = event.target as HTMLElement;
|
|
326
|
+
if (!(target instanceof HTMLInputElement) || !target.matches('[data-mk-fb-value]')) return;
|
|
327
|
+
const rule = ruleOf(target);
|
|
328
|
+
const field = fieldOf(rule.field);
|
|
329
|
+
const text = target.value;
|
|
330
|
+
const number = typeOf(field) === 'number';
|
|
331
|
+
const parsed = number ? (text.trim() === '' ? null : Number(text.replace(',', '.'))) : text;
|
|
332
|
+
if (number && parsed !== null && Number.isNaN(parsed)) target.setAttribute('aria-invalid', 'true');
|
|
333
|
+
else target.removeAttribute('aria-invalid');
|
|
334
|
+
const value = number && (parsed === null || Number.isNaN(parsed as number)) ? null : (parsed as string | number | null);
|
|
335
|
+
if (Array.isArray(rule.value)) rule.value[target.dataset.mkFbValue === '1' ? 1 : 0] = value;
|
|
336
|
+
else rule.value = value;
|
|
337
|
+
emit();
|
|
338
|
+
});
|
|
339
|
+
scope.on(tree, 'focusout', (event) => {
|
|
340
|
+
const target = event.target as HTMLElement;
|
|
341
|
+
if (target.getAttribute('aria-invalid') === 'true') keep(shake(target));
|
|
342
|
+
});
|
|
343
|
+
|
|
344
|
+
render();
|
|
345
|
+
|
|
346
|
+
return {
|
|
347
|
+
setValue(next) {
|
|
348
|
+
if (scope.disposed) return;
|
|
349
|
+
model = cloneFilter(next);
|
|
350
|
+
render();
|
|
351
|
+
},
|
|
352
|
+
clear() {
|
|
353
|
+
model = emptyFilter(model.combinator);
|
|
354
|
+
render();
|
|
355
|
+
emit();
|
|
356
|
+
},
|
|
357
|
+
get value() {
|
|
358
|
+
return cloneFilter(model);
|
|
359
|
+
},
|
|
360
|
+
get summary() {
|
|
361
|
+
return describeFilter(model, fields);
|
|
362
|
+
},
|
|
363
|
+
destroy: () => scope.dispose(),
|
|
364
|
+
};
|
|
365
|
+
}
|
|
@@ -0,0 +1,231 @@
|
|
|
1
|
+
/* filter-builder — the main group bare, nested groups as 16 px cards on surface-2 with an accent rail; each condition a
|
|
2
|
+
wrapping row of 12 px controls on surface-3; the Y/O switch a 10 px segmented control whose indicator slides; small
|
|
3
|
+
round connectors ("y" / "o") between the conditions; the sentence that reads the filter under it. */
|
|
4
|
+
.mk-filter-builder,
|
|
5
|
+
mk-filter-builder {
|
|
6
|
+
display: grid;
|
|
7
|
+
gap: 0.75rem;
|
|
8
|
+
min-width: 0;
|
|
9
|
+
color: var(--mk-text);
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
[data-mk-fb-group] {
|
|
13
|
+
display: grid;
|
|
14
|
+
gap: 0.6rem;
|
|
15
|
+
min-width: 0;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
[data-mk-fb-group]:not([data-depth='0']) {
|
|
19
|
+
padding: 0.75rem;
|
|
20
|
+
border-radius: 16px;
|
|
21
|
+
background: var(--mk-surface-2);
|
|
22
|
+
box-shadow: inset 3px 0 0 color-mix(in srgb, var(--mk-accent) 60%, transparent);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
[data-mk-fb-head] {
|
|
26
|
+
display: flex;
|
|
27
|
+
flex-wrap: wrap;
|
|
28
|
+
align-items: center;
|
|
29
|
+
gap: 0.5rem 0.75rem;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
[data-mk-fb-combinator] {
|
|
33
|
+
position: relative;
|
|
34
|
+
display: inline-flex;
|
|
35
|
+
flex: none;
|
|
36
|
+
padding: 3px;
|
|
37
|
+
border-radius: 10px;
|
|
38
|
+
background: var(--mk-surface-3);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
[data-mk-fb-indicator] {
|
|
42
|
+
position: absolute;
|
|
43
|
+
top: 0;
|
|
44
|
+
left: 0;
|
|
45
|
+
border-radius: 8px;
|
|
46
|
+
background: var(--mk-surface-1);
|
|
47
|
+
box-shadow: 0 1px 3px color-mix(in srgb, var(--mk-shadow-color, #000) 18%, transparent);
|
|
48
|
+
pointer-events: none;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
[data-mk-fb-combinator] [role='radio'] {
|
|
52
|
+
position: relative;
|
|
53
|
+
z-index: 1;
|
|
54
|
+
min-width: 2.25rem;
|
|
55
|
+
height: 1.9rem;
|
|
56
|
+
padding: 0 0.6rem;
|
|
57
|
+
border: 0;
|
|
58
|
+
border-radius: 8px;
|
|
59
|
+
background: transparent;
|
|
60
|
+
color: var(--mk-muted);
|
|
61
|
+
font: inherit;
|
|
62
|
+
font-size: 0.8125rem;
|
|
63
|
+
font-weight: 700;
|
|
64
|
+
cursor: pointer;
|
|
65
|
+
transition: color 0.2s ease;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
[data-mk-fb-combinator] [role='radio'][aria-checked='true'] {
|
|
69
|
+
color: var(--mk-text);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
[data-mk-fb-caption] {
|
|
73
|
+
color: var(--mk-muted);
|
|
74
|
+
font-size: 0.875rem;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
[data-mk-fb-rules] {
|
|
78
|
+
display: grid;
|
|
79
|
+
gap: 0.4rem;
|
|
80
|
+
min-width: 0;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
[data-mk-fb-rule] {
|
|
84
|
+
display: flex;
|
|
85
|
+
flex-wrap: wrap;
|
|
86
|
+
align-items: center;
|
|
87
|
+
gap: 0.4rem;
|
|
88
|
+
min-width: 0;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
[data-mk-fb-rule] :is(select, input) {
|
|
92
|
+
box-sizing: border-box;
|
|
93
|
+
min-width: 0;
|
|
94
|
+
height: 2.5rem;
|
|
95
|
+
padding: 0 0.7rem;
|
|
96
|
+
border: 0;
|
|
97
|
+
border-radius: 12px;
|
|
98
|
+
background: var(--mk-surface-3);
|
|
99
|
+
color: var(--mk-text);
|
|
100
|
+
font: inherit;
|
|
101
|
+
font-size: 0.875rem;
|
|
102
|
+
outline: none;
|
|
103
|
+
transition: box-shadow 0.2s ease;
|
|
104
|
+
}
|
|
105
|
+
|
|
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
|
+
[data-mk-fb-rule] input {
|
|
118
|
+
width: 9rem;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
[data-mk-fb-rule] :is(select, input):focus-visible {
|
|
122
|
+
box-shadow: 0 0 0 3px color-mix(in srgb, var(--mk-accent) 35%, transparent);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
[data-mk-fb-rule] input[aria-invalid='true'] {
|
|
126
|
+
box-shadow: 0 0 0 2px var(--mk-error, #c62828);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
[data-mk-fb-values] {
|
|
130
|
+
display: inline-flex;
|
|
131
|
+
flex-wrap: wrap;
|
|
132
|
+
align-items: center;
|
|
133
|
+
gap: 0.4rem;
|
|
134
|
+
min-width: 0;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
[data-mk-fb-and] {
|
|
138
|
+
color: var(--mk-muted);
|
|
139
|
+
font-size: 0.8125rem;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
[data-mk-fb-remove],
|
|
143
|
+
[data-mk-fb-remove-group] {
|
|
144
|
+
display: grid;
|
|
145
|
+
flex: none;
|
|
146
|
+
place-items: center;
|
|
147
|
+
width: 2rem;
|
|
148
|
+
height: 2rem;
|
|
149
|
+
padding: 0;
|
|
150
|
+
border: 0;
|
|
151
|
+
border-radius: 8px;
|
|
152
|
+
background: transparent;
|
|
153
|
+
color: var(--mk-muted);
|
|
154
|
+
cursor: pointer;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
[data-mk-fb-remove-group] {
|
|
158
|
+
margin-left: auto;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
:is([data-mk-fb-remove], [data-mk-fb-remove-group]):hover {
|
|
162
|
+
background: color-mix(in srgb, var(--mk-error, #c62828) 12%, transparent);
|
|
163
|
+
color: var(--mk-error, #c62828);
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
[data-mk-fb-connector] {
|
|
167
|
+
justify-self: start;
|
|
168
|
+
display: inline-grid;
|
|
169
|
+
place-items: center;
|
|
170
|
+
min-width: 1.6rem;
|
|
171
|
+
height: 1.4rem;
|
|
172
|
+
padding: 0 0.35rem;
|
|
173
|
+
border-radius: 999px;
|
|
174
|
+
background: var(--mk-accent-soft);
|
|
175
|
+
color: var(--mk-on-accent-soft, var(--mk-text));
|
|
176
|
+
font-size: 0.75rem;
|
|
177
|
+
font-weight: 700;
|
|
178
|
+
text-transform: uppercase;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
[data-mk-fb-actions] {
|
|
182
|
+
display: flex;
|
|
183
|
+
flex-wrap: wrap;
|
|
184
|
+
gap: 0.4rem;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
[data-mk-fb-actions] button {
|
|
188
|
+
height: 2.1rem;
|
|
189
|
+
padding: 0 0.8rem;
|
|
190
|
+
border: 1px dashed var(--mk-outline);
|
|
191
|
+
border-radius: 10px;
|
|
192
|
+
background: transparent;
|
|
193
|
+
color: var(--mk-muted);
|
|
194
|
+
font: inherit;
|
|
195
|
+
font-size: 0.8125rem;
|
|
196
|
+
font-weight: 600;
|
|
197
|
+
cursor: pointer;
|
|
198
|
+
transition: background-color 0.2s ease, color 0.2s ease, border-color 0.2s ease;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
[data-mk-fb-actions] button:hover {
|
|
202
|
+
border-color: var(--mk-accent);
|
|
203
|
+
background: var(--mk-accent-soft);
|
|
204
|
+
color: var(--mk-on-accent-soft, var(--mk-text));
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
:is([data-mk-fb-actions] button, [data-mk-fb-remove], [data-mk-fb-remove-group], [data-mk-fb-combinator] [role='radio']):focus-visible {
|
|
208
|
+
outline: 2px solid var(--mk-accent);
|
|
209
|
+
outline-offset: 2px;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
[data-mk-fb-summary] {
|
|
213
|
+
margin: 0;
|
|
214
|
+
color: var(--mk-muted);
|
|
215
|
+
font-size: 0.875rem;
|
|
216
|
+
line-height: 1.5;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
@media (max-width: 480px) {
|
|
220
|
+
[data-mk-fb-rule] :is(select, input) {
|
|
221
|
+
flex: 1 1 8rem;
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
@media (prefers-reduced-motion: reduce) {
|
|
226
|
+
[data-mk-fb-combinator] [role='radio'],
|
|
227
|
+
[data-mk-fb-actions] button,
|
|
228
|
+
[data-mk-fb-rule] :is(select, input) {
|
|
229
|
+
transition: none;
|
|
230
|
+
}
|
|
231
|
+
}
|