@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,257 @@
|
|
|
1
|
+
import { gsap, registerMotion } from '../../core/gsap';
|
|
2
|
+
import { reduced } from '../../core/reduced';
|
|
3
|
+
import { createScope } from '../../core/scope';
|
|
4
|
+
import { inert, type Instance } from '../../core/types';
|
|
5
|
+
import { confetti } from '../../motion/confetti';
|
|
6
|
+
import { blurIn } from '../../motion/micro';
|
|
7
|
+
|
|
8
|
+
/*
|
|
9
|
+
* Scratch card: real HTML content (a coupon) under a metallic cover drawn on a
|
|
10
|
+
* canvas with a sheen and "Rasca aquí". The pointer scratches it with a round
|
|
11
|
+
* brush; progress is measured by sampling the cover. Past the threshold the
|
|
12
|
+
* rest dissolves with a blur, the content pops in and confetti flies. While
|
|
13
|
+
* covered the content is inert; a "Revelar" button is the way in for the
|
|
14
|
+
* keyboard and screen readers, and the reveal is announced. Without a 2D
|
|
15
|
+
* canvas the cover is plain CSS and the button reveals it.
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
export interface ScratchCardOptions {
|
|
19
|
+
/** Share of the cover to scratch before it reveals itself (default .5). */
|
|
20
|
+
threshold?: number;
|
|
21
|
+
/** Brush radius in px (default 22). */
|
|
22
|
+
brush?: number;
|
|
23
|
+
coverLabel?: string;
|
|
24
|
+
revealLabel?: string;
|
|
25
|
+
revealedLabel?: string;
|
|
26
|
+
/** Confetti on reveal (default true). */
|
|
27
|
+
celebrate?: boolean;
|
|
28
|
+
onProgress?: (progress: number) => void;
|
|
29
|
+
onReveal?: () => void;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export interface ScratchCardInstance extends Instance {
|
|
33
|
+
reveal(): void;
|
|
34
|
+
reset(): void;
|
|
35
|
+
readonly revealed: boolean;
|
|
36
|
+
readonly progress: number;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export function createScratchCard(root: HTMLElement | null, options: ScratchCardOptions = {}): ScratchCardInstance {
|
|
40
|
+
if (!root) return inert({ reveal() {}, reset() {}, revealed: false, progress: 0 });
|
|
41
|
+
registerMotion();
|
|
42
|
+
const scope = createScope();
|
|
43
|
+
const threshold = Number.isFinite(options.threshold) ? Math.min(0.95, Math.max(0.05, options.threshold!)) : 0.5;
|
|
44
|
+
const brush = Number.isFinite(options.brush) && options.brush! > 2 ? options.brush! : 22;
|
|
45
|
+
const coverText = options.coverLabel ?? 'Rasca aquí';
|
|
46
|
+
|
|
47
|
+
let content = root.querySelector<HTMLElement>('[data-mk-scratch-content]');
|
|
48
|
+
const made: Element[] = [];
|
|
49
|
+
scope.add(() => made.forEach((element) => element.remove()));
|
|
50
|
+
if (!content) {
|
|
51
|
+
content = document.createElement('div');
|
|
52
|
+
content.dataset.mkScratchContent = '';
|
|
53
|
+
content.append(...root.childNodes);
|
|
54
|
+
root.append(content);
|
|
55
|
+
scope.add(() => {
|
|
56
|
+
root.append(...content!.childNodes);
|
|
57
|
+
content!.remove();
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
const cover = document.createElement('div');
|
|
61
|
+
cover.dataset.mkScratchCover = '';
|
|
62
|
+
cover.setAttribute('aria-hidden', 'true');
|
|
63
|
+
const canvas = document.createElement('canvas');
|
|
64
|
+
const context = canvas.getContext('2d', { willReadFrequently: true } as CanvasRenderingContext2DSettings);
|
|
65
|
+
if (context) cover.append(canvas);
|
|
66
|
+
else {
|
|
67
|
+
cover.dataset.mkScratchFallback = '';
|
|
68
|
+
cover.textContent = coverText;
|
|
69
|
+
}
|
|
70
|
+
const button = document.createElement('button');
|
|
71
|
+
button.type = 'button';
|
|
72
|
+
button.dataset.mkScratchReveal = '';
|
|
73
|
+
button.textContent = options.revealLabel ?? 'Revelar';
|
|
74
|
+
const live = document.createElement('span');
|
|
75
|
+
live.className = 'mk-sr-only';
|
|
76
|
+
live.setAttribute('role', 'status');
|
|
77
|
+
root.append(cover, button, live);
|
|
78
|
+
made.push(cover, button, live);
|
|
79
|
+
scope.attr(root, 'data-mk-ready', '');
|
|
80
|
+
|
|
81
|
+
let revealed = false;
|
|
82
|
+
let progress = 0;
|
|
83
|
+
let size = { width: 0, height: 0, ratio: 1 };
|
|
84
|
+
const effects = new Set<gsap.core.Animation>();
|
|
85
|
+
const keep = (animation: gsap.core.Animation | null) => {
|
|
86
|
+
if (!animation) return;
|
|
87
|
+
effects.add(animation);
|
|
88
|
+
// Finished ones leave as new ones arrive: the animation's own onComplete stays untouched.
|
|
89
|
+
for (const effect of effects) if (effect.totalProgress() === 1) effects.delete(effect);
|
|
90
|
+
};
|
|
91
|
+
let party: ReturnType<typeof confetti> | null = null;
|
|
92
|
+
scope.add(() => {
|
|
93
|
+
for (const effect of effects) effect.kill();
|
|
94
|
+
effects.clear();
|
|
95
|
+
party?.stop();
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
const cover_ = (covered: boolean) => {
|
|
99
|
+
content!.inert = covered;
|
|
100
|
+
if (covered) content!.setAttribute('aria-hidden', 'true');
|
|
101
|
+
else content!.removeAttribute('aria-hidden');
|
|
102
|
+
};
|
|
103
|
+
scope.add(() => cover_(false));
|
|
104
|
+
|
|
105
|
+
/** Paint the metallic cover with its sheen and label. */
|
|
106
|
+
const paint = () => {
|
|
107
|
+
if (!context) return;
|
|
108
|
+
const box = root.getBoundingClientRect();
|
|
109
|
+
const ratio = Math.min(2, globalThis.devicePixelRatio || 1);
|
|
110
|
+
size = { width: Math.max(1, Math.round(box.width)), height: Math.max(1, Math.round(box.height)), ratio };
|
|
111
|
+
canvas.width = size.width * ratio;
|
|
112
|
+
canvas.height = size.height * ratio;
|
|
113
|
+
context.setTransform(ratio, 0, 0, ratio, 0, 0);
|
|
114
|
+
context.globalCompositeOperation = 'source-over';
|
|
115
|
+
const base = context.createLinearGradient(0, 0, size.width, size.height);
|
|
116
|
+
base.addColorStop(0, '#c9ccd2');
|
|
117
|
+
base.addColorStop(0.45, '#eef0f3');
|
|
118
|
+
base.addColorStop(0.55, '#d7dadf');
|
|
119
|
+
base.addColorStop(1, '#a9adb5');
|
|
120
|
+
context.fillStyle = base;
|
|
121
|
+
context.fillRect(0, 0, size.width, size.height);
|
|
122
|
+
// Fine grain, like foil.
|
|
123
|
+
for (let i = 0; i < (size.width * size.height) / 60; i++) {
|
|
124
|
+
context.fillStyle = Math.random() > 0.5 ? 'rgba(255,255,255,0.35)' : 'rgba(0,0,0,0.06)';
|
|
125
|
+
context.fillRect(Math.random() * size.width, Math.random() * size.height, 1, 1);
|
|
126
|
+
}
|
|
127
|
+
context.fillStyle = 'rgba(40, 42, 48, 0.75)';
|
|
128
|
+
// The cover speaks in the page's typeface.
|
|
129
|
+
const family = getComputedStyle(root).fontFamily || 'system-ui, sans-serif';
|
|
130
|
+
context.font = `600 ${Math.max(14, Math.min(22, size.width / 14))}px ${family}`;
|
|
131
|
+
context.textAlign = 'center';
|
|
132
|
+
context.textBaseline = 'middle';
|
|
133
|
+
context.fillText(coverText, size.width / 2, size.height / 2);
|
|
134
|
+
};
|
|
135
|
+
|
|
136
|
+
const measure = () => {
|
|
137
|
+
if (!context || !size.width) return progress;
|
|
138
|
+
const data = context.getImageData(0, 0, canvas.width, canvas.height).data;
|
|
139
|
+
const step = 8 * size.ratio;
|
|
140
|
+
let clear = 0;
|
|
141
|
+
let total = 0;
|
|
142
|
+
for (let y = 0; y < canvas.height; y += step) {
|
|
143
|
+
for (let x = 0; x < canvas.width; x += step) {
|
|
144
|
+
total += 1;
|
|
145
|
+
if (data[(Math.floor(y) * canvas.width + Math.floor(x)) * 4 + 3]! < 64) clear += 1;
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
return total ? clear / total : 0;
|
|
149
|
+
};
|
|
150
|
+
|
|
151
|
+
const reveal = () => {
|
|
152
|
+
if (scope.disposed || revealed) return;
|
|
153
|
+
revealed = true;
|
|
154
|
+
progress = 1;
|
|
155
|
+
scope.attr(root, 'data-mk-revealed', '');
|
|
156
|
+
cover_(false);
|
|
157
|
+
button.hidden = true;
|
|
158
|
+
live.textContent = options.revealedLabel ?? 'Premio revelado';
|
|
159
|
+
const done = () => {
|
|
160
|
+
cover.hidden = true;
|
|
161
|
+
};
|
|
162
|
+
if (reduced()) done();
|
|
163
|
+
else {
|
|
164
|
+
keep(gsap.to(cover, { opacity: 0, filter: 'blur(12px)', scale: 1.04, duration: 0.5, ease: 'power2.out', onComplete: done }));
|
|
165
|
+
keep(blurIn(content, { from: 'pop', delay: 0.1 }));
|
|
166
|
+
if (options.celebrate !== false) party = confetti(root, { count: 60 });
|
|
167
|
+
}
|
|
168
|
+
options.onReveal?.();
|
|
169
|
+
root.dispatchEvent(new CustomEvent('mk:reveal', { bubbles: true }));
|
|
170
|
+
};
|
|
171
|
+
|
|
172
|
+
const reset = () => {
|
|
173
|
+
if (scope.disposed) return;
|
|
174
|
+
revealed = false;
|
|
175
|
+
progress = 0;
|
|
176
|
+
scope.attr(root, 'data-mk-revealed', null);
|
|
177
|
+
gsap.killTweensOf(cover);
|
|
178
|
+
gsap.set(cover, { clearProps: 'all' });
|
|
179
|
+
cover.hidden = false;
|
|
180
|
+
button.hidden = false;
|
|
181
|
+
cover_(true);
|
|
182
|
+
live.textContent = '';
|
|
183
|
+
paint();
|
|
184
|
+
};
|
|
185
|
+
|
|
186
|
+
let drawing = false;
|
|
187
|
+
let last: { x: number; y: number } | null = null;
|
|
188
|
+
let moves = 0;
|
|
189
|
+
const scratch = (event: PointerEvent) => {
|
|
190
|
+
if (!context || revealed) return;
|
|
191
|
+
const box = canvas.getBoundingClientRect();
|
|
192
|
+
const point = { x: event.clientX - box.left, y: event.clientY - box.top };
|
|
193
|
+
context.globalCompositeOperation = 'destination-out';
|
|
194
|
+
context.lineCap = 'round';
|
|
195
|
+
context.lineJoin = 'round';
|
|
196
|
+
context.lineWidth = brush * 2;
|
|
197
|
+
context.beginPath();
|
|
198
|
+
context.moveTo(last?.x ?? point.x, last?.y ?? point.y);
|
|
199
|
+
context.lineTo(point.x, point.y);
|
|
200
|
+
context.stroke();
|
|
201
|
+
last = point;
|
|
202
|
+
// Sampling every move would be wasteful: every fifth is enough.
|
|
203
|
+
if (++moves % 5) return;
|
|
204
|
+
progress = measure();
|
|
205
|
+
options.onProgress?.(progress);
|
|
206
|
+
root.dispatchEvent(new CustomEvent('mk:progress', { bubbles: true, detail: { progress } }));
|
|
207
|
+
if (progress >= threshold) reveal();
|
|
208
|
+
};
|
|
209
|
+
scope.on(canvas, 'pointerdown', (event) => {
|
|
210
|
+
const pointer = event as PointerEvent;
|
|
211
|
+
drawing = true;
|
|
212
|
+
last = null;
|
|
213
|
+
canvas.setPointerCapture?.(pointer.pointerId);
|
|
214
|
+
scratch(pointer);
|
|
215
|
+
});
|
|
216
|
+
scope.on(canvas, 'pointermove', (event) => {
|
|
217
|
+
if (drawing) scratch(event as PointerEvent);
|
|
218
|
+
});
|
|
219
|
+
const stop = () => {
|
|
220
|
+
drawing = false;
|
|
221
|
+
last = null;
|
|
222
|
+
};
|
|
223
|
+
scope.on(canvas, 'pointerup', stop);
|
|
224
|
+
scope.on(canvas, 'pointercancel', stop);
|
|
225
|
+
scope.on(button, 'click', reveal);
|
|
226
|
+
if (typeof ResizeObserver === 'function' && context) {
|
|
227
|
+
let width = 0;
|
|
228
|
+
scope.observe(
|
|
229
|
+
new ResizeObserver(() => {
|
|
230
|
+
const next = Math.round(root.getBoundingClientRect().width);
|
|
231
|
+
if (next === width || revealed) return;
|
|
232
|
+
width = next;
|
|
233
|
+
paint();
|
|
234
|
+
}),
|
|
235
|
+
).observe(root);
|
|
236
|
+
}
|
|
237
|
+
cover_(true);
|
|
238
|
+
paint();
|
|
239
|
+
// A web font that arrives late repaints the untouched cover.
|
|
240
|
+
let alive = true;
|
|
241
|
+
scope.add(() => (alive = false));
|
|
242
|
+
document.fonts?.ready.then(() => {
|
|
243
|
+
if (alive && !revealed && progress === 0) paint();
|
|
244
|
+
});
|
|
245
|
+
|
|
246
|
+
return {
|
|
247
|
+
reveal,
|
|
248
|
+
reset,
|
|
249
|
+
get revealed() {
|
|
250
|
+
return revealed;
|
|
251
|
+
},
|
|
252
|
+
get progress() {
|
|
253
|
+
return progress;
|
|
254
|
+
},
|
|
255
|
+
destroy: () => scope.dispose(),
|
|
256
|
+
};
|
|
257
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
import { useRef, type HTMLAttributes, type MutableRefObject, type ReactNode } from 'react';
|
|
3
|
+
import { useIsomorphicLayoutEffect, useLatest } from '../../react/use-recipe';
|
|
4
|
+
import { createScratchCard, type ScratchCardInstance, type ScratchCardOptions } from './engine';
|
|
5
|
+
|
|
6
|
+
export interface ScratchCardProps extends Omit<HTMLAttributes<HTMLDivElement>, 'children' | 'onProgress'>, ScratchCardOptions {
|
|
7
|
+
/** What the cover hides. */
|
|
8
|
+
children?: ReactNode;
|
|
9
|
+
instanceRef?: MutableRefObject<ScratchCardInstance | null>;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
/** <ScratchCard onReveal={claim}><Coupon code="OTOÑO25" /></ScratchCard>. */
|
|
13
|
+
export function ScratchCard({ threshold, brush, coverLabel, revealLabel, revealedLabel, celebrate, onProgress, onReveal, instanceRef, className, children, ...props }: ScratchCardProps) {
|
|
14
|
+
const ref = useRef<HTMLDivElement>(null);
|
|
15
|
+
const progress = useLatest(onProgress);
|
|
16
|
+
const reveal = useLatest(onReveal);
|
|
17
|
+
useIsomorphicLayoutEffect(() => {
|
|
18
|
+
const created = createScratchCard(ref.current, {
|
|
19
|
+
threshold,
|
|
20
|
+
brush,
|
|
21
|
+
coverLabel,
|
|
22
|
+
revealLabel,
|
|
23
|
+
revealedLabel,
|
|
24
|
+
celebrate,
|
|
25
|
+
onProgress: (value) => progress.current?.(value),
|
|
26
|
+
onReveal: () => reveal.current?.(),
|
|
27
|
+
});
|
|
28
|
+
if (instanceRef) instanceRef.current = created;
|
|
29
|
+
return () => {
|
|
30
|
+
created.destroy();
|
|
31
|
+
if (instanceRef) instanceRef.current = null;
|
|
32
|
+
};
|
|
33
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
34
|
+
}, [threshold, brush, coverLabel, revealLabel, revealedLabel, celebrate]);
|
|
35
|
+
return (
|
|
36
|
+
<div {...props} ref={ref} className={['mk-scratch-card', className].filter(Boolean).join(' ')}>
|
|
37
|
+
<div data-mk-scratch-content="">{children}</div>
|
|
38
|
+
</div>
|
|
39
|
+
);
|
|
40
|
+
}
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
/* scratch-card — a Melonmind card (20 px radius) whose content hides under a metallic cover; a small glass
|
|
2
|
+
"Revelar" pill sits on top for the keyboard. */
|
|
3
|
+
.mk-scratch-card,
|
|
4
|
+
mk-scratch-card {
|
|
5
|
+
position: relative;
|
|
6
|
+
display: block;
|
|
7
|
+
overflow: hidden;
|
|
8
|
+
border-radius: 20px;
|
|
9
|
+
background: var(--mk-surface-1);
|
|
10
|
+
box-shadow: inset 0 0 0 1px var(--mk-outline-soft);
|
|
11
|
+
isolation: isolate;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
:is(.mk-scratch-card, mk-scratch-card) [data-mk-scratch-content] {
|
|
15
|
+
position: relative;
|
|
16
|
+
min-height: 9rem;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
:is(.mk-scratch-card, mk-scratch-card) [data-mk-scratch-cover] {
|
|
20
|
+
position: absolute;
|
|
21
|
+
inset: 0;
|
|
22
|
+
z-index: 1;
|
|
23
|
+
cursor: crosshair;
|
|
24
|
+
touch-action: none;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
:is(.mk-scratch-card, mk-scratch-card) [data-mk-scratch-cover] canvas {
|
|
28
|
+
display: block;
|
|
29
|
+
width: 100%;
|
|
30
|
+
height: 100%;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
:is(.mk-scratch-card, mk-scratch-card) [data-mk-scratch-cover][data-mk-scratch-fallback] {
|
|
34
|
+
display: grid;
|
|
35
|
+
place-items: center;
|
|
36
|
+
background: linear-gradient(135deg, #c9ccd2, #eef0f3 45%, #d7dadf 55%, #a9adb5);
|
|
37
|
+
color: rgb(40 42 48 / 0.75);
|
|
38
|
+
font-weight: 600;
|
|
39
|
+
cursor: default;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
:is(.mk-scratch-card, mk-scratch-card) [data-mk-scratch-cover][hidden] {
|
|
43
|
+
display: none;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
:is(.mk-scratch-card, mk-scratch-card) [data-mk-scratch-reveal] {
|
|
47
|
+
position: absolute;
|
|
48
|
+
right: 0.75rem;
|
|
49
|
+
bottom: 0.75rem;
|
|
50
|
+
z-index: 2;
|
|
51
|
+
min-height: 2.25rem;
|
|
52
|
+
padding: 0 0.9rem;
|
|
53
|
+
border: 0;
|
|
54
|
+
border-radius: 64px;
|
|
55
|
+
background: rgb(20 21 24 / 0.72);
|
|
56
|
+
box-shadow:
|
|
57
|
+
inset 1px 1px 1px rgb(255 255 255 / 0.14),
|
|
58
|
+
inset -1px -1px 1px rgb(255 255 255 / 0.05);
|
|
59
|
+
backdrop-filter: blur(12px);
|
|
60
|
+
color: #fff;
|
|
61
|
+
font: inherit;
|
|
62
|
+
font-size: 0.8125rem;
|
|
63
|
+
font-weight: 500;
|
|
64
|
+
cursor: pointer;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
:is(.mk-scratch-card, mk-scratch-card) [data-mk-scratch-reveal]:focus-visible {
|
|
68
|
+
outline: 2px solid var(--mk-accent);
|
|
69
|
+
outline-offset: 2px;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
:is(.mk-scratch-card, mk-scratch-card) [data-mk-scratch-reveal][hidden] {
|
|
73
|
+
display: none;
|
|
74
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
# scroll-island
|
|
2
|
+
|
|
3
|
+
Qué hace, en una frase.
|
|
4
|
+
|
|
5
|
+
Origen: archivo del kit y parámetros (tiempos, curvas).
|
|
6
|
+
|
|
7
|
+
## HTML / Astro
|
|
8
|
+
|
|
9
|
+
```html
|
|
10
|
+
<mk-scroll-island></mk-scroll-island>
|
|
11
|
+
<script>
|
|
12
|
+
import { defineElements } from '@yewdiel21/motion-kit/elements';
|
|
13
|
+
defineElements(['mk-scroll-island']);
|
|
14
|
+
</script>
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
## React
|
|
18
|
+
|
|
19
|
+
```tsx
|
|
20
|
+
<ScrollIsland />
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
## Opciones
|
|
24
|
+
|
|
25
|
+
| Opción | Atributo | Por defecto | Qué hace |
|
|
26
|
+
|---|---|---|---|
|
|
27
|
+
|
|
28
|
+
## Variables CSS
|
|
29
|
+
|
|
30
|
+
`--mk-scroll-island-radius`
|
|
31
|
+
|
|
32
|
+
## Accesibilidad
|
|
33
|
+
|
|
34
|
+
Roles, foco y teclado. Con movimiento reducido…
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { MkElement, defineElement, type OptionType } from '../../elements/base';
|
|
2
|
+
import { createScrollIsland, type ScrollIslandInstance } from './engine';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* <mk-scroll-island class="mk-scroll-island" style="height: 24rem; overflow: auto">…</mk-scroll-island>
|
|
6
|
+
* or <mk-scroll-island page></mk-scroll-island> for the page. Emits mk:section ({section}).
|
|
7
|
+
*/
|
|
8
|
+
export class MkScrollIslandElement extends MkElement<ScrollIslandInstance> {
|
|
9
|
+
static options: Record<string, OptionType> = { sections: 'string', reach: 'number', idle: 'number', page: 'boolean' };
|
|
10
|
+
|
|
11
|
+
protected create(options: Record<string, unknown>) {
|
|
12
|
+
return createScrollIsland(options.page ? document.documentElement : this, options);
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export const defineScrollIsland = () => defineElement('mk-scroll-island', MkScrollIslandElement);
|
|
@@ -0,0 +1,252 @@
|
|
|
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 { blurIn } from '../../motion/micro';
|
|
6
|
+
|
|
7
|
+
/*
|
|
8
|
+
* Scroll island: the scroller's own bar is hidden and a thin pill takes its
|
|
9
|
+
* place, shown while scrolling (it fades .9 s after). When the mouse comes near
|
|
10
|
+
* the edge, the pill swells into an island, a dark capsule (like iOS's island)
|
|
11
|
+
* with the percentage read and the current section (its last heading above a
|
|
12
|
+
* third of the view), on Melonmind's swing; dragging the island scrolls, a click
|
|
13
|
+
* on the rail glides there. Far from the edge it shrinks back into the bar. It
|
|
14
|
+
* follows the page or any scrolling element; touch keeps the native scrolling.
|
|
15
|
+
* The island is decorative (aria-hidden): keyboard and assistive tech scroll as
|
|
16
|
+
* always.
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
export interface ScrollIslandOptions {
|
|
20
|
+
/** Headings that name the sections (default 'h2, h3'); false: only the percentage. */
|
|
21
|
+
sections?: string | false;
|
|
22
|
+
/** How near the edge, in px, the pointer turns the bar into the island (default 56). */
|
|
23
|
+
reach?: number;
|
|
24
|
+
/** ms the bar stays after scrolling stops (default 900). */
|
|
25
|
+
idle?: number;
|
|
26
|
+
/** Follow the page's scroll (default: when the root is <html> or <body>). */
|
|
27
|
+
page?: boolean;
|
|
28
|
+
/** The current section changed (its heading's text, or null before the first). */
|
|
29
|
+
onSection?: (section: string | null) => void;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export interface ScrollIslandInstance extends Instance {
|
|
33
|
+
/** Measure again (after the content changed size). */
|
|
34
|
+
refresh(): void;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
type State = 'hidden' | 'bar' | 'island';
|
|
38
|
+
/** Space kept from the scroller's edges. */
|
|
39
|
+
const INSET = 6;
|
|
40
|
+
const BAR_WIDTH = 4;
|
|
41
|
+
const ISLAND_HEIGHT = 36;
|
|
42
|
+
const MIN_BAR = 28;
|
|
43
|
+
|
|
44
|
+
export function createScrollIsland(root: HTMLElement | null, options: ScrollIslandOptions = {}): ScrollIslandInstance {
|
|
45
|
+
if (!root) return inert({ refresh() {} });
|
|
46
|
+
registerMotion();
|
|
47
|
+
const scope = createScope();
|
|
48
|
+
const page = options.page ?? (root === document.documentElement || root === document.body);
|
|
49
|
+
const scroller = page ? ((document.scrollingElement as HTMLElement | null) ?? document.documentElement) : root;
|
|
50
|
+
const reach = Number.isFinite(options.reach) ? Math.max(8, options.reach!) : 56;
|
|
51
|
+
const idle = Number.isFinite(options.idle) ? Math.max(0, options.idle!) : 900;
|
|
52
|
+
const marked = page ? document.documentElement : root;
|
|
53
|
+
// The scroller's own bar hides (scroll-island.css).
|
|
54
|
+
scope.attr(marked, 'data-mk-scroll-island-host', '');
|
|
55
|
+
|
|
56
|
+
const island = document.createElement('div');
|
|
57
|
+
island.dataset.mkScrollIsland = '';
|
|
58
|
+
island.dataset.mkState = 'hidden';
|
|
59
|
+
island.setAttribute('aria-hidden', 'true');
|
|
60
|
+
const rail = document.createElement('span');
|
|
61
|
+
rail.dataset.mkIslandRail = '';
|
|
62
|
+
const thumb = document.createElement('div');
|
|
63
|
+
thumb.dataset.mkIslandThumb = '';
|
|
64
|
+
const content = document.createElement('span');
|
|
65
|
+
content.dataset.mkIslandContent = '';
|
|
66
|
+
const percent = document.createElement('span');
|
|
67
|
+
percent.dataset.mkIslandPercent = '';
|
|
68
|
+
const sectionText = document.createElement('span');
|
|
69
|
+
sectionText.dataset.mkIslandSection = '';
|
|
70
|
+
content.append(percent, sectionText);
|
|
71
|
+
thumb.append(content);
|
|
72
|
+
island.append(rail, thumb);
|
|
73
|
+
document.body.append(island);
|
|
74
|
+
scope.add(() => island.remove());
|
|
75
|
+
|
|
76
|
+
const effects = new Set<gsap.core.Animation>();
|
|
77
|
+
const keep = (animation: gsap.core.Animation | null) => {
|
|
78
|
+
if (!animation) return;
|
|
79
|
+
effects.add(animation);
|
|
80
|
+
// Finished ones leave as new ones arrive: the animation's own onComplete stays untouched.
|
|
81
|
+
for (const effect of effects) if (effect.totalProgress() === 1) effects.delete(effect);
|
|
82
|
+
};
|
|
83
|
+
scope.add(() => {
|
|
84
|
+
for (const effect of effects) effect.kill();
|
|
85
|
+
effects.clear();
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
/** The scroller's box on screen (the window for the page). */
|
|
89
|
+
const frame = () => (page ? { top: 0, left: 0, right: innerWidth, bottom: innerHeight, height: innerHeight } : root.getBoundingClientRect());
|
|
90
|
+
const track = () => {
|
|
91
|
+
const box = frame();
|
|
92
|
+
return { top: box.top + INSET, height: Math.max(0, box.height - INSET * 2), right: box.right, left: box.left, bottom: box.bottom };
|
|
93
|
+
};
|
|
94
|
+
const range = () => Math.max(0, scroller.scrollHeight - scroller.clientHeight);
|
|
95
|
+
const scrollable = () => range() > 1;
|
|
96
|
+
const progress = () => (range() ? Math.min(1, Math.max(0, scroller.scrollTop / range())) : 0);
|
|
97
|
+
|
|
98
|
+
let state: State = 'hidden';
|
|
99
|
+
let section: string | null | undefined;
|
|
100
|
+
let hideTimer: ReturnType<typeof setTimeout> | undefined;
|
|
101
|
+
let dragging: { id: number; offset: number } | null = null;
|
|
102
|
+
scope.add(() => clearTimeout(hideTimer));
|
|
103
|
+
|
|
104
|
+
const headings = () => (options.sections === false ? [] : [...(page ? document.body : root).querySelectorAll<HTMLElement>(options.sections ?? 'h2, h3')]);
|
|
105
|
+
const currentSection = () => {
|
|
106
|
+
const box = frame();
|
|
107
|
+
const line = box.top + scroller.clientHeight * 0.3;
|
|
108
|
+
let found: HTMLElement | null = null;
|
|
109
|
+
for (const heading of headings()) if (heading.getBoundingClientRect().top <= line) found = heading;
|
|
110
|
+
return found?.textContent?.trim() || null;
|
|
111
|
+
};
|
|
112
|
+
|
|
113
|
+
/** Where the thumb goes: the bar's length follows the view; the island is a fixed capsule. */
|
|
114
|
+
const geometry = (as: State) => {
|
|
115
|
+
const { height } = track();
|
|
116
|
+
const size = as === 'island' ? ISLAND_HEIGHT : Math.max(MIN_BAR, Math.min(height, height * (scroller.clientHeight / Math.max(1, scroller.scrollHeight))));
|
|
117
|
+
const top = (height - size) * progress();
|
|
118
|
+
if (as !== 'island') return { top, height: size, width: BAR_WIDTH, borderRadius: BAR_WIDTH / 2 };
|
|
119
|
+
return { top, height: size, width: Math.max(ISLAND_HEIGHT, content.scrollWidth + 28), borderRadius: size / 2 };
|
|
120
|
+
};
|
|
121
|
+
|
|
122
|
+
const place = () => {
|
|
123
|
+
const box = track();
|
|
124
|
+
Object.assign(island.style, { top: `${box.top}px`, height: `${box.height}px`, left: `${box.left}px`, width: `${Math.max(0, box.right - box.left - 4)}px` });
|
|
125
|
+
};
|
|
126
|
+
|
|
127
|
+
const paintText = () => {
|
|
128
|
+
percent.textContent = `${Math.round(progress() * 100)} %`;
|
|
129
|
+
const now = currentSection();
|
|
130
|
+
if (now !== section) {
|
|
131
|
+
const first = section === undefined;
|
|
132
|
+
section = now;
|
|
133
|
+
sectionText.textContent = now ?? '';
|
|
134
|
+
sectionText.hidden = !now;
|
|
135
|
+
if (!first && state === 'island' && !reduced()) keep(blurIn(sectionText, { from: 'soft', duration: 0.3 }));
|
|
136
|
+
options.onSection?.(now);
|
|
137
|
+
root.dispatchEvent(new CustomEvent('mk:section', { bubbles: true, detail: { section: now } }));
|
|
138
|
+
}
|
|
139
|
+
};
|
|
140
|
+
|
|
141
|
+
/** Move to a state; `follow` keeps the thumb on the position without a tween (while scrolling or dragging). */
|
|
142
|
+
const go = (next: State, follow = false) => {
|
|
143
|
+
if (!scrollable()) next = 'hidden';
|
|
144
|
+
const changed = next !== state;
|
|
145
|
+
state = next;
|
|
146
|
+
island.dataset.mkState = next;
|
|
147
|
+
place();
|
|
148
|
+
const shape = geometry(next === 'hidden' ? 'bar' : next);
|
|
149
|
+
if (follow || !changed || reduced()) {
|
|
150
|
+
gsap.killTweensOf(thumb);
|
|
151
|
+
gsap.set(thumb, shape);
|
|
152
|
+
return;
|
|
153
|
+
}
|
|
154
|
+
gsap.killTweensOf(thumb);
|
|
155
|
+
keep(gsap.to(thumb, { ...shape, duration: next === 'island' ? 0.42 : 0.32, ease: next === 'island' ? EASE.swatch : 'power3.out' }));
|
|
156
|
+
};
|
|
157
|
+
|
|
158
|
+
const settleLater = () => {
|
|
159
|
+
clearTimeout(hideTimer);
|
|
160
|
+
hideTimer = setTimeout(() => {
|
|
161
|
+
if (state === 'bar' && !dragging) go('hidden');
|
|
162
|
+
}, idle);
|
|
163
|
+
};
|
|
164
|
+
|
|
165
|
+
const onScroll = () => {
|
|
166
|
+
paintText();
|
|
167
|
+
if (state === 'island' || dragging) go(state, true);
|
|
168
|
+
else {
|
|
169
|
+
go('bar', true);
|
|
170
|
+
settleLater();
|
|
171
|
+
}
|
|
172
|
+
};
|
|
173
|
+
scope.on(page ? window : root, 'scroll', onScroll, { passive: true });
|
|
174
|
+
// A scroller inside the page moves with the page: the overlay follows it.
|
|
175
|
+
if (!page) scope.on(window, 'scroll', () => state !== 'hidden' && place(), { passive: true, capture: true });
|
|
176
|
+
scope.on(window, 'resize', () => go(state, true));
|
|
177
|
+
|
|
178
|
+
const near = (x: number, y: number) => {
|
|
179
|
+
const box = track();
|
|
180
|
+
return y >= box.top - INSET && y <= box.top + box.height + INSET && x <= box.right && box.right - x <= reach;
|
|
181
|
+
};
|
|
182
|
+
scope.on(window, 'pointermove', (event) => {
|
|
183
|
+
const move = event as PointerEvent;
|
|
184
|
+
if (move.pointerType === 'touch') return;
|
|
185
|
+
if (dragging) {
|
|
186
|
+
if (move.pointerId !== dragging.id) return;
|
|
187
|
+
const box = track();
|
|
188
|
+
const shape = geometry('island');
|
|
189
|
+
const ratio = (move.clientY - box.top - dragging.offset) / Math.max(1, box.height - shape.height);
|
|
190
|
+
scroller.scrollTop = Math.min(1, Math.max(0, ratio)) * range();
|
|
191
|
+
onScroll();
|
|
192
|
+
return;
|
|
193
|
+
}
|
|
194
|
+
if (near(move.clientX, move.clientY)) {
|
|
195
|
+
if (state !== 'island' && scrollable()) {
|
|
196
|
+
clearTimeout(hideTimer);
|
|
197
|
+
paintText();
|
|
198
|
+
go('island');
|
|
199
|
+
}
|
|
200
|
+
} else if (state === 'island') {
|
|
201
|
+
const box = track();
|
|
202
|
+
// A little slack, so the island does not flicker at the border of its reach.
|
|
203
|
+
if (box.right - move.clientX > reach + 24 || move.clientY < box.top - 24 || move.clientY > box.top + box.height + 24) {
|
|
204
|
+
go('bar');
|
|
205
|
+
settleLater();
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
// Dragging the island scrolls; a click on the rail glides there.
|
|
211
|
+
scope.on(thumb, 'pointerdown', (event) => {
|
|
212
|
+
const down = event as PointerEvent;
|
|
213
|
+
if (state !== 'island' || down.button !== 0) return;
|
|
214
|
+
down.preventDefault();
|
|
215
|
+
down.stopPropagation();
|
|
216
|
+
const box = thumb.getBoundingClientRect();
|
|
217
|
+
dragging = { id: down.pointerId, offset: down.clientY - box.top || ISLAND_HEIGHT / 2 };
|
|
218
|
+
island.setAttribute('data-mk-dragging', '');
|
|
219
|
+
try {
|
|
220
|
+
thumb.setPointerCapture(down.pointerId);
|
|
221
|
+
} catch {
|
|
222
|
+
// jsdom and synthetic events have no active pointer to capture.
|
|
223
|
+
}
|
|
224
|
+
});
|
|
225
|
+
const release = (event: Event) => {
|
|
226
|
+
if (!dragging || (event as PointerEvent).pointerId !== dragging.id) return;
|
|
227
|
+
dragging = null;
|
|
228
|
+
island.removeAttribute('data-mk-dragging');
|
|
229
|
+
};
|
|
230
|
+
scope.on(window, 'pointerup', release);
|
|
231
|
+
scope.on(window, 'pointercancel', release);
|
|
232
|
+
scope.on(rail, 'pointerdown', (event) => {
|
|
233
|
+
const down = event as PointerEvent;
|
|
234
|
+
if (state !== 'island' || down.button !== 0) return;
|
|
235
|
+
down.preventDefault();
|
|
236
|
+
const box = track();
|
|
237
|
+
const size = geometry('island').height;
|
|
238
|
+
const ratio = (down.clientY - box.top - size / 2) / Math.max(1, box.height - size);
|
|
239
|
+
scroller.scrollTo({ top: Math.round(Math.min(1, Math.max(0, ratio)) * range()), behavior: reduced() ? 'auto' : 'smooth' });
|
|
240
|
+
});
|
|
241
|
+
|
|
242
|
+
paintText();
|
|
243
|
+
go('hidden');
|
|
244
|
+
|
|
245
|
+
return {
|
|
246
|
+
refresh: () => {
|
|
247
|
+
paintText();
|
|
248
|
+
go(state, true);
|
|
249
|
+
},
|
|
250
|
+
destroy: () => scope.dispose(),
|
|
251
|
+
};
|
|
252
|
+
}
|