@yewdiel21/motion-kit 0.10.0 → 0.15.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +81 -0
- package/bin/motion-kit.mjs +3 -3
- package/dist/auto/index.d.ts +56 -3
- package/dist/auto/index.js +107 -3
- package/dist/auto/index.js.map +1 -1
- package/dist/{chunk-XAKC2QY7.js → chunk-6IRJYXJU.js} +3 -28
- package/dist/chunk-6IRJYXJU.js.map +1 -0
- package/dist/chunk-A6QQCF6V.js +21333 -0
- package/dist/chunk-A6QQCF6V.js.map +1 -0
- package/dist/chunk-JXRS3OEA.js +3638 -0
- package/dist/chunk-JXRS3OEA.js.map +1 -0
- package/dist/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/add-to-cart.css +72 -0
- package/dist/css/components/attachment.css +266 -0
- package/dist/css/components/bidirectional-slider.css +168 -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 +686 -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 +227 -0
- package/dist/css/components/heatmap-chart.css +391 -0
- package/dist/css/components/hold-confirm.css +87 -0
- package/dist/css/components/hover-card.css +19 -7
- package/dist/css/components/image-preview.css +283 -0
- package/dist/css/components/json-viewer.css +304 -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/nav.css +102 -102
- package/dist/css/components/notification-bell.css +127 -0
- package/dist/css/components/orbit-menu.css +130 -0
- package/dist/css/components/pagination.css +10 -5
- package/dist/css/components/password-strength.css +183 -0
- package/dist/css/components/pie-chart.css +390 -0
- package/dist/css/components/pill-button.css +135 -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/radar-chart.css +370 -0
- package/dist/css/components/radial-chart.css +398 -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/sankey-chart.css +358 -0
- package/dist/css/components/scratch-card.css +75 -0
- package/dist/css/components/scroll-island.css +111 -0
- package/dist/css/components/select.css +136 -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/toolbar.css +122 -0
- package/dist/css/components/tooltip.css +79 -0
- package/dist/css/components/tree-view.css +289 -0
- package/dist/css/components/virtual-list.css +195 -0
- package/dist/css/components/voice-dots.css +70 -0
- package/dist/css/tokens.css +185 -181
- package/dist/elements/index.d.ts +849 -7
- package/dist/elements/index.js +1642 -8
- package/dist/elements/index.js.map +1 -1
- package/dist/{engine-GDGwQnJE.d.ts → engine-5zRIssTO.d.ts} +11 -1
- package/dist/engine-QECQnGz1.d.ts +33 -0
- package/dist/index.css +10890 -311
- package/dist/index.d.ts +89 -5
- package/dist/index.js +273 -9
- package/dist/index.js.map +1 -1
- package/dist/react/index.d.ts +686 -24
- package/dist/react/index.js +2345 -9
- package/dist/react/index.js.map +1 -1
- package/dist/{reduced-BmBzKH94.d.ts → reduced-6X_bns60.d.ts} +9 -2
- package/dist/reveal-BfyKKsw4.d.ts +2474 -0
- package/dist/store-CA2Qcun_.d.ts +453 -0
- package/package.json +9 -16
- package/registry.json +1096 -543
- package/src/auto/auto-init.ts +104 -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/add-to-cart/README.md +34 -0
- package/src/components/add-to-cart/add-to-cart.css +71 -0
- package/src/components/add-to-cart/element.ts +20 -0
- package/src/components/add-to-cart/engine.ts +206 -0
- package/src/components/add-to-cart/react.tsx +29 -0
- package/src/components/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/bidirectional-slider/README.md +46 -0
- package/src/components/bidirectional-slider/bidirectional-slider.css +167 -0
- package/src/components/bidirectional-slider/element.ts +42 -0
- package/src/components/bidirectional-slider/engine.ts +289 -0
- package/src/components/bidirectional-slider/react.tsx +45 -0
- package/src/components/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 +34 -0
- package/src/components/chart/chart.css +295 -0
- package/src/components/chart/element.ts +43 -0
- package/src/components/chart/engine.ts +838 -0
- package/src/components/chart/parts.ts +237 -0
- package/src/components/chart/react.tsx +50 -0
- package/src/components/chart/use-chart.ts +55 -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 +685 -0
- package/src/components/data-table/element.ts +54 -0
- package/src/components/data-table/engine.ts +1111 -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 +831 -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 +396 -0
- package/src/components/filter-builder/filter-builder.css +226 -0
- package/src/components/filter-builder/react.tsx +36 -0
- package/src/components/heatmap-chart/README.md +48 -0
- package/src/components/heatmap-chart/element.ts +21 -0
- package/src/components/heatmap-chart/engine.ts +281 -0
- package/src/components/heatmap-chart/heatmap-chart.css +93 -0
- package/src/components/heatmap-chart/react.tsx +14 -0
- package/src/components/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 +98 -7
- package/src/components/hover-card/hover-card.css +19 -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 +405 -0
- package/src/components/image-preview/image-preview.css +282 -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 +449 -0
- package/src/components/json-viewer/json-viewer.css +303 -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/nav/README.md +12 -10
- package/src/components/nav/element.ts +5 -5
- package/src/components/nav/engine.ts +21 -14
- package/src/components/nav/nav.css +102 -102
- package/src/components/nav/react.tsx +4 -4
- package/src/components/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/orbit-menu/README.md +46 -0
- package/src/components/orbit-menu/element.ts +27 -0
- package/src/components/orbit-menu/engine.ts +179 -0
- package/src/components/orbit-menu/orbit-menu.css +129 -0
- package/src/components/orbit-menu/react.tsx +44 -0
- package/src/components/pagination/README.md +1 -1
- package/src/components/pagination/engine.ts +50 -4
- package/src/components/pagination/pagination.css +10 -5
- package/src/components/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/pie-chart/README.md +48 -0
- package/src/components/pie-chart/element.ts +30 -0
- package/src/components/pie-chart/engine.ts +283 -0
- package/src/components/pie-chart/pie-chart.css +92 -0
- package/src/components/pie-chart/react.tsx +14 -0
- package/src/components/pill-button/README.md +46 -0
- package/src/components/pill-button/element.ts +16 -0
- package/src/components/pill-button/engine.ts +77 -0
- package/src/components/pill-button/pill-button.css +134 -0
- package/src/components/pill-button/react.tsx +32 -0
- package/src/components/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/radar-chart/README.md +48 -0
- package/src/components/radar-chart/element.ts +30 -0
- package/src/components/radar-chart/engine.ts +327 -0
- package/src/components/radar-chart/radar-chart.css +72 -0
- package/src/components/radar-chart/react.tsx +14 -0
- package/src/components/radial-chart/README.md +48 -0
- package/src/components/radial-chart/element.ts +30 -0
- package/src/components/radial-chart/engine.ts +290 -0
- package/src/components/radial-chart/radial-chart.css +100 -0
- package/src/components/radial-chart/react.tsx +14 -0
- package/src/components/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/sankey-chart/README.md +48 -0
- package/src/components/sankey-chart/element.ts +31 -0
- package/src/components/sankey-chart/engine.ts +401 -0
- package/src/components/sankey-chart/react.tsx +14 -0
- package/src/components/sankey-chart/sankey-chart.css +60 -0
- package/src/components/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/scroll-island/README.md +34 -0
- package/src/components/scroll-island/element.ts +16 -0
- package/src/components/scroll-island/engine.ts +252 -0
- package/src/components/scroll-island/react.tsx +26 -0
- package/src/components/scroll-island/scroll-island.css +110 -0
- package/src/components/select/README.md +34 -0
- package/src/components/select/element.ts +26 -0
- package/src/components/select/engine.ts +267 -0
- package/src/components/select/react.tsx +33 -0
- package/src/components/select/select.css +135 -0
- package/src/components/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/toolbar/README.md +46 -0
- package/src/components/toolbar/element.ts +30 -0
- package/src/components/toolbar/engine.ts +230 -0
- package/src/components/toolbar/react.tsx +34 -0
- package/src/components/toolbar/toolbar.css +121 -0
- package/src/components/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 +743 -0
- package/src/components/tree-view/react.tsx +82 -0
- package/src/components/tree-view/tree-view.css +288 -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/components/voice-dots/README.md +48 -0
- package/src/components/voice-dots/element.ts +45 -0
- package/src/components/voice-dots/engine.ts +243 -0
- package/src/components/voice-dots/react.tsx +52 -0
- package/src/components/voice-dots/voice-dots.css +69 -0
- package/src/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 +186 -0
- package/src/index.ts +88 -0
- package/src/motion/liquid.ts +14 -4
- 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 +71 -1
- package/dist/chunk-IKQ5FODN.js +0 -1617
- 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
- package/dist/store-DYb3tTsZ.d.ts +0 -195
|
@@ -0,0 +1,373 @@
|
|
|
1
|
+
import { gsap, EASE, registerMotion } from './gsap';
|
|
2
|
+
import { reduced } from './reduced';
|
|
3
|
+
import { createScope } from './scope';
|
|
4
|
+
import { rollDigits, shake } from '../motion/micro';
|
|
5
|
+
import {
|
|
6
|
+
BIG_STEP, dayPeriods, defaultHour12, formatSegment, segmentLayout, segmentLimits, stepSegment, typeDigit,
|
|
7
|
+
type SegmentType, type SegmentValues,
|
|
8
|
+
} from './segments';
|
|
9
|
+
|
|
10
|
+
export type { SegmentValues } from './segments';
|
|
11
|
+
|
|
12
|
+
/*
|
|
13
|
+
* Segmented field controller (time-field, date-range-picker): one
|
|
14
|
+
* role="spinbutton" per segment in the locale's order, inside a group it
|
|
15
|
+
* appends to `root`. Segments are contenteditable with inputmode="numeric" so
|
|
16
|
+
* phones open the number pad, but every edit is intercepted (keydown on
|
|
17
|
+
* desktop, beforeinput on phones) and the controller writes the text. Arrows
|
|
18
|
+
* step with wrap and the digits roll in the direction of the change; typing
|
|
19
|
+
* fills a segment and moves to the next; Backspace erases, then moves back; a
|
|
20
|
+
* pill glides behind the focused segment (swing curve).
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
export interface SegmentFieldOptions {
|
|
24
|
+
kind: 'date' | 'time' | 'datetime';
|
|
25
|
+
locale: string;
|
|
26
|
+
/** Time and datetime; default: the locale's. */
|
|
27
|
+
hour12?: boolean;
|
|
28
|
+
seconds?: boolean;
|
|
29
|
+
/** Arrow step per segment (default 1). */
|
|
30
|
+
steps?: Partial<Record<SegmentType, number>>;
|
|
31
|
+
/** Accessible names of the segments. */
|
|
32
|
+
labels?: Partial<Record<SegmentType, string>>;
|
|
33
|
+
disabled?: boolean;
|
|
34
|
+
readOnly?: boolean;
|
|
35
|
+
/** Any value changed (by the person or by setValues with notify). */
|
|
36
|
+
onChange?: (values: SegmentValues) => void;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export interface SegmentField {
|
|
40
|
+
/** The group: [data-mk-segments] role="group". */
|
|
41
|
+
readonly element: HTMLElement;
|
|
42
|
+
values(): SegmentValues;
|
|
43
|
+
/** Keys the field does not have are ignored; null empties a segment. No onChange. */
|
|
44
|
+
setValues(values: SegmentValues, animate?: boolean): void;
|
|
45
|
+
/** aria-invalid on the group; `shakeIt` shakes it once. */
|
|
46
|
+
invalid(on: boolean, shakeIt?: boolean): void;
|
|
47
|
+
focus(which?: 'first' | 'last'): void;
|
|
48
|
+
destroy(): void;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const LABELS: Record<SegmentType, string> = {
|
|
52
|
+
day: 'día',
|
|
53
|
+
month: 'mes',
|
|
54
|
+
year: 'año',
|
|
55
|
+
hour: 'hora',
|
|
56
|
+
minute: 'minutos',
|
|
57
|
+
second: 'segundos',
|
|
58
|
+
dayPeriod: 'a. m./p. m.',
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
const placeholder = (type: SegmentType, language: string) => {
|
|
62
|
+
if (type === 'year') return language === 'es' ? 'aaaa' : 'yyyy';
|
|
63
|
+
if (type === 'day') return 'dd';
|
|
64
|
+
if (type === 'month') return 'mm';
|
|
65
|
+
return '––';
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
interface Segment {
|
|
69
|
+
type: SegmentType;
|
|
70
|
+
element: HTMLElement;
|
|
71
|
+
limits: [number, number];
|
|
72
|
+
step: number;
|
|
73
|
+
value: number | null;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export function createSegmentField(root: HTMLElement, options: SegmentFieldOptions): SegmentField {
|
|
77
|
+
registerMotion();
|
|
78
|
+
const scope = createScope();
|
|
79
|
+
const language = options.locale.split('-')[0]!.toLowerCase();
|
|
80
|
+
const hour12 = options.kind === 'date' ? false : (options.hour12 ?? defaultHour12(options.locale));
|
|
81
|
+
const periods = dayPeriods(options.locale);
|
|
82
|
+
const layout = segmentLayout(options.kind, options.locale, { hour12, seconds: options.seconds });
|
|
83
|
+
const editable = !options.disabled && !options.readOnly;
|
|
84
|
+
|
|
85
|
+
const group = document.createElement('div');
|
|
86
|
+
group.dataset.mkSegments = '';
|
|
87
|
+
group.setAttribute('role', 'group');
|
|
88
|
+
if (options.disabled) group.setAttribute('aria-disabled', 'true');
|
|
89
|
+
const pill = document.createElement('span');
|
|
90
|
+
pill.dataset.mkSegmentFocus = '';
|
|
91
|
+
pill.setAttribute('aria-hidden', 'true');
|
|
92
|
+
group.append(pill);
|
|
93
|
+
|
|
94
|
+
const segments: Segment[] = [];
|
|
95
|
+
for (const part of layout) {
|
|
96
|
+
if ('literal' in part) {
|
|
97
|
+
const literal = document.createElement('span');
|
|
98
|
+
literal.dataset.mkSegmentLiteral = '';
|
|
99
|
+
literal.setAttribute('aria-hidden', 'true');
|
|
100
|
+
literal.textContent = part.literal;
|
|
101
|
+
group.append(literal);
|
|
102
|
+
continue;
|
|
103
|
+
}
|
|
104
|
+
const { type } = part;
|
|
105
|
+
const element = document.createElement('span');
|
|
106
|
+
element.dataset.mkSegment = type;
|
|
107
|
+
element.setAttribute('role', 'spinbutton');
|
|
108
|
+
element.setAttribute('aria-label', options.labels?.[type] ?? LABELS[type]);
|
|
109
|
+
element.setAttribute('tabindex', options.disabled ? '-1' : '0');
|
|
110
|
+
element.setAttribute('spellcheck', 'false');
|
|
111
|
+
element.setAttribute('autocorrect', 'off');
|
|
112
|
+
element.setAttribute('autocapitalize', 'off');
|
|
113
|
+
element.setAttribute('enterkeyhint', 'next');
|
|
114
|
+
element.setAttribute('inputmode', type === 'dayPeriod' ? 'text' : 'numeric');
|
|
115
|
+
if (editable) element.setAttribute('contenteditable', 'true');
|
|
116
|
+
if (options.disabled) element.setAttribute('aria-disabled', 'true');
|
|
117
|
+
if (options.readOnly) element.setAttribute('aria-readonly', 'true');
|
|
118
|
+
const limits = segmentLimits(type, hour12);
|
|
119
|
+
element.setAttribute('aria-valuemin', String(limits[0]));
|
|
120
|
+
element.setAttribute('aria-valuemax', String(limits[1]));
|
|
121
|
+
group.append(element);
|
|
122
|
+
const step = Math.max(1, Math.round(options.steps?.[type] ?? 1));
|
|
123
|
+
segments.push({ type, element, limits, step, value: null });
|
|
124
|
+
}
|
|
125
|
+
root.append(group);
|
|
126
|
+
scope.add(() => group.remove());
|
|
127
|
+
|
|
128
|
+
const effects = new Set<gsap.core.Animation>();
|
|
129
|
+
const keep = (animation: gsap.core.Animation | null) => {
|
|
130
|
+
if (!animation) return;
|
|
131
|
+
effects.add(animation);
|
|
132
|
+
// Finished ones leave as new ones arrive: the animation's own onComplete stays untouched.
|
|
133
|
+
for (const effect of effects) if (effect.totalProgress() === 1) effects.delete(effect);
|
|
134
|
+
};
|
|
135
|
+
scope.add(() => {
|
|
136
|
+
for (const effect of effects) effect.kill();
|
|
137
|
+
effects.clear();
|
|
138
|
+
gsap.killTweensOf([pill, group]);
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
/** Typing in progress: the segment and the digits typed so far. */
|
|
142
|
+
let typing: { segment: Segment; buffer: string } | null = null;
|
|
143
|
+
const partialYear = (segment: Segment) => segment.type === 'year' && typing?.segment === segment && typing.buffer !== '';
|
|
144
|
+
|
|
145
|
+
const text = (segment: Segment) => {
|
|
146
|
+
if (partialYear(segment)) return typing!.buffer;
|
|
147
|
+
return segment.value === null ? placeholder(segment.type, language) : formatSegment(segment.type, segment.value, periods);
|
|
148
|
+
};
|
|
149
|
+
const paint = (segment: Segment, direction: 1 | -1 | 0 = 0) => {
|
|
150
|
+
const shown = text(segment);
|
|
151
|
+
const { element } = segment;
|
|
152
|
+
if (direction && !reduced() && segment.value !== null && element.textContent) keep(rollDigits(element, shown, direction, 0.28));
|
|
153
|
+
else element.textContent = shown;
|
|
154
|
+
element.toggleAttribute('data-mk-empty', segment.value === null && !partialYear(segment));
|
|
155
|
+
if (segment.value === null) {
|
|
156
|
+
element.removeAttribute('aria-valuenow');
|
|
157
|
+
element.setAttribute('aria-valuetext', 'Vacío');
|
|
158
|
+
} else {
|
|
159
|
+
element.setAttribute('aria-valuenow', String(segment.value));
|
|
160
|
+
element.setAttribute('aria-valuetext', shown);
|
|
161
|
+
}
|
|
162
|
+
};
|
|
163
|
+
segments.forEach((segment) => paint(segment));
|
|
164
|
+
|
|
165
|
+
const values = (): SegmentValues => {
|
|
166
|
+
const out: SegmentValues = {};
|
|
167
|
+
for (const segment of segments) out[segment.type] = partialYear(segment) ? null : segment.value;
|
|
168
|
+
return out;
|
|
169
|
+
};
|
|
170
|
+
let last = JSON.stringify(values());
|
|
171
|
+
const notify = () => {
|
|
172
|
+
const now = JSON.stringify(values());
|
|
173
|
+
if (now === last) return;
|
|
174
|
+
last = now;
|
|
175
|
+
options.onChange?.(values());
|
|
176
|
+
};
|
|
177
|
+
|
|
178
|
+
const set = (segment: Segment, value: number | null, direction: 1 | -1 | 0 = 0) => {
|
|
179
|
+
segment.value = value;
|
|
180
|
+
paint(segment, direction);
|
|
181
|
+
};
|
|
182
|
+
|
|
183
|
+
const indexOf = (element: Element | null) => segments.findIndex((segment) => segment.element === element);
|
|
184
|
+
const focusAt = (index: number) => segments[index]?.element.focus();
|
|
185
|
+
|
|
186
|
+
/** Leaving a segment ends its typing; a partial year is taken as typed. */
|
|
187
|
+
const endTyping = () => {
|
|
188
|
+
if (!typing) return;
|
|
189
|
+
const { segment, buffer } = typing;
|
|
190
|
+
typing = null;
|
|
191
|
+
if (segment.type === 'year' && buffer) {
|
|
192
|
+
const year = Number(buffer);
|
|
193
|
+
set(segment, year >= segment.limits[0] ? year : segment.value);
|
|
194
|
+
} else paint(segment);
|
|
195
|
+
notify();
|
|
196
|
+
};
|
|
197
|
+
|
|
198
|
+
// The focus pill glides behind the focused segment.
|
|
199
|
+
let pillShown = false;
|
|
200
|
+
const movePill = (segment: Segment) => {
|
|
201
|
+
const box = { x: segment.element.offsetLeft, width: segment.element.offsetWidth };
|
|
202
|
+
if (!pillShown || reduced()) {
|
|
203
|
+
gsap.set(pill, box);
|
|
204
|
+
if (!reduced()) keep(gsap.to(pill, { opacity: 1, duration: 0.15, ease: 'power2.out' }));
|
|
205
|
+
} else keep(gsap.to(pill, { ...box, opacity: 1, duration: 0.35, ease: EASE.swatch, overwrite: 'auto' }));
|
|
206
|
+
pillShown = true;
|
|
207
|
+
};
|
|
208
|
+
|
|
209
|
+
const typeInto = (segment: Segment, digit: string) => {
|
|
210
|
+
const index = segments.indexOf(segment);
|
|
211
|
+
const buffer = typing?.segment === segment ? typing.buffer : '';
|
|
212
|
+
const result = typeDigit(buffer, digit, segment.limits);
|
|
213
|
+
typing = result.buffer ? { segment, buffer: result.buffer } : null;
|
|
214
|
+
if (result.value !== null) set(segment, result.value);
|
|
215
|
+
else if (segment.type === 'year') paint(segment);
|
|
216
|
+
else if (typing) segment.element.textContent = result.buffer.padStart(2, '0');
|
|
217
|
+
notify();
|
|
218
|
+
if (result.done && index < segments.length - 1) focusAt(index + 1);
|
|
219
|
+
};
|
|
220
|
+
|
|
221
|
+
const erase = (segment: Segment) => {
|
|
222
|
+
const index = segments.indexOf(segment);
|
|
223
|
+
if (segment.value === null && !partialYear(segment)) {
|
|
224
|
+
if (index > 0) focusAt(index - 1);
|
|
225
|
+
return;
|
|
226
|
+
}
|
|
227
|
+
typing = null;
|
|
228
|
+
set(segment, null);
|
|
229
|
+
notify();
|
|
230
|
+
};
|
|
231
|
+
|
|
232
|
+
const stepBy = (segment: Segment, delta: number, step: number) => {
|
|
233
|
+
typing = null;
|
|
234
|
+
const before = segment.value;
|
|
235
|
+
const next = stepSegment(before, delta, segment.limits, step);
|
|
236
|
+
if (next === before) return;
|
|
237
|
+
const wrapped = before !== null && (delta > 0 ? next < before : next > before);
|
|
238
|
+
const direction = (delta > 0) !== wrapped ? 1 : -1;
|
|
239
|
+
set(segment, next, before === null ? 0 : direction);
|
|
240
|
+
notify();
|
|
241
|
+
};
|
|
242
|
+
|
|
243
|
+
scope.on(group, 'keydown', (event) => {
|
|
244
|
+
const key = event as KeyboardEvent;
|
|
245
|
+
const index = indexOf(key.target as Element);
|
|
246
|
+
const segment = segments[index];
|
|
247
|
+
if (!segment) return;
|
|
248
|
+
if (key.metaKey || key.ctrlKey || key.altKey) return;
|
|
249
|
+
if (key.key === 'Tab') return;
|
|
250
|
+
if (key.key === 'ArrowLeft' || key.key === 'ArrowRight') {
|
|
251
|
+
key.preventDefault();
|
|
252
|
+
focusAt(index + (key.key === 'ArrowLeft' ? -1 : 1));
|
|
253
|
+
return;
|
|
254
|
+
}
|
|
255
|
+
if (key.key === 'Enter') return;
|
|
256
|
+
// Everything else is ours: contenteditable must not write by itself.
|
|
257
|
+
if (key.key.length === 1 || ['Backspace', 'Delete', 'ArrowUp', 'ArrowDown', 'PageUp', 'PageDown', 'Home', 'End'].includes(key.key)) key.preventDefault();
|
|
258
|
+
if (!editable) return;
|
|
259
|
+
const { type, step } = segment;
|
|
260
|
+
switch (key.key) {
|
|
261
|
+
case 'ArrowUp':
|
|
262
|
+
return stepBy(segment, step, step);
|
|
263
|
+
case 'ArrowDown':
|
|
264
|
+
return stepBy(segment, -step, step);
|
|
265
|
+
case 'PageUp':
|
|
266
|
+
return stepBy(segment, Math.max(step, BIG_STEP[type]), step);
|
|
267
|
+
case 'PageDown':
|
|
268
|
+
return stepBy(segment, -Math.max(step, BIG_STEP[type]), step);
|
|
269
|
+
case 'Home':
|
|
270
|
+
case 'End': {
|
|
271
|
+
typing = null;
|
|
272
|
+
const target = key.key === 'Home' ? segment.limits[0] : segment.limits[1];
|
|
273
|
+
if (target !== segment.value) {
|
|
274
|
+
set(segment, target, segment.value === null ? 0 : target > segment.value ? 1 : -1);
|
|
275
|
+
notify();
|
|
276
|
+
}
|
|
277
|
+
return;
|
|
278
|
+
}
|
|
279
|
+
case 'Backspace':
|
|
280
|
+
case 'Delete':
|
|
281
|
+
return erase(segment);
|
|
282
|
+
}
|
|
283
|
+
if (type === 'dayPeriod') {
|
|
284
|
+
const letter = key.key.toLowerCase();
|
|
285
|
+
const match = periods.findIndex((period) => period.toLowerCase().replace(/[^\p{L}]/gu, '').startsWith(letter));
|
|
286
|
+
if (match >= 0 && /\p{L}/u.test(letter)) {
|
|
287
|
+
if (segment.value !== match) {
|
|
288
|
+
set(segment, match, match ? 1 : -1);
|
|
289
|
+
notify();
|
|
290
|
+
}
|
|
291
|
+
if (index < segments.length - 1) focusAt(index + 1);
|
|
292
|
+
}
|
|
293
|
+
return;
|
|
294
|
+
}
|
|
295
|
+
if (/^\d$/.test(key.key)) typeInto(segment, key.key);
|
|
296
|
+
});
|
|
297
|
+
|
|
298
|
+
// Phones send beforeinput instead of key codes: read what they meant, write nothing.
|
|
299
|
+
scope.on(group, 'beforeinput', (event) => {
|
|
300
|
+
const input = event as InputEvent;
|
|
301
|
+
const segment = segments[indexOf(input.target as Element)];
|
|
302
|
+
if (!segment) return;
|
|
303
|
+
input.preventDefault();
|
|
304
|
+
if (!editable) return;
|
|
305
|
+
if (input.inputType.startsWith('delete')) return erase(segment);
|
|
306
|
+
for (const character of input.data ?? '') {
|
|
307
|
+
const current = segments[indexOf(document.activeElement)] ?? segment;
|
|
308
|
+
if (/\d/.test(character) && current.type !== 'dayPeriod') typeInto(current, character);
|
|
309
|
+
else if (current.type === 'dayPeriod') {
|
|
310
|
+
const match = periods.findIndex((period) => period.toLowerCase().startsWith(character.toLowerCase()));
|
|
311
|
+
if (match >= 0) {
|
|
312
|
+
set(current, match);
|
|
313
|
+
notify();
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
});
|
|
318
|
+
// Paste, drop and IME composition must not leave stray text either.
|
|
319
|
+
scope.on(group, 'paste', (event) => event.preventDefault());
|
|
320
|
+
scope.on(group, 'drop', (event) => event.preventDefault());
|
|
321
|
+
|
|
322
|
+
scope.on(group, 'focusin', (event) => {
|
|
323
|
+
const segment = segments[indexOf(event.target as Element)];
|
|
324
|
+
if (!segment) return;
|
|
325
|
+
if (typing && typing.segment !== segment) endTyping();
|
|
326
|
+
typing = null;
|
|
327
|
+
movePill(segment);
|
|
328
|
+
// The caret is hidden; select the text so the whole segment reads as active.
|
|
329
|
+
const selection = typeof getSelection === 'function' ? getSelection() : null;
|
|
330
|
+
try {
|
|
331
|
+
selection?.selectAllChildren(segment.element);
|
|
332
|
+
selection?.collapseToEnd();
|
|
333
|
+
} catch {
|
|
334
|
+
// jsdom without a full selection model.
|
|
335
|
+
}
|
|
336
|
+
});
|
|
337
|
+
scope.on(group, 'focusout', (event) => {
|
|
338
|
+
const next = (event as FocusEvent).relatedTarget as Node | null;
|
|
339
|
+
const leaving = segments[indexOf(event.target as Element)];
|
|
340
|
+
if (leaving && typing?.segment === leaving) endTyping();
|
|
341
|
+
if (next && group.contains(next)) return;
|
|
342
|
+
pillShown = false;
|
|
343
|
+
if (reduced()) gsap.set(pill, { opacity: 0 });
|
|
344
|
+
else keep(gsap.to(pill, { opacity: 0, duration: 0.15, ease: 'power2.in' }));
|
|
345
|
+
});
|
|
346
|
+
|
|
347
|
+
return {
|
|
348
|
+
element: group,
|
|
349
|
+
values,
|
|
350
|
+
setValues(next, animate = false) {
|
|
351
|
+
typing = null;
|
|
352
|
+
for (const segment of segments) {
|
|
353
|
+
if (!(segment.type in next)) continue;
|
|
354
|
+
const raw = next[segment.type];
|
|
355
|
+
const value = raw === null || raw === undefined ? null : Math.min(segment.limits[1], Math.max(segment.limits[0], Math.round(raw)));
|
|
356
|
+
if (value === segment.value) continue;
|
|
357
|
+
const direction = !animate || value === null || segment.value === null ? 0 : value > segment.value ? 1 : -1;
|
|
358
|
+
set(segment, value, direction);
|
|
359
|
+
}
|
|
360
|
+
last = JSON.stringify(values());
|
|
361
|
+
},
|
|
362
|
+
invalid(on, shakeIt = false) {
|
|
363
|
+
if (on) group.setAttribute('aria-invalid', 'true');
|
|
364
|
+
else group.removeAttribute('aria-invalid');
|
|
365
|
+
if (on && shakeIt) keep(shake(group));
|
|
366
|
+
},
|
|
367
|
+
focus(which = 'first') {
|
|
368
|
+
const target = which === 'last' ? segments[segments.length - 1] : segments[0];
|
|
369
|
+
target?.element.focus();
|
|
370
|
+
},
|
|
371
|
+
destroy: () => scope.dispose(),
|
|
372
|
+
};
|
|
373
|
+
}
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
import { safeFormat } from './dates';
|
|
2
|
+
|
|
3
|
+
/*
|
|
4
|
+
* Segmented date and time input, the pure part: which segments a locale shows
|
|
5
|
+
* and in what order, for a date, a time or both (es: dd/mm/aaaa, 24 h and
|
|
6
|
+
* "dd/mm/aaaa, HH:MM"; en-US: mm/dd/yyyy and 12 h with
|
|
7
|
+
* AM/PM), each segment's limits, stepping with wrap, typing digit by digit, and
|
|
8
|
+
* time values ("HH:MM[:SS]", always 24 h). core/segment-field.ts puts it on screen.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
export type SegmentType = 'day' | 'month' | 'year' | 'hour' | 'minute' | 'second' | 'dayPeriod';
|
|
12
|
+
export type SegmentValues = Partial<Record<SegmentType, number | null>>;
|
|
13
|
+
export type SegmentPart = { type: SegmentType } | { literal: string };
|
|
14
|
+
|
|
15
|
+
export interface TimeValue {
|
|
16
|
+
hour: number;
|
|
17
|
+
minute: number;
|
|
18
|
+
second: number;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/** 0 = a.m., 1 = p.m. */
|
|
22
|
+
export type DayPeriod = 0 | 1;
|
|
23
|
+
|
|
24
|
+
/** Whether the locale writes 12-hour times. */
|
|
25
|
+
export function defaultHour12(locale: string): boolean {
|
|
26
|
+
const options = safeFormat(locale, { hour: 'numeric' }).resolvedOptions() as Intl.ResolvedDateTimeFormatOptions & { hourCycle?: string };
|
|
27
|
+
if (options.hourCycle) return options.hourCycle === 'h11' || options.hourCycle === 'h12';
|
|
28
|
+
return Boolean(options.hour12);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const KNOWN = new Set<string>(['day', 'month', 'year', 'hour', 'minute', 'second', 'dayPeriod']);
|
|
32
|
+
|
|
33
|
+
/** Segments and the text between them, in the locale's order. */
|
|
34
|
+
export function segmentLayout(kind: 'date' | 'time' | 'datetime', locale: string, { hour12, seconds = false }: { hour12?: boolean; seconds?: boolean } = {}): SegmentPart[] {
|
|
35
|
+
const twelve = hour12 ?? defaultHour12(locale);
|
|
36
|
+
const date: Intl.DateTimeFormatOptions = { day: '2-digit', month: '2-digit', year: 'numeric' };
|
|
37
|
+
const time: Intl.DateTimeFormatOptions = { hour: '2-digit', minute: '2-digit', ...(seconds ? { second: '2-digit' } : {}), hourCycle: twelve ? 'h12' : 'h23' };
|
|
38
|
+
// Date and time together: the locale's own order and joint ("23/11/2026, 13:05").
|
|
39
|
+
const format = safeFormat(locale, kind === 'date' ? date : kind === 'time' ? time : { ...date, ...time });
|
|
40
|
+
const parts: SegmentPart[] = [];
|
|
41
|
+
for (const part of format.formatToParts(new Date(2026, 10, 23, 13, 5, 9))) {
|
|
42
|
+
if (KNOWN.has(part.type)) parts.push({ type: part.type as SegmentType });
|
|
43
|
+
else if (part.type === 'literal') {
|
|
44
|
+
// Some engines spell a narrow no-break space before AM/PM; a plain space reads the same.
|
|
45
|
+
const literal = part.value.replace(/[ ]/g, ' ');
|
|
46
|
+
const last = parts[parts.length - 1];
|
|
47
|
+
if (last && 'literal' in last) last.literal += literal;
|
|
48
|
+
else parts.push({ literal });
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
// Leading or trailing text (a comma, a space) belongs to no segment.
|
|
52
|
+
while (parts.length && 'literal' in parts[0]!) parts.shift();
|
|
53
|
+
while (parts.length && 'literal' in parts[parts.length - 1]!) parts.pop();
|
|
54
|
+
return parts;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/** The locale's words for a.m. and p.m. */
|
|
58
|
+
export function dayPeriods(locale: string): [string, string] {
|
|
59
|
+
const format = safeFormat(locale, { hour: 'numeric', hourCycle: 'h12' });
|
|
60
|
+
const read = (hour: number) =>
|
|
61
|
+
format.formatToParts(new Date(2026, 0, 1, hour)).find((part) => part.type === 'dayPeriod')?.value.replace(/[ ]/g, ' ') ?? (hour < 12 ? 'AM' : 'PM');
|
|
62
|
+
return [read(1), read(13)];
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export const daysIn = (year: number, month: number) => {
|
|
66
|
+
const date = new Date(2000, 0, 1);
|
|
67
|
+
date.setFullYear(year, month, 0);
|
|
68
|
+
return date.getDate();
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
/** [min, max] of a segment. */
|
|
72
|
+
export function segmentLimits(type: SegmentType, hour12 = false): [number, number] {
|
|
73
|
+
switch (type) {
|
|
74
|
+
case 'day':
|
|
75
|
+
return [1, 31];
|
|
76
|
+
case 'month':
|
|
77
|
+
return [1, 12];
|
|
78
|
+
case 'year':
|
|
79
|
+
return [1, 9999];
|
|
80
|
+
case 'hour':
|
|
81
|
+
return hour12 ? [1, 12] : [0, 23];
|
|
82
|
+
case 'minute':
|
|
83
|
+
case 'second':
|
|
84
|
+
return [0, 59];
|
|
85
|
+
default:
|
|
86
|
+
return [0, 1];
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/** A bigger jump for PageUp / PageDown. */
|
|
91
|
+
export const BIG_STEP: Record<SegmentType, number> = { day: 7, month: 3, year: 5, hour: 3, minute: 15, second: 15, dayPeriod: 1 };
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* `value` moved by `delta` with wrap. Steps above 1 land on multiples of the
|
|
95
|
+
* step (09:32 + 15 → 09:45). An empty segment starts at its minimum (or its
|
|
96
|
+
* maximum when going down).
|
|
97
|
+
*/
|
|
98
|
+
export function stepSegment(value: number | null, delta: number, [min, max]: [number, number], step = 1): number {
|
|
99
|
+
if (value === null) return delta >= 0 ? min : max - ((max - min) % Math.max(1, step));
|
|
100
|
+
const size = max - min + 1;
|
|
101
|
+
let next: number;
|
|
102
|
+
if (step > 1 && Math.abs(delta) === step && (value - min) % step !== 0) {
|
|
103
|
+
next = delta > 0 ? Math.ceil((value - min) / step) * step + min : Math.floor((value - min) / step) * step + min;
|
|
104
|
+
} else next = value + delta;
|
|
105
|
+
return ((((next - min) % size) + size) % size) + min;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
export interface Typed {
|
|
109
|
+
/** Digits kept for the next key ('' when the segment is done). */
|
|
110
|
+
buffer: string;
|
|
111
|
+
/** The value to show, or null while the digits typed so far are below the minimum (a leading 0). */
|
|
112
|
+
value: number | null;
|
|
113
|
+
/** The segment is full: focus moves on. */
|
|
114
|
+
done: boolean;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/** One digit typed into a segment that already holds `buffer`. */
|
|
118
|
+
export function typeDigit(buffer: string, digit: string, [min, max]: [number, number]): Typed {
|
|
119
|
+
const width = String(max).length;
|
|
120
|
+
let text = buffer + digit;
|
|
121
|
+
if (Number(text) > max || text.length > width) text = digit;
|
|
122
|
+
const value = Number(text);
|
|
123
|
+
const done = text.length >= width || value * 10 > max;
|
|
124
|
+
return { buffer: done ? '' : text, value: value < min ? null : value, done };
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
export function formatSegment(type: SegmentType, value: number, periods: [string, string] = ['AM', 'PM']): string {
|
|
128
|
+
if (type === 'dayPeriod') return periods[value ? 1 : 0];
|
|
129
|
+
return String(value).padStart(type === 'year' ? 4 : 2, '0');
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/** "09:30", "9:30:15" → the time; null when it is not one. */
|
|
133
|
+
export function parseTime(text: string | null | undefined): TimeValue | null {
|
|
134
|
+
const match = /^(\d{1,2}):(\d{2})(?::(\d{2}))?$/.exec((text ?? '').trim());
|
|
135
|
+
if (!match) return null;
|
|
136
|
+
const [hour, minute, second] = [Number(match[1]), Number(match[2]), Number(match[3] ?? 0)];
|
|
137
|
+
if (hour > 23 || minute > 59 || second > 59) return null;
|
|
138
|
+
return { hour, minute, second };
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
const two = (n: number) => String(n).padStart(2, '0');
|
|
142
|
+
|
|
143
|
+
/** "HH:MM", or "HH:MM:SS" with `seconds`. */
|
|
144
|
+
export const formatTime = (time: TimeValue, seconds = false) => `${two(time.hour)}:${two(time.minute)}${seconds ? `:${two(time.second)}` : ''}`;
|
|
145
|
+
|
|
146
|
+
/** Minutes since midnight (seconds as a fraction), to compare times. */
|
|
147
|
+
export const timeMinutes = (time: TimeValue) => time.hour * 60 + time.minute + time.second / 60;
|
|
148
|
+
|
|
149
|
+
/** 24 h → 12 h with its period (0 → 12 a.m., 13 → 1 p.m.). */
|
|
150
|
+
export const to12 = (hour: number): { hour: number; period: DayPeriod } => ({ hour: hour % 12 || 12, period: hour >= 12 ? 1 : 0 });
|
|
151
|
+
|
|
152
|
+
/** 12 h and its period → 24 h. */
|
|
153
|
+
export const to24 = (hour: number, period: number) => (hour % 12) + (period ? 12 : 0);
|
package/src/core/tree.ts
ADDED
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
import { foldText } from './mark';
|
|
2
|
+
|
|
3
|
+
/*
|
|
4
|
+
* Trees (tree-view, file-explorer, json-viewer), the pure part: finding a node
|
|
5
|
+
* and its path, three-state checks (a branch is checked when every child is,
|
|
6
|
+
* mixed when some are; checking a branch checks everything under it, except
|
|
7
|
+
* disabled nodes, which keep their state and do not count for their branch)
|
|
8
|
+
* and searching (the matches, the ancestors that keep them visible and open;
|
|
9
|
+
* accents and case ignored).
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
export interface TreeNode {
|
|
13
|
+
id: string;
|
|
14
|
+
label: string;
|
|
15
|
+
children?: TreeNode[];
|
|
16
|
+
/** A short glyph before the label. */
|
|
17
|
+
icon?: string;
|
|
18
|
+
disabled?: boolean;
|
|
19
|
+
/** Has children that loadChildren() will bring. */
|
|
20
|
+
hasChildren?: boolean;
|
|
21
|
+
/** Anything the app wants to keep with the node. */
|
|
22
|
+
data?: unknown;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export type CheckState = 'true' | 'false' | 'mixed';
|
|
26
|
+
|
|
27
|
+
export const isBranch = (node: TreeNode) => Boolean(node.children?.length || node.hasChildren);
|
|
28
|
+
|
|
29
|
+
/** Every node, depth first, with its depth and parent. */
|
|
30
|
+
export function walk(nodes: TreeNode[], visit: (node: TreeNode, depth: number, parent: TreeNode | null) => void, depth = 0, parent: TreeNode | null = null) {
|
|
31
|
+
for (const node of nodes) {
|
|
32
|
+
visit(node, depth, parent);
|
|
33
|
+
if (node.children) walk(node.children, visit, depth + 1, node);
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/** The node with `id` and the path from the top to it (it included). */
|
|
38
|
+
export function findNode(nodes: TreeNode[], id: string): { node: TreeNode; path: TreeNode[] } | null {
|
|
39
|
+
for (const node of nodes) {
|
|
40
|
+
if (node.id === id) return { node, path: [node] };
|
|
41
|
+
if (node.children) {
|
|
42
|
+
const found = findNode(node.children, id);
|
|
43
|
+
if (found) return { node: found.node, path: [node, ...found.path] };
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
return null;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/** Each node's state: leaves from `checked`, branches from their children (the enabled ones, when there are). */
|
|
50
|
+
export function checkStates(nodes: TreeNode[], checked: Set<string>): Map<string, CheckState> {
|
|
51
|
+
const states = new Map<string, CheckState>();
|
|
52
|
+
const visit = (node: TreeNode): CheckState => {
|
|
53
|
+
let state: CheckState;
|
|
54
|
+
if (!node.children?.length) state = checked.has(node.id) ? 'true' : 'false';
|
|
55
|
+
else {
|
|
56
|
+
const all = node.children.map(visit);
|
|
57
|
+
const enabled = all.filter((_, i) => !node.children![i]!.disabled);
|
|
58
|
+
const children = enabled.length ? enabled : all;
|
|
59
|
+
state = children.every((child) => child === 'true') ? 'true' : children.every((child) => child === 'false') ? 'false' : 'mixed';
|
|
60
|
+
}
|
|
61
|
+
states.set(node.id, state);
|
|
62
|
+
return state;
|
|
63
|
+
};
|
|
64
|
+
nodes.forEach(visit);
|
|
65
|
+
return states;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/** Check or uncheck `id` and everything under it but disabled nodes; returns the new set (branches follow their children). */
|
|
69
|
+
export function toggleCheck(nodes: TreeNode[], checked: Set<string>, id: string, value?: boolean): Set<string> {
|
|
70
|
+
const found = findNode(nodes, id);
|
|
71
|
+
if (!found) return new Set(checked);
|
|
72
|
+
const states = checkStates(nodes, checked);
|
|
73
|
+
const on = value ?? states.get(id) !== 'true';
|
|
74
|
+
const next = new Set(checked);
|
|
75
|
+
const set = (node: TreeNode) => {
|
|
76
|
+
if (on) next.add(node.id);
|
|
77
|
+
else next.delete(node.id);
|
|
78
|
+
for (const child of node.children ?? []) if (!child.disabled) set(child);
|
|
79
|
+
};
|
|
80
|
+
set(found.node);
|
|
81
|
+
// Branches mirror their children.
|
|
82
|
+
const after = checkStates(nodes, next);
|
|
83
|
+
walk(nodes, (node) => {
|
|
84
|
+
if (!node.children?.length) return;
|
|
85
|
+
if (after.get(node.id) === 'true') next.add(node.id);
|
|
86
|
+
else next.delete(node.id);
|
|
87
|
+
});
|
|
88
|
+
return next;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
const fold = foldText;
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* The nodes whose text has `query` (accents and case ignored), the ancestors
|
|
95
|
+
* that keep them visible, and the branches to open to show them.
|
|
96
|
+
*/
|
|
97
|
+
export function searchTree(nodes: TreeNode[], query: string, text: (node: TreeNode) => string = (node) => node.label) {
|
|
98
|
+
const needle = fold(query.trim());
|
|
99
|
+
const matches = new Set<string>();
|
|
100
|
+
const visible = new Set<string>();
|
|
101
|
+
const open = new Set<string>();
|
|
102
|
+
if (!needle) return { matches, visible, open, active: false };
|
|
103
|
+
const visit = (node: TreeNode, ancestors: TreeNode[]) => {
|
|
104
|
+
if (fold(text(node)).includes(needle)) {
|
|
105
|
+
matches.add(node.id);
|
|
106
|
+
visible.add(node.id);
|
|
107
|
+
for (const ancestor of ancestors) {
|
|
108
|
+
visible.add(ancestor.id);
|
|
109
|
+
open.add(ancestor.id);
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
for (const child of node.children ?? []) visit(child, [...ancestors, node]);
|
|
113
|
+
};
|
|
114
|
+
for (const node of nodes) visit(node, []);
|
|
115
|
+
return { matches, visible, open, active: true };
|
|
116
|
+
}
|