@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,283 @@
|
|
|
1
|
+
import { gsap, EASE, 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 { countTo } from '../../motion/count';
|
|
6
|
+
import { rollDigits } from '../../motion/micro';
|
|
7
|
+
|
|
8
|
+
/*
|
|
9
|
+
* Stat card: a KPI with its trend. The figure (Intl format of the page: money,
|
|
10
|
+
* percent, compact) counts up when the card comes into view (.9 s, flow); the
|
|
11
|
+
* change from `previous` sits in a badge (▲ / ▼ and the percentage, green when
|
|
12
|
+
* it moves the good way — `positive` says which — red when not, neutral when
|
|
13
|
+
* flat) that pops in after it while its arrow nudges; a sparkline of `series`
|
|
14
|
+
* draws itself (.9 s, wipe) with a soft area and an end dot that pops; a
|
|
15
|
+
* `target` fills a bar. update() counts from the shown figure to the new one,
|
|
16
|
+
* rolls the badge's digits, turns the arrow when the trend flips and morphs the
|
|
17
|
+
* sparkline to the new points. The visual figure is aria-hidden: a sentence
|
|
18
|
+
* with the final values speaks for it.
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
export interface StatCardOptions {
|
|
22
|
+
label?: string;
|
|
23
|
+
value?: number;
|
|
24
|
+
/** The value it is compared with (the previous period). */
|
|
25
|
+
previous?: number;
|
|
26
|
+
/** Intl.NumberFormat options (e.g. { style: 'currency', currency: 'MXN' }) or a formatter. */
|
|
27
|
+
format?: Intl.NumberFormatOptions | ((value: number) => string);
|
|
28
|
+
/** Locale for the numbers (default: the page's language). */
|
|
29
|
+
locale?: string;
|
|
30
|
+
/** Which way is good (default 'up'; 'down' for costs, churn, latency). */
|
|
31
|
+
positive?: 'up' | 'down';
|
|
32
|
+
/** What the change compares with (default 'vs. periodo anterior'). */
|
|
33
|
+
comparison?: string;
|
|
34
|
+
/** Values for the sparkline (two or more). */
|
|
35
|
+
series?: number[];
|
|
36
|
+
/** A goal: a bar shows how far the value is. */
|
|
37
|
+
target?: number;
|
|
38
|
+
/** Count when it comes into view (default true); false shows the figure at once. */
|
|
39
|
+
countOnView?: boolean;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export interface StatCardInstance extends Instance {
|
|
43
|
+
update(next: Omit<StatCardOptions, 'countOnView' | 'locale'>): void;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
type Trend = 'up' | 'down' | 'flat';
|
|
47
|
+
const SVG = 'http://www.w3.org/2000/svg';
|
|
48
|
+
const ARROW = '<svg aria-hidden="true" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.6" stroke-linecap="round" stroke-linejoin="round"><path d="M12 19V5M6 11l6-6 6 6"/></svg>';
|
|
49
|
+
|
|
50
|
+
/** Sparkline points in a 100 × 32 box (2 px of air top and bottom). */
|
|
51
|
+
const pointsOf = (series: number[]) => {
|
|
52
|
+
const lo = Math.min(...series);
|
|
53
|
+
const hi = Math.max(...series);
|
|
54
|
+
const span = hi - lo || 1;
|
|
55
|
+
return series.map((value, i) => [series.length === 1 ? 50 : (i / (series.length - 1)) * 100, 30 - ((value - lo) / span) * 28] as [number, number]);
|
|
56
|
+
};
|
|
57
|
+
const pathOf = (points: Array<[number, number]>) => `M${points.map(([x, y]) => `${x.toFixed(2)},${y.toFixed(2)}`).join('L')}`;
|
|
58
|
+
|
|
59
|
+
export function createStatCard(root: HTMLElement | null, initial: StatCardOptions = {}): StatCardInstance {
|
|
60
|
+
if (!root) return inert({ update() {} });
|
|
61
|
+
registerMotion();
|
|
62
|
+
const scope = createScope();
|
|
63
|
+
let options: StatCardOptions = { ...initial };
|
|
64
|
+
const effects = new Set<gsap.core.Animation>();
|
|
65
|
+
const keep = (animation: gsap.core.Animation | null) => {
|
|
66
|
+
if (!animation) return;
|
|
67
|
+
effects.add(animation);
|
|
68
|
+
// Finished ones leave as new ones arrive: the animation's own onComplete stays untouched.
|
|
69
|
+
for (const effect of effects) if (effect.totalProgress() === 1) effects.delete(effect);
|
|
70
|
+
};
|
|
71
|
+
const locale = options.locale ?? root.closest('[lang]')?.getAttribute('lang') ?? undefined;
|
|
72
|
+
const formatter = () => {
|
|
73
|
+
if (typeof options.format === 'function') return options.format;
|
|
74
|
+
let intl: Intl.NumberFormat;
|
|
75
|
+
try {
|
|
76
|
+
intl = new Intl.NumberFormat(locale, { maximumFractionDigits: 1, ...options.format });
|
|
77
|
+
} catch {
|
|
78
|
+
intl = new Intl.NumberFormat(undefined, { maximumFractionDigits: 1 });
|
|
79
|
+
}
|
|
80
|
+
return (value: number) => intl.format(value);
|
|
81
|
+
};
|
|
82
|
+
const percent = new Intl.NumberFormat(locale, { maximumFractionDigits: 1, minimumFractionDigits: 1 });
|
|
83
|
+
|
|
84
|
+
const el = <K extends keyof HTMLElementTagNameMap>(tag: K, attribute: string) => {
|
|
85
|
+
const element = document.createElement(tag);
|
|
86
|
+
element.setAttribute(attribute, '');
|
|
87
|
+
return element;
|
|
88
|
+
};
|
|
89
|
+
const labelEl = el('span', 'data-mk-stat-label');
|
|
90
|
+
const row = el('div', 'data-mk-stat-row');
|
|
91
|
+
const valueEl = el('strong', 'data-mk-stat-value');
|
|
92
|
+
valueEl.setAttribute('aria-hidden', 'true');
|
|
93
|
+
const badge = el('span', 'data-mk-stat-delta');
|
|
94
|
+
badge.setAttribute('aria-hidden', 'true');
|
|
95
|
+
const arrow = el('span', 'data-mk-stat-arrow');
|
|
96
|
+
arrow.innerHTML = ARROW;
|
|
97
|
+
const deltaText = el('span', 'data-mk-stat-delta-text');
|
|
98
|
+
badge.append(arrow, deltaText);
|
|
99
|
+
row.append(valueEl, badge);
|
|
100
|
+
const comparisonEl = el('p', 'data-mk-stat-comparison');
|
|
101
|
+
comparisonEl.setAttribute('aria-hidden', 'true');
|
|
102
|
+
const sparkHost = el('div', 'data-mk-stat-spark');
|
|
103
|
+
sparkHost.setAttribute('aria-hidden', 'true');
|
|
104
|
+
const svg = document.createElementNS(SVG, 'svg');
|
|
105
|
+
svg.setAttribute('viewBox', '0 0 100 32');
|
|
106
|
+
svg.setAttribute('preserveAspectRatio', 'none');
|
|
107
|
+
const area = document.createElementNS(SVG, 'path');
|
|
108
|
+
area.setAttribute('data-mk-stat-area', '');
|
|
109
|
+
const line = document.createElementNS(SVG, 'path');
|
|
110
|
+
line.setAttribute('data-mk-stat-line', '');
|
|
111
|
+
line.setAttribute('vector-effect', 'non-scaling-stroke');
|
|
112
|
+
line.setAttribute('pathLength', '1');
|
|
113
|
+
svg.append(area, line);
|
|
114
|
+
const dot = el('span', 'data-mk-stat-dot');
|
|
115
|
+
sparkHost.append(svg, dot);
|
|
116
|
+
const target = el('div', 'data-mk-stat-target');
|
|
117
|
+
const targetBar = el('span', 'data-mk-stat-target-bar');
|
|
118
|
+
const targetFill = el('span', 'data-mk-stat-target-fill');
|
|
119
|
+
targetBar.append(targetFill);
|
|
120
|
+
const targetText = el('span', 'data-mk-stat-target-text');
|
|
121
|
+
target.append(targetBar, targetText);
|
|
122
|
+
const summary = el('span', 'data-mk-stat-summary');
|
|
123
|
+
summary.className = 'mk-sr-only';
|
|
124
|
+
root.append(labelEl, row, comparisonEl, sparkHost, target, summary);
|
|
125
|
+
scope.add(() => {
|
|
126
|
+
for (const effect of effects) effect.kill();
|
|
127
|
+
effects.clear();
|
|
128
|
+
gsap.killTweensOf([valueEl, badge, arrow, line, area, dot, targetFill]);
|
|
129
|
+
for (const part of [labelEl, row, comparisonEl, sparkHost, target, summary]) part.remove();
|
|
130
|
+
});
|
|
131
|
+
scope.attr(root, 'role', 'group');
|
|
132
|
+
scope.attr(root, 'aria-label', options.label ?? null);
|
|
133
|
+
scope.attr(root, 'data-mk-trend', null);
|
|
134
|
+
scope.attr(root, 'data-mk-good', null);
|
|
135
|
+
scope.attr(root, 'data-mk-ready', '');
|
|
136
|
+
|
|
137
|
+
const trendOf = (): { trend: Trend; change: number | null } => {
|
|
138
|
+
const { value, previous } = options;
|
|
139
|
+
if (typeof value !== 'number' || typeof previous !== 'number' || !Number.isFinite(value) || !Number.isFinite(previous) || previous === 0) return { trend: 'flat', change: null };
|
|
140
|
+
const change = ((value - previous) / Math.abs(previous)) * 100;
|
|
141
|
+
return { trend: Math.abs(change) < 0.05 ? 'flat' : change > 0 ? 'up' : 'down', change };
|
|
142
|
+
};
|
|
143
|
+
|
|
144
|
+
let shownPoints: Array<[number, number]> | null = null;
|
|
145
|
+
const drawSpark = (points: Array<[number, number]>) => {
|
|
146
|
+
line.setAttribute('d', pathOf(points));
|
|
147
|
+
area.setAttribute('d', `${pathOf(points)}L100,32L0,32Z`);
|
|
148
|
+
const [x, y] = points[points.length - 1]!;
|
|
149
|
+
dot.style.left = `${x}%`;
|
|
150
|
+
dot.style.top = `${(y / 32) * 100}%`;
|
|
151
|
+
};
|
|
152
|
+
|
|
153
|
+
/** Write everything but the figure; `animate` runs the update motion. */
|
|
154
|
+
const paint = (animate: boolean, previousTrend: Trend | null) => {
|
|
155
|
+
const format = formatter();
|
|
156
|
+
labelEl.textContent = options.label ?? '';
|
|
157
|
+
labelEl.hidden = !options.label;
|
|
158
|
+
if (options.label) root.setAttribute('aria-label', options.label);
|
|
159
|
+
else root.removeAttribute('aria-label');
|
|
160
|
+
const { trend, change } = trendOf();
|
|
161
|
+
const good = trend === 'flat' ? 'neutral' : (trend === 'up') === ((options.positive ?? 'up') === 'up') ? 'good' : 'bad';
|
|
162
|
+
root.setAttribute('data-mk-trend', trend);
|
|
163
|
+
root.setAttribute('data-mk-good', good);
|
|
164
|
+
badge.hidden = change === null;
|
|
165
|
+
const text = change === null ? '' : `${trend === 'flat' ? '' : trend === 'up' ? '+' : '−'}${percent.format(Math.abs(change))} %`;
|
|
166
|
+
if (animate && !reduced() && deltaText.textContent && deltaText.textContent !== text) keep(rollDigits(deltaText, text, trend === 'down' ? -1 : 1, 0.3));
|
|
167
|
+
else deltaText.textContent = text;
|
|
168
|
+
if (animate && previousTrend && previousTrend !== trend && !reduced()) keep(gsap.fromTo(arrow, { scale: 0.6 }, { scale: 1, duration: 0.35, ease: EASE.click, clearProps: 'scale' }));
|
|
169
|
+
const comparison = options.comparison ?? 'vs. periodo anterior';
|
|
170
|
+
comparisonEl.textContent = typeof options.previous === 'number' ? `${comparison} (${format(options.previous)})` : '';
|
|
171
|
+
comparisonEl.hidden = typeof options.previous !== 'number';
|
|
172
|
+
// Sparkline: draw, or morph from the shown points.
|
|
173
|
+
const series = (options.series ?? []).filter((value) => Number.isFinite(value));
|
|
174
|
+
sparkHost.hidden = series.length < 2;
|
|
175
|
+
if (series.length >= 2) {
|
|
176
|
+
const next = pointsOf(series);
|
|
177
|
+
if (animate && shownPoints && shownPoints.length === next.length && !reduced()) {
|
|
178
|
+
const from = shownPoints.map((point) => [...point]) as Array<[number, number]>;
|
|
179
|
+
const state = { t: 0 };
|
|
180
|
+
keep(
|
|
181
|
+
gsap.to(state, {
|
|
182
|
+
t: 1,
|
|
183
|
+
duration: 0.6,
|
|
184
|
+
ease: EASE.swatch,
|
|
185
|
+
onUpdate: () => drawSpark(from.map(([x, y], i) => [x + (next[i]![0] - x) * state.t, y + (next[i]![1] - y) * state.t])),
|
|
186
|
+
}),
|
|
187
|
+
);
|
|
188
|
+
} else drawSpark(next);
|
|
189
|
+
shownPoints = next;
|
|
190
|
+
} else shownPoints = null;
|
|
191
|
+
// Target bar.
|
|
192
|
+
const goal = options.target;
|
|
193
|
+
const hasTarget = typeof goal === 'number' && goal > 0 && typeof options.value === 'number';
|
|
194
|
+
target.hidden = !hasTarget;
|
|
195
|
+
if (hasTarget) {
|
|
196
|
+
const ratio = Math.max(0, Math.min(1, options.value! / goal!));
|
|
197
|
+
targetText.textContent = `${percent.format(ratio * 100)} % de la meta (${format(goal!)})`;
|
|
198
|
+
if (animate && !reduced()) keep(gsap.to(targetFill, { scaleX: ratio, duration: 0.6, ease: EASE.wipe }));
|
|
199
|
+
else gsap.set(targetFill, { scaleX: ratio });
|
|
200
|
+
}
|
|
201
|
+
// The sentence that speaks for the figure.
|
|
202
|
+
const value = typeof options.value === 'number' ? format(options.value) : '—';
|
|
203
|
+
const said = change === null ? '' : trend === 'flat' ? ` Sin cambio ${comparison}.` : ` ${trend === 'up' ? 'Sube' : 'Baja'} ${percent.format(Math.abs(change))} % ${comparison}.`;
|
|
204
|
+
summary.textContent = `${options.label ? `${options.label}: ` : ''}${value}.${said}`;
|
|
205
|
+
};
|
|
206
|
+
|
|
207
|
+
/** The figure: counted (from `from`) or written. */
|
|
208
|
+
const figure = (from: number | null) => {
|
|
209
|
+
const value = options.value;
|
|
210
|
+
if (typeof value !== 'number') {
|
|
211
|
+
valueEl.textContent = '—';
|
|
212
|
+
return;
|
|
213
|
+
}
|
|
214
|
+
if (from === null || reduced()) {
|
|
215
|
+
countTo(valueEl, value, { from: value, format: formatter() });
|
|
216
|
+
return;
|
|
217
|
+
}
|
|
218
|
+
keep(countTo(valueEl, value, { from, duration: 0.9, format: formatter() }));
|
|
219
|
+
};
|
|
220
|
+
|
|
221
|
+
/** The first appearance: figure counts, badge pops, sparkline draws, target fills. */
|
|
222
|
+
const enter = () => {
|
|
223
|
+
paint(false, null);
|
|
224
|
+
if (reduced() || options.countOnView === false) {
|
|
225
|
+
figure(null);
|
|
226
|
+
return;
|
|
227
|
+
}
|
|
228
|
+
figure(0);
|
|
229
|
+
if (!badge.hidden) {
|
|
230
|
+
keep(gsap.fromTo(badge, { opacity: 0, scale: 0.6, filter: 'blur(4px)' }, { opacity: 1, scale: 1, filter: 'blur(0px)', duration: 0.35, delay: 0.45, ease: EASE.pop, clearProps: 'opacity,transform,filter' }));
|
|
231
|
+
keep(gsap.fromTo(arrow, { y: root.dataset.mkTrend === 'down' ? -4 : 4 }, { y: 0, duration: 0.45, delay: 0.6, ease: EASE.click, clearProps: 'transform' }));
|
|
232
|
+
}
|
|
233
|
+
if (!sparkHost.hidden) {
|
|
234
|
+
keep(gsap.fromTo(line, { strokeDasharray: 1, strokeDashoffset: 1 }, { strokeDashoffset: 0, duration: 0.9, ease: EASE.wipe, clearProps: 'strokeDasharray,strokeDashoffset' }));
|
|
235
|
+
keep(gsap.fromTo(area, { opacity: 0 }, { opacity: 1, duration: 0.5, delay: 0.4, ease: 'power1.out', clearProps: 'opacity' }));
|
|
236
|
+
keep(gsap.fromTo(dot, { scale: 0 }, { scale: 1, duration: 0.3, delay: 0.85, ease: EASE.pop, clearProps: 'scale' }));
|
|
237
|
+
}
|
|
238
|
+
if (!target.hidden) {
|
|
239
|
+
const ratio = gsap.getProperty(targetFill, 'scaleX') as number;
|
|
240
|
+
keep(gsap.fromTo(targetFill, { scaleX: 0 }, { scaleX: ratio, duration: 0.8, delay: 0.2, ease: EASE.wipe }));
|
|
241
|
+
}
|
|
242
|
+
};
|
|
243
|
+
|
|
244
|
+
let entered = false;
|
|
245
|
+
if (typeof IntersectionObserver === 'function' && options.countOnView !== false && !reduced()) {
|
|
246
|
+
paint(false, null);
|
|
247
|
+
valueEl.textContent = formatter()(0);
|
|
248
|
+
gsap.set([badge, dot], { opacity: 0 });
|
|
249
|
+
const observer = scope.observe(
|
|
250
|
+
new IntersectionObserver((entries) => {
|
|
251
|
+
if (entered || !entries.some((entry) => entry.isIntersecting)) return;
|
|
252
|
+
entered = true;
|
|
253
|
+
gsap.set([badge, dot], { clearProps: 'opacity' });
|
|
254
|
+
observer.disconnect();
|
|
255
|
+
enter();
|
|
256
|
+
}, { threshold: 0.4 }),
|
|
257
|
+
);
|
|
258
|
+
observer.observe(root);
|
|
259
|
+
} else {
|
|
260
|
+
entered = true;
|
|
261
|
+
paint(false, null);
|
|
262
|
+
figure(null);
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
return {
|
|
266
|
+
update(next) {
|
|
267
|
+
if (scope.disposed) return;
|
|
268
|
+
const previousTrend = (root.dataset.mkTrend as Trend | undefined) ?? null;
|
|
269
|
+
options = { ...options, ...next };
|
|
270
|
+
if (!entered) {
|
|
271
|
+
paint(false, null);
|
|
272
|
+
return;
|
|
273
|
+
}
|
|
274
|
+
paint(true, previousTrend);
|
|
275
|
+
const value = options.value;
|
|
276
|
+
if (typeof value !== 'number') valueEl.textContent = '—';
|
|
277
|
+
// countTo carries on from the figure it last wrote.
|
|
278
|
+
else if (reduced()) countTo(valueEl, value, { from: value, format: formatter() });
|
|
279
|
+
else keep(countTo(valueEl, value, { duration: 0.9, format: formatter() }));
|
|
280
|
+
},
|
|
281
|
+
destroy: () => scope.dispose(),
|
|
282
|
+
};
|
|
283
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
import { useRef, type HTMLAttributes } from 'react';
|
|
3
|
+
import { useIsomorphicLayoutEffect, useLatest } from '../../react/use-recipe';
|
|
4
|
+
import { createStatCard, type StatCardInstance, type StatCardOptions } from './engine';
|
|
5
|
+
|
|
6
|
+
export interface StatCardProps extends Omit<HTMLAttributes<HTMLDivElement>, 'children'>, StatCardOptions {}
|
|
7
|
+
|
|
8
|
+
/** <StatCard label="Ingresos" value={128400} previous={118900} format={{ style: 'currency', currency: 'MXN' }} series={week} />. */
|
|
9
|
+
export function StatCard({ label, value, previous, format, locale, positive, comparison, series, target, countOnView, className, ...props }: StatCardProps) {
|
|
10
|
+
const ref = useRef<HTMLDivElement>(null);
|
|
11
|
+
const instance = useRef<StatCardInstance | null>(null);
|
|
12
|
+
const formatter = useLatest(format);
|
|
13
|
+
const custom = typeof format === 'function';
|
|
14
|
+
const data = { label, value, previous, positive, comparison, series, target, format: custom ? undefined : format };
|
|
15
|
+
const key = JSON.stringify(data);
|
|
16
|
+
const options = () => ({ ...data, format: custom ? (n: number) => (formatter.current as (n: number) => string)(n) : data.format });
|
|
17
|
+
useIsomorphicLayoutEffect(() => {
|
|
18
|
+
const created = createStatCard(ref.current, { ...options(), locale, countOnView });
|
|
19
|
+
instance.current = created;
|
|
20
|
+
return () => {
|
|
21
|
+
created.destroy();
|
|
22
|
+
instance.current = null;
|
|
23
|
+
};
|
|
24
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
25
|
+
}, [locale, countOnView]);
|
|
26
|
+
const first = useRef(true);
|
|
27
|
+
useIsomorphicLayoutEffect(() => {
|
|
28
|
+
if (first.current) {
|
|
29
|
+
first.current = false;
|
|
30
|
+
return;
|
|
31
|
+
}
|
|
32
|
+
instance.current?.update(options());
|
|
33
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
34
|
+
}, [key, custom]);
|
|
35
|
+
return <div {...props} ref={ref} className={['mk-stat-card', className].filter(Boolean).join(' ')} />;
|
|
36
|
+
}
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
/* stat-card — a 20 px card on surface-1: a muted label, a 2rem tabular figure, a pill badge with the change (green
|
|
2
|
+
when it moves the good way, red when not, neutral when flat), the comparison line, a 2 px sparkline over a soft
|
|
3
|
+
area with an end dot, and an optional goal bar. --mk-stat-good / --mk-stat-bad recolour the badge. */
|
|
4
|
+
.mk-stat-card,
|
|
5
|
+
mk-stat-card {
|
|
6
|
+
--mk-stat-good: #1f9d5b;
|
|
7
|
+
--mk-stat-bad: var(--mk-error, #c62828);
|
|
8
|
+
--mk-stat-line: var(--mk-accent);
|
|
9
|
+
display: grid;
|
|
10
|
+
gap: 0.5rem;
|
|
11
|
+
box-sizing: border-box;
|
|
12
|
+
min-width: 0;
|
|
13
|
+
padding: 1.1rem 1.2rem 1.2rem;
|
|
14
|
+
border: 1px solid var(--mk-outline-soft);
|
|
15
|
+
border-radius: 20px;
|
|
16
|
+
background: var(--mk-surface-1);
|
|
17
|
+
color: var(--mk-text);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
[data-mk-stat-label] {
|
|
21
|
+
overflow: hidden;
|
|
22
|
+
color: var(--mk-muted);
|
|
23
|
+
font-size: 0.875rem;
|
|
24
|
+
font-weight: 500;
|
|
25
|
+
text-overflow: ellipsis;
|
|
26
|
+
white-space: nowrap;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
[data-mk-stat-row] {
|
|
30
|
+
display: flex;
|
|
31
|
+
flex-wrap: wrap;
|
|
32
|
+
align-items: center;
|
|
33
|
+
gap: 0.35rem 0.6rem;
|
|
34
|
+
min-width: 0;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
[data-mk-stat-value] {
|
|
38
|
+
min-width: 0;
|
|
39
|
+
overflow-wrap: anywhere;
|
|
40
|
+
font-size: clamp(1.5rem, 4vw, 2rem);
|
|
41
|
+
font-variant-numeric: tabular-nums;
|
|
42
|
+
font-weight: 600;
|
|
43
|
+
letter-spacing: -0.02em;
|
|
44
|
+
line-height: 1.1;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
[data-mk-stat-delta] {
|
|
48
|
+
display: inline-flex;
|
|
49
|
+
align-items: center;
|
|
50
|
+
gap: 0.2rem;
|
|
51
|
+
height: 1.5rem;
|
|
52
|
+
padding: 0 0.5rem 0 0.4rem;
|
|
53
|
+
border-radius: 999px;
|
|
54
|
+
background: color-mix(in srgb, var(--mk-muted) 14%, transparent);
|
|
55
|
+
color: var(--mk-muted);
|
|
56
|
+
font-size: 0.8125rem;
|
|
57
|
+
font-variant-numeric: tabular-nums;
|
|
58
|
+
font-weight: 600;
|
|
59
|
+
white-space: nowrap;
|
|
60
|
+
transition: background-color 0.3s ease, color 0.3s ease;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
:is(.mk-stat-card, mk-stat-card)[data-mk-good='good'] [data-mk-stat-delta] {
|
|
64
|
+
background: color-mix(in srgb, var(--mk-stat-good) 16%, transparent);
|
|
65
|
+
color: var(--mk-stat-good);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
:is(.mk-stat-card, mk-stat-card)[data-mk-good='bad'] [data-mk-stat-delta] {
|
|
69
|
+
background: color-mix(in srgb, var(--mk-stat-bad) 14%, transparent);
|
|
70
|
+
color: var(--mk-stat-bad);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
[data-mk-stat-arrow] {
|
|
74
|
+
display: inline-grid;
|
|
75
|
+
transition: rotate 0.35s cubic-bezier(0.38, 0.49, 0, 1.5);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
:is(.mk-stat-card, mk-stat-card)[data-mk-trend='down'] [data-mk-stat-arrow] {
|
|
79
|
+
rotate: 180deg;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
:is(.mk-stat-card, mk-stat-card)[data-mk-trend='flat'] [data-mk-stat-arrow] {
|
|
83
|
+
rotate: 90deg;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
[data-mk-stat-comparison] {
|
|
87
|
+
margin: 0;
|
|
88
|
+
color: var(--mk-muted);
|
|
89
|
+
font-size: 0.8125rem;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
[data-mk-stat-spark] {
|
|
93
|
+
position: relative;
|
|
94
|
+
height: 2.5rem;
|
|
95
|
+
margin-top: 0.25rem;
|
|
96
|
+
color: var(--mk-stat-line);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
[data-mk-stat-spark] svg {
|
|
100
|
+
display: block;
|
|
101
|
+
width: 100%;
|
|
102
|
+
height: 100%;
|
|
103
|
+
overflow: visible;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
[data-mk-stat-line] {
|
|
107
|
+
fill: none;
|
|
108
|
+
stroke: currentColor;
|
|
109
|
+
stroke-width: 2px;
|
|
110
|
+
stroke-linecap: round;
|
|
111
|
+
stroke-linejoin: round;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
[data-mk-stat-area] {
|
|
115
|
+
fill: color-mix(in srgb, currentColor 14%, transparent);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
[data-mk-stat-dot] {
|
|
119
|
+
position: absolute;
|
|
120
|
+
width: 8px;
|
|
121
|
+
height: 8px;
|
|
122
|
+
margin: -4px 0 0 -4px;
|
|
123
|
+
border-radius: 50%;
|
|
124
|
+
background: currentColor;
|
|
125
|
+
box-shadow: 0 0 0 2.5px var(--mk-surface-1);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
[data-mk-stat-target] {
|
|
129
|
+
display: grid;
|
|
130
|
+
gap: 0.35rem;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
[data-mk-stat-target-bar] {
|
|
134
|
+
display: block;
|
|
135
|
+
height: 6px;
|
|
136
|
+
overflow: hidden;
|
|
137
|
+
border-radius: 999px;
|
|
138
|
+
background: var(--mk-surface-3);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
[data-mk-stat-target-fill] {
|
|
142
|
+
display: block;
|
|
143
|
+
height: 100%;
|
|
144
|
+
border-radius: inherit;
|
|
145
|
+
background: var(--mk-stat-line);
|
|
146
|
+
transform-origin: 0 50%;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
[data-mk-stat-target-text] {
|
|
150
|
+
color: var(--mk-muted);
|
|
151
|
+
font-size: 0.75rem;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
:is(.mk-stat-card, mk-stat-card) [hidden] {
|
|
155
|
+
display: none;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
@media (prefers-reduced-motion: reduce) {
|
|
159
|
+
[data-mk-stat-delta],
|
|
160
|
+
[data-mk-stat-arrow] {
|
|
161
|
+
transition: none;
|
|
162
|
+
}
|
|
163
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
# swipe-actions
|
|
2
|
+
|
|
3
|
+
«Deslizar para ver opciones» en filas de lista: un arrastre horizontal sobre la fila (`[data-mk-swipe-row]`) aparta su cara (`[data-mk-swipe-content]`) y muestra detrás las acciones de ese lado (`[data-mk-swipe-actions="left|right"]` con botones `[data-mk-swipe-action]`). Los iconos crecen mientras aparecen; al soltar, la fila se abre o se cierra con la curva swing. Un deslizamiento largo (60 %) ejecuta la acción principal del lado: la cara sale y, si la acción lleva `data-mk-swipe-remove`, la fila se pliega (blur y alto a 0) antes de emitir `mk:action`, para que la app la quite de sus datos.
|
|
4
|
+
|
|
5
|
+
```html
|
|
6
|
+
<mk-swipe-actions class="mk-swipe-actions">
|
|
7
|
+
<div data-mk-swipe-row>
|
|
8
|
+
<div data-mk-swipe-actions="left"><button data-mk-swipe-action="archive" aria-label="Archivar">A</button></div>
|
|
9
|
+
<div data-mk-swipe-content>Factura de octubre</div>
|
|
10
|
+
<div data-mk-swipe-actions="right"><button data-mk-swipe-action="delete" data-mk-swipe-remove data-mk-tone="danger" aria-label="Eliminar">×</button></div>
|
|
11
|
+
</div>
|
|
12
|
+
</mk-swipe-actions>
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
`defineElements()`, `createSwipeActions(root, options)` o `<SwipeActions onAction={run}><SwipeActions.Row right={[{ id: 'delete', label: 'Eliminar', remove: true, tone: 'danger' }]}>…</SwipeActions.Row></SwipeActions>`.
|
|
16
|
+
|
|
17
|
+
Opciones: `fullSwipe=0.6` (0 lo desactiva), `onAction(action, row)`. API: `open(row, side)`, `close()`, `openRow`, `destroy()`. Eventos `mk:action` `{action, row}` y `mk:open` `{row, side}`. La acción principal es la marcada con `data-mk-swipe-primary` o, si no hay, la más exterior del lado. Una sola fila abierta; Escape o un toque fuera la cierran. Las acciones son botones reales: enfocarlas con teclado abre su fila. El desplazamiento vertical sigue siendo nativo (`touch-action: pan-y`).
|
|
18
|
+
|
|
19
|
+
Tonos: `data-mk-tone="danger|accent"` (por defecto, el color del texto). Movimiento reducido: la fila se abre, se cierra y se pliega sin animación.
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { MkElement, defineElement, type OptionType } from '../../elements/base';
|
|
2
|
+
import { createSwipeActions, type SwipeActionsInstance } from './engine';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* <mk-swipe-actions class="mk-swipe-actions">
|
|
6
|
+
* <div data-mk-swipe-row>
|
|
7
|
+
* <div data-mk-swipe-actions="left"><button data-mk-swipe-action="archive" aria-label="Archivar">…</button></div>
|
|
8
|
+
* <div data-mk-swipe-content>…the row…</div>
|
|
9
|
+
* <div data-mk-swipe-actions="right"><button data-mk-swipe-action="delete" data-mk-swipe-remove aria-label="Eliminar">…</button></div>
|
|
10
|
+
* </div>
|
|
11
|
+
* </mk-swipe-actions>
|
|
12
|
+
* Emits mk:action ({action, row}) and mk:open ({row, side}).
|
|
13
|
+
*/
|
|
14
|
+
export class MkSwipeActionsElement extends MkElement<SwipeActionsInstance> {
|
|
15
|
+
static options: Record<string, OptionType> = { fullSwipe: 'number' };
|
|
16
|
+
|
|
17
|
+
protected create(options: Record<string, unknown>) {
|
|
18
|
+
return createSwipeActions(this, options);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
close() {
|
|
22
|
+
this.instance?.close();
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export const defineSwipeActions = () => defineElement('mk-swipe-actions', MkSwipeActionsElement);
|