@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,101 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
import { forwardRef, useImperativeHandle, useRef, useState, type HTMLAttributes, type ReactElement, type ReactNode, type Ref } from 'react';
|
|
3
|
+
import { useIsomorphicLayoutEffect, useLatest } from '../../react/use-recipe';
|
|
4
|
+
import type { VirtualRange } from '../../core/virtual';
|
|
5
|
+
import { createVirtualList, type VirtualKey, type VirtualListInstance, type VirtualListOptions, type VirtualScrollOptions } from './engine';
|
|
6
|
+
|
|
7
|
+
export interface VirtualListProps<T> extends Omit<HTMLAttributes<HTMLDivElement>, 'onSelect' | 'children'>, Omit<VirtualListOptions<T>, 'items' | 'renderItem' | 'onRange'> {
|
|
8
|
+
items: T[];
|
|
9
|
+
/** A row's content; without it, its text. */
|
|
10
|
+
renderItem?: (item: T, index: number) => ReactNode;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export interface VirtualListHandle {
|
|
14
|
+
scrollToIndex(index: number, options?: VirtualScrollOptions): void;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const keyFor = <T,>(getKey: VirtualListOptions<T>['getKey'], item: T, index: number): VirtualKey => {
|
|
18
|
+
if (getKey) return getKey(item, index);
|
|
19
|
+
const id = item && typeof item === 'object' ? (item as { id?: unknown }).id : undefined;
|
|
20
|
+
return typeof id === 'string' || typeof id === 'number' ? id : index;
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* <VirtualList items={rows} itemHeight={44} renderItem={(row) => <Row {...row} />} selectable="multiple" />.
|
|
25
|
+
* React renders only the rows in view; the engine adopts its viewport, and places, measures and picks the rows.
|
|
26
|
+
*/
|
|
27
|
+
function VirtualListInner<T>(
|
|
28
|
+
{ items, renderItem, itemHeight, estimatedHeight, overscan, itemText, getKey, selectable, selected, label, emptyLabel, locale, onSelect, onActivate, onEndReached, className, ...props }: VirtualListProps<T>,
|
|
29
|
+
handle: Ref<VirtualListHandle>,
|
|
30
|
+
) {
|
|
31
|
+
const ref = useRef<HTMLDivElement>(null);
|
|
32
|
+
const instance = useRef<VirtualListInstance<T> | null>(null);
|
|
33
|
+
const [range, setRange] = useState<VirtualRange>({ start: 0, end: 0 });
|
|
34
|
+
const shown = useRef(items);
|
|
35
|
+
const select = useLatest(onSelect);
|
|
36
|
+
const activate = useLatest(onActivate);
|
|
37
|
+
const more = useLatest(onEndReached);
|
|
38
|
+
const text = useLatest(itemText);
|
|
39
|
+
const key = useLatest(getKey);
|
|
40
|
+
useImperativeHandle(handle, () => ({ scrollToIndex: (index, options) => instance.current?.scrollToIndex(index, options) }), []);
|
|
41
|
+
const custom = Boolean(renderItem);
|
|
42
|
+
useIsomorphicLayoutEffect(() => {
|
|
43
|
+
const created = createVirtualList<T>(ref.current, {
|
|
44
|
+
items: shown.current,
|
|
45
|
+
itemHeight,
|
|
46
|
+
estimatedHeight,
|
|
47
|
+
overscan,
|
|
48
|
+
selectable,
|
|
49
|
+
selected,
|
|
50
|
+
label,
|
|
51
|
+
emptyLabel,
|
|
52
|
+
locale,
|
|
53
|
+
itemText: itemText ? (item) => text.current?.(item) ?? '' : undefined,
|
|
54
|
+
getKey: (item, index) => keyFor(key.current, item, index),
|
|
55
|
+
onSelect: (keys, chosen) => select.current?.(keys, chosen),
|
|
56
|
+
onActivate: (item, index) => activate.current?.(item, index),
|
|
57
|
+
onEndReached: onEndReached ? () => more.current?.() : undefined,
|
|
58
|
+
onRange: custom ? setRange : undefined,
|
|
59
|
+
});
|
|
60
|
+
instance.current = created;
|
|
61
|
+
return () => {
|
|
62
|
+
created.destroy();
|
|
63
|
+
instance.current = null;
|
|
64
|
+
};
|
|
65
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
66
|
+
}, [itemHeight, estimatedHeight, overscan, String(selectable), label, emptyLabel, locale, custom, Boolean(onEndReached), Boolean(itemText)]);
|
|
67
|
+
useIsomorphicLayoutEffect(() => {
|
|
68
|
+
if (shown.current === items) return;
|
|
69
|
+
shown.current = items;
|
|
70
|
+
instance.current?.setItems(items);
|
|
71
|
+
}, [items]);
|
|
72
|
+
const selectedKey = JSON.stringify(selected ?? null);
|
|
73
|
+
useIsomorphicLayoutEffect(() => {
|
|
74
|
+
if (selected && instance.current && JSON.stringify(instance.current.selected) !== JSON.stringify(selected)) instance.current.select(selected);
|
|
75
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
76
|
+
}, [selectedKey]);
|
|
77
|
+
// After React drew the window: place and measure it.
|
|
78
|
+
useIsomorphicLayoutEffect(() => {
|
|
79
|
+
if (custom) instance.current?.refresh();
|
|
80
|
+
});
|
|
81
|
+
return (
|
|
82
|
+
<div {...props} ref={ref} className={['mk-virtual-list', className].filter(Boolean).join(' ')}>
|
|
83
|
+
<div data-mk-vl-viewport="">
|
|
84
|
+
<div data-mk-vl-spacer="">
|
|
85
|
+
{renderItem
|
|
86
|
+
? items.slice(range.start, Math.min(range.end, items.length)).map((item, i) => {
|
|
87
|
+
const index = range.start + i;
|
|
88
|
+
return (
|
|
89
|
+
<div key={keyFor(getKey, item, index)} data-mk-vl-item="" data-index={index}>
|
|
90
|
+
{renderItem(item, index)}
|
|
91
|
+
</div>
|
|
92
|
+
);
|
|
93
|
+
})
|
|
94
|
+
: null}
|
|
95
|
+
</div>
|
|
96
|
+
</div>
|
|
97
|
+
</div>
|
|
98
|
+
);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
export const VirtualList = forwardRef(VirtualListInner) as <T>(props: VirtualListProps<T> & { ref?: Ref<VirtualListHandle> }) => ReactElement;
|
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
/* virtual-list — a 12 px box on surface-1 that scrolls inside (--mk-vl-height, 24rem); the rows sit on a spacer as tall
|
|
2
|
+
as all of them. Chosen rows take accent-soft and a check that pops; a 2 px accent ring glides between rows for the
|
|
3
|
+
keyboard; a row reached with scrollToIndex() flashes; the position pill (8 px, inverse glass) rides the right edge
|
|
4
|
+
while scrolling. */
|
|
5
|
+
.mk-virtual-list,
|
|
6
|
+
mk-virtual-list {
|
|
7
|
+
--mk-vl-height: 24rem;
|
|
8
|
+
position: relative;
|
|
9
|
+
display: block;
|
|
10
|
+
min-width: 0;
|
|
11
|
+
color: var(--mk-text);
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
[data-mk-vl-viewport] {
|
|
15
|
+
position: relative;
|
|
16
|
+
box-sizing: border-box;
|
|
17
|
+
height: var(--mk-vl-height);
|
|
18
|
+
overflow-y: auto;
|
|
19
|
+
border: 1px solid var(--mk-outline-soft);
|
|
20
|
+
border-radius: 12px;
|
|
21
|
+
outline: none;
|
|
22
|
+
background: var(--mk-surface-1);
|
|
23
|
+
overscroll-behavior: contain;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
[data-mk-vl-viewport]:focus-visible {
|
|
27
|
+
box-shadow: 0 0 0 3px color-mix(in srgb, var(--mk-accent) 35%, transparent);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
[data-mk-vl-spacer] {
|
|
31
|
+
position: relative;
|
|
32
|
+
width: 100%;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
[data-mk-vl-item] {
|
|
36
|
+
position: absolute;
|
|
37
|
+
top: 0;
|
|
38
|
+
right: 0;
|
|
39
|
+
left: 0;
|
|
40
|
+
box-sizing: border-box;
|
|
41
|
+
display: flex;
|
|
42
|
+
align-items: center;
|
|
43
|
+
gap: 0.75rem;
|
|
44
|
+
min-width: 0;
|
|
45
|
+
padding: 0.75rem 1rem;
|
|
46
|
+
border-bottom: 1px solid var(--mk-outline-soft);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
[role='listbox'] > [data-mk-vl-spacer] > [data-mk-vl-item] {
|
|
50
|
+
cursor: pointer;
|
|
51
|
+
-webkit-user-select: none;
|
|
52
|
+
user-select: none;
|
|
53
|
+
transition:
|
|
54
|
+
background-color 0.15s,
|
|
55
|
+
color 0.15s;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
[role='listbox'] > [data-mk-vl-spacer] > [data-mk-vl-item]:hover {
|
|
59
|
+
background: color-mix(in srgb, var(--mk-text) 5%, transparent);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
[data-mk-vl-item][aria-selected='true'],
|
|
63
|
+
[role='listbox'] > [data-mk-vl-spacer] > [data-mk-vl-item][aria-selected='true']:hover {
|
|
64
|
+
background: var(--mk-accent-soft);
|
|
65
|
+
color: var(--mk-on-accent-soft, var(--mk-text));
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/* The check of a chosen row; it pops only on the row just chosen, not on rows scrolled into view. */
|
|
69
|
+
[role='listbox'] > [data-mk-vl-spacer] > [data-mk-vl-item]::after {
|
|
70
|
+
content: '';
|
|
71
|
+
flex: none;
|
|
72
|
+
width: 1.125rem;
|
|
73
|
+
height: 1.125rem;
|
|
74
|
+
margin-left: auto;
|
|
75
|
+
background: var(--mk-accent);
|
|
76
|
+
-webkit-mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='black' stroke-width='3' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M5 12.5l4.5 4.5L19 7.5'/%3E%3C/svg%3E") center / contain no-repeat;
|
|
77
|
+
mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='black' stroke-width='3' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M5 12.5l4.5 4.5L19 7.5'/%3E%3C/svg%3E") center / contain no-repeat;
|
|
78
|
+
opacity: 0;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
[role='listbox'] > [data-mk-vl-spacer] > [data-mk-vl-item][aria-selected='true']::after {
|
|
82
|
+
opacity: 1;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
[data-mk-vl-item][data-mk-vl-just]::after {
|
|
86
|
+
animation: mk-vl-check 0.4s cubic-bezier(0.38, 0.49, 0, 1.5);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
@keyframes mk-vl-check {
|
|
90
|
+
from {
|
|
91
|
+
opacity: 0;
|
|
92
|
+
transform: scale(0.3) rotate(-20deg);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
[data-mk-vl-cursor] {
|
|
97
|
+
position: absolute;
|
|
98
|
+
top: 0;
|
|
99
|
+
right: 4px;
|
|
100
|
+
left: 4px;
|
|
101
|
+
z-index: 1;
|
|
102
|
+
border-radius: 8px;
|
|
103
|
+
box-shadow: inset 0 0 0 2px var(--mk-accent);
|
|
104
|
+
opacity: 0;
|
|
105
|
+
pointer-events: none;
|
|
106
|
+
transition: opacity 0.15s;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
[data-mk-vl-viewport]:focus-visible [data-mk-vl-cursor] {
|
|
110
|
+
opacity: 1;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
[data-mk-vl-flash] {
|
|
114
|
+
animation: mk-vl-flash 1.2s cubic-bezier(0.38, 0.49, 0, 1);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
@keyframes mk-vl-flash {
|
|
118
|
+
0%,
|
|
119
|
+
30% {
|
|
120
|
+
background-color: color-mix(in srgb, var(--mk-accent) 24%, var(--mk-surface-1));
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
[data-mk-vl-foot] {
|
|
125
|
+
position: absolute;
|
|
126
|
+
top: 0;
|
|
127
|
+
right: 0;
|
|
128
|
+
left: 0;
|
|
129
|
+
display: flex;
|
|
130
|
+
align-items: center;
|
|
131
|
+
justify-content: center;
|
|
132
|
+
gap: 0.5rem;
|
|
133
|
+
height: 48px;
|
|
134
|
+
color: var(--mk-muted);
|
|
135
|
+
font-size: 0.875rem;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
[data-mk-vl-foot][hidden],
|
|
139
|
+
[data-mk-vl-empty][hidden],
|
|
140
|
+
[data-mk-vl-pill][hidden] {
|
|
141
|
+
display: none;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
[data-mk-vl-foot][aria-busy='true']::before {
|
|
145
|
+
content: '';
|
|
146
|
+
width: 0.875rem;
|
|
147
|
+
height: 0.875rem;
|
|
148
|
+
border: 2px solid currentColor;
|
|
149
|
+
border-right-color: transparent;
|
|
150
|
+
border-radius: 50%;
|
|
151
|
+
animation: mk-vl-spin 0.8s linear infinite;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
@keyframes mk-vl-spin {
|
|
155
|
+
to {
|
|
156
|
+
transform: rotate(360deg);
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
[data-mk-vl-empty] {
|
|
161
|
+
position: absolute;
|
|
162
|
+
inset: 0;
|
|
163
|
+
display: grid;
|
|
164
|
+
place-items: center;
|
|
165
|
+
margin: 0;
|
|
166
|
+
color: var(--mk-muted);
|
|
167
|
+
font-size: 0.875rem;
|
|
168
|
+
pointer-events: none;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
[data-mk-vl-pill] {
|
|
172
|
+
position: absolute;
|
|
173
|
+
top: 0.5rem;
|
|
174
|
+
right: 0.875rem;
|
|
175
|
+
z-index: 2;
|
|
176
|
+
padding: 0.3rem 0.625rem;
|
|
177
|
+
border-radius: 8px;
|
|
178
|
+
background: var(--mk-glass-inverse, var(--mk-text));
|
|
179
|
+
box-shadow: 0 6px 18px -8px color-mix(in srgb, var(--mk-shadow-color, #000) 45%, transparent);
|
|
180
|
+
color: var(--mk-on-glass-inverse, var(--mk-surface-1));
|
|
181
|
+
font-size: 0.75rem;
|
|
182
|
+
font-variant-numeric: tabular-nums;
|
|
183
|
+
font-weight: 600;
|
|
184
|
+
white-space: nowrap;
|
|
185
|
+
pointer-events: none;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
@media (prefers-reduced-motion: reduce) {
|
|
189
|
+
[data-mk-vl-item][data-mk-vl-just]::after,
|
|
190
|
+
[data-mk-vl-flash],
|
|
191
|
+
[data-mk-vl-foot][aria-busy='true']::before {
|
|
192
|
+
animation: none;
|
|
193
|
+
}
|
|
194
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
# voice-dots
|
|
2
|
+
|
|
3
|
+
Puntos que respiran, escuchan, piensan y hablan; pueden seguir el nivel real de una fuente de audio.
|
|
4
|
+
|
|
5
|
+
## Instalación
|
|
6
|
+
|
|
7
|
+
```tsx
|
|
8
|
+
import '@yewdiel21/motion-kit/css';
|
|
9
|
+
import { VoiceDots } from '@yewdiel21/motion-kit/react';
|
|
10
|
+
```
|
|
11
|
+
|
|
12
|
+
```sh
|
|
13
|
+
npx @yewdiel21/motion-kit add voice-dots --react --elements
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
## React
|
|
17
|
+
|
|
18
|
+
```tsx
|
|
19
|
+
<VoiceDots state="listening" source={stream} level={level} />
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
Los callbacks del ejemplo (`ask`, `run`, `setTool`, `setVolume`) pertenecen a tu aplicación. React acepta `className`, `style` y atributos HTML. Los componentes con estado exponen `instanceRef` (excepto PillButton).
|
|
23
|
+
|
|
24
|
+
## HTML / Astro
|
|
25
|
+
|
|
26
|
+
```html
|
|
27
|
+
<mk-voice-dots state="listening" size="12"></mk-voice-dots>
|
|
28
|
+
<script type="module">
|
|
29
|
+
import { defineElements } from '@yewdiel21/motion-kit/elements';
|
|
30
|
+
defineElements(['mk-voice-dots']);
|
|
31
|
+
</script>
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
En Astro usa un `<script>` procesado por Astro. En HTML directo el import requiere bundler o import map. También puedes usar `import { createVoiceDots } from '@yewdiel21/motion-kit'` y llamar a la factory con el contenedor y las mismas opciones. Al desmontar, llama a `instance.destroy()`.
|
|
35
|
+
|
|
36
|
+
## Opciones
|
|
37
|
+
|
|
38
|
+
state: idle, listening, thinking o speaking. count: 4 (3 a 6). size: 12 px. colorful: false. labels: textos de estado. En React source acepta MediaStream o HTMLMediaElement; level acepta 0…1 o un array. Instancia: setState(), setLevel(), listen(source), stop(), state.
|
|
39
|
+
|
|
40
|
+
El componente no solicita acceso al micrófono por sí solo. Obtén el stream tras una acción del usuario con `navigator.mediaDevices.getUserMedia({audio:true})`. `await instance.listen(stream)` devuelve una función para desconectar el análisis; la aplicación debe detener sus tracks al terminar. La fuente se analiza localmente. `stop()` desconecta la fuente. Sin audio, listening acepta `setLevel()` y speaking usa una onda de demostración.
|
|
41
|
+
|
|
42
|
+
Los nombres camelCase se escriben como atributos kebab-case. Los arrays y objetos se pasan como JSON. Los booleanos aceptan atributo vacío o `"false"`.
|
|
43
|
+
|
|
44
|
+
## Estilos y accesibilidad
|
|
45
|
+
|
|
46
|
+
Variables: --mk-voice-size; colores semánticos del tema..
|
|
47
|
+
|
|
48
|
+
Respeta los colores del tema, foco visible y `prefers-reduced-motion`. Los gráficos incluyen una tabla accesible y selección con flechas, Home, End y Escape. Heatmap usa arriba/abajo para un día e izquierda/derecha para una semana. Toolbar usa flechas y Home/End; OrbitMenu cierra con Escape y devuelve el foco. Los controles conservan nombres accesibles; VoiceDots anuncia los estados sin leer cada fotograma.
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { MkElement, defineElement, type OptionType } from '../../elements/base';
|
|
2
|
+
import { createVoiceDots, type VoiceDotsInstance, type VoiceState } from './engine';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* <mk-voice-dots class="mk-voice-dots" state="listening"></mk-voice-dots>
|
|
6
|
+
* Dots that move with a voice. element.listen(stream) follows a microphone; emits mk:state ({state}).
|
|
7
|
+
*/
|
|
8
|
+
export class MkVoiceDotsElement extends MkElement<VoiceDotsInstance> {
|
|
9
|
+
static options: Record<string, OptionType> = { state: 'string', count: 'number', colorful: 'boolean', size: 'number' };
|
|
10
|
+
|
|
11
|
+
protected create(options: Record<string, unknown>) {
|
|
12
|
+
return createVoiceDots(this, options);
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/** A new state flows from where the dots are (the engine, and the audio it follows, stay). */
|
|
16
|
+
attributeChangedCallback(name: string, previous: string | null, next: string | null) {
|
|
17
|
+
if (name === 'state' && this.instance) {
|
|
18
|
+
this.instance.setState((next ?? 'idle') as VoiceState);
|
|
19
|
+
return;
|
|
20
|
+
}
|
|
21
|
+
super.attributeChangedCallback(name, previous, next);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
get state() {
|
|
25
|
+
return this.instance?.state ?? 'idle';
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
set state(next: VoiceState) {
|
|
29
|
+
this.setAttribute('state', next);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
setLevel(level: number | number[]) {
|
|
33
|
+
this.instance?.setLevel(level);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
listen(source: MediaStream | HTMLMediaElement) {
|
|
37
|
+
return this.instance?.listen(source) ?? Promise.resolve(() => {});
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
stop() {
|
|
41
|
+
this.instance?.stop();
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export const defineVoiceDots = () => defineElement('mk-voice-dots', MkVoiceDotsElement);
|
|
@@ -0,0 +1,243 @@
|
|
|
1
|
+
import { registerMotion } from '../../core/gsap';
|
|
2
|
+
import { whileVisible } from '../../core/loop';
|
|
3
|
+
import { reduced } from '../../core/reduced';
|
|
4
|
+
import { createScope } from '../../core/scope';
|
|
5
|
+
import { inert, type Instance } from '../../core/types';
|
|
6
|
+
|
|
7
|
+
/*
|
|
8
|
+
* Voice dots: four dots that show a voice conversation, like an assistant's.
|
|
9
|
+
* Idle, they breathe one after another. Listening, each dot stretches into a
|
|
10
|
+
* pill with its band of the voice (from a microphone or a playing element
|
|
11
|
+
* through an AnalyserNode, or levels the app gives), springing up fast and
|
|
12
|
+
* settling slower so syllables read as a bounce. Thinking, they gather into a
|
|
13
|
+
* ring and orbit. Speaking, they ride a travelling wave (with the playing
|
|
14
|
+
* audio's level when there is one). Every change of state flows from where the
|
|
15
|
+
* dots are, so nothing jumps; the frame loop only runs while the dots are on
|
|
16
|
+
* screen. Monochrome (the text colour) by default, or the chart colours. A
|
|
17
|
+
* status line says the state to assistive tech; with reduced motion the dots
|
|
18
|
+
* stay still and only their colour and the status change.
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
export type VoiceState = 'idle' | 'listening' | 'thinking' | 'speaking';
|
|
22
|
+
|
|
23
|
+
export interface VoiceDotsOptions {
|
|
24
|
+
/** Default 'idle'. */
|
|
25
|
+
state?: VoiceState;
|
|
26
|
+
/** Dots (default 4, 3…6). */
|
|
27
|
+
count?: number;
|
|
28
|
+
/** The chart colours instead of the text colour. */
|
|
29
|
+
colorful?: boolean;
|
|
30
|
+
/** Dot size in px (default 12). */
|
|
31
|
+
size?: number;
|
|
32
|
+
/** The status text for each state. */
|
|
33
|
+
labels?: Partial<Record<VoiceState, string>>;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export interface VoiceDotsInstance extends Instance {
|
|
37
|
+
setState(state: VoiceState): void;
|
|
38
|
+
readonly state: VoiceState;
|
|
39
|
+
/** The voice's level, 0…1: one for all dots or one per dot (from your own analysis). */
|
|
40
|
+
setLevel(level: number | number[]): void;
|
|
41
|
+
/** Follow a microphone stream or a playing <audio>/<video>; resolves to a function that stops following. */
|
|
42
|
+
listen(source: MediaStream | HTMLMediaElement): Promise<() => void>;
|
|
43
|
+
/** Stop following the audio (the state stays). */
|
|
44
|
+
stop(): void;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const LABELS: Record<VoiceState, string> = { idle: 'Asistente en espera', listening: 'Escuchando…', thinking: 'Pensando…', speaking: 'Hablando…' };
|
|
48
|
+
const STATES: VoiceState[] = ['idle', 'listening', 'thinking', 'speaking'];
|
|
49
|
+
/** One AudioContext source per media element: the platform allows only one. */
|
|
50
|
+
const mediaSources = new WeakMap<HTMLMediaElement, MediaElementAudioSourceNode>();
|
|
51
|
+
let audio: AudioContext | null = null;
|
|
52
|
+
|
|
53
|
+
export function createVoiceDots(root: HTMLElement | null, options: VoiceDotsOptions = {}): VoiceDotsInstance {
|
|
54
|
+
if (!root) return inert({ setState() {}, state: 'idle' as VoiceState, setLevel() {}, listen: async () => () => {}, stop() {} });
|
|
55
|
+
registerMotion();
|
|
56
|
+
const scope = createScope();
|
|
57
|
+
const count = Math.max(3, Math.min(6, Math.round(options.count ?? 4)));
|
|
58
|
+
const size = Math.max(4, options.size ?? 12);
|
|
59
|
+
const labels = { ...LABELS, ...options.labels };
|
|
60
|
+
let state: VoiceState = STATES.includes(options.state as VoiceState) ? options.state! : 'idle';
|
|
61
|
+
|
|
62
|
+
const row = document.createElement('span');
|
|
63
|
+
row.dataset.mkVoiceRow = '';
|
|
64
|
+
row.setAttribute('aria-hidden', 'true');
|
|
65
|
+
const dots = Array.from({ length: count }, (_, i) => {
|
|
66
|
+
const dot = document.createElement('span');
|
|
67
|
+
dot.dataset.mkVoiceDot = String(i + 1);
|
|
68
|
+
row.append(dot);
|
|
69
|
+
return dot;
|
|
70
|
+
});
|
|
71
|
+
const status = document.createElement('span');
|
|
72
|
+
status.className = 'mk-sr-only';
|
|
73
|
+
status.setAttribute('role', 'status');
|
|
74
|
+
root.append(row, status);
|
|
75
|
+
scope.add(() => {
|
|
76
|
+
row.remove();
|
|
77
|
+
status.remove();
|
|
78
|
+
});
|
|
79
|
+
scope.attr(root, 'data-mk-ready', '');
|
|
80
|
+
scope.attr(root, 'data-mk-state', state);
|
|
81
|
+
if (options.colorful) scope.attr(root, 'data-mk-colorful', '');
|
|
82
|
+
root.style.setProperty('--mk-voice-size', `${size}px`);
|
|
83
|
+
scope.add(() => {
|
|
84
|
+
root.style.removeProperty('--mk-voice-size');
|
|
85
|
+
if (!root.getAttribute('style')) root.removeAttribute('style');
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
// Per dot: what is drawn (height, x, y) and the level it follows.
|
|
89
|
+
const drawn = dots.map(() => ({ h: size, x: 0, y: 0, vh: 0 }));
|
|
90
|
+
const levels = dots.map(() => 0);
|
|
91
|
+
let time = 0;
|
|
92
|
+
const gap = size * 0.9;
|
|
93
|
+
const home = (i: number) => (i - (count - 1) / 2) * (size + gap);
|
|
94
|
+
|
|
95
|
+
const draw = () => {
|
|
96
|
+
dots.forEach((dot, i) => {
|
|
97
|
+
const d = drawn[i]!;
|
|
98
|
+
dot.style.height = `${d.h.toFixed(2)}px`;
|
|
99
|
+
dot.style.transform = `translate(${d.x.toFixed(2)}px, ${d.y.toFixed(2)}px)`;
|
|
100
|
+
});
|
|
101
|
+
};
|
|
102
|
+
const still = () => {
|
|
103
|
+
dots.forEach((_, i) => Object.assign(drawn[i]!, { h: size, x: 0, y: 0, vh: 0 }));
|
|
104
|
+
draw();
|
|
105
|
+
};
|
|
106
|
+
|
|
107
|
+
let analyser: AnalyserNode | null = null;
|
|
108
|
+
let bins: Uint8Array<ArrayBuffer> | null = null;
|
|
109
|
+
let release: (() => void) | null = null;
|
|
110
|
+
let listening = 0;
|
|
111
|
+
/** The voice's bands (about 120 Hz – 3 kHz) into one level per dot. */
|
|
112
|
+
const sample = () => {
|
|
113
|
+
if (!analyser || !bins) return;
|
|
114
|
+
analyser.getByteFrequencyData(bins);
|
|
115
|
+
const hz = (audio?.sampleRate ?? 48000) / 2 / bins.length;
|
|
116
|
+
const low = Math.max(1, Math.floor(120 / hz));
|
|
117
|
+
const high = Math.min(bins.length - 1, Math.ceil(3000 / hz));
|
|
118
|
+
const span = (high - low) / count;
|
|
119
|
+
for (let i = 0; i < count; i++) {
|
|
120
|
+
let sum = 0;
|
|
121
|
+
const from = Math.floor(low + span * i);
|
|
122
|
+
const to = Math.max(from + 1, Math.floor(low + span * (i + 1)));
|
|
123
|
+
for (let k = from; k < to; k++) sum += bins[k]!;
|
|
124
|
+
// Higher bands are quieter in a voice: they get a lift.
|
|
125
|
+
levels[i] = Math.min(1, (sum / (to - from) / 255) * (1.4 + i * 0.35));
|
|
126
|
+
}
|
|
127
|
+
};
|
|
128
|
+
|
|
129
|
+
const tick = (dt: number) => {
|
|
130
|
+
if (reduced()) return;
|
|
131
|
+
time += dt / 60;
|
|
132
|
+
sample();
|
|
133
|
+
const k = 1 - Math.exp(-dt * 0.35);
|
|
134
|
+
dots.forEach((_, i) => {
|
|
135
|
+
const d = drawn[i]!;
|
|
136
|
+
let h = size;
|
|
137
|
+
let x = 0;
|
|
138
|
+
let y = 0;
|
|
139
|
+
if (state === 'idle') {
|
|
140
|
+
h = size * (1 + 0.18 * Math.max(0, Math.sin(time * 3.2 - i * 0.7)));
|
|
141
|
+
y = -2 * Math.max(0, Math.sin(time * 3.2 - i * 0.7));
|
|
142
|
+
} else if (state === 'listening') {
|
|
143
|
+
h = size + levels[i]! * size * 3.2;
|
|
144
|
+
} else if (state === 'thinking') {
|
|
145
|
+
// A ring around the middle, turning.
|
|
146
|
+
const angle = time * 4 + (i / count) * Math.PI * 2;
|
|
147
|
+
const ring = size * 1.2;
|
|
148
|
+
x = Math.cos(angle) * ring - home(i);
|
|
149
|
+
y = Math.sin(angle) * ring;
|
|
150
|
+
} else {
|
|
151
|
+
const wave = 0.5 + 0.5 * Math.sin(time * 9 - i * 0.9);
|
|
152
|
+
const voice = analyser ? levels[i]! : 0.55;
|
|
153
|
+
h = size + wave * voice * size * 2.4;
|
|
154
|
+
}
|
|
155
|
+
// Height on a spring that rises fast and falls slower (a bounce per syllable); position eases.
|
|
156
|
+
const stiffness = h > d.h ? 0.5 : 0.18;
|
|
157
|
+
d.vh = (d.vh + (h - d.h) * stiffness) * 0.6;
|
|
158
|
+
d.h = Math.max(size * 0.8, d.h + d.vh * dt);
|
|
159
|
+
d.x += (x - d.x) * k;
|
|
160
|
+
d.y += (y - d.y) * k;
|
|
161
|
+
});
|
|
162
|
+
draw();
|
|
163
|
+
};
|
|
164
|
+
const stopLoop = whileVisible(root, tick);
|
|
165
|
+
scope.add(stopLoop);
|
|
166
|
+
|
|
167
|
+
const stop = () => {
|
|
168
|
+
listening++;
|
|
169
|
+
release?.();
|
|
170
|
+
release = null;
|
|
171
|
+
analyser = null;
|
|
172
|
+
bins = null;
|
|
173
|
+
levels.fill(0);
|
|
174
|
+
};
|
|
175
|
+
scope.add(stop);
|
|
176
|
+
|
|
177
|
+
const setState = (next: VoiceState) => {
|
|
178
|
+
if (!STATES.includes(next)) return;
|
|
179
|
+
state = next;
|
|
180
|
+
root.setAttribute('data-mk-state', next);
|
|
181
|
+
status.textContent = next === 'idle' ? '' : labels[next];
|
|
182
|
+
if (reduced()) still();
|
|
183
|
+
root.dispatchEvent(new CustomEvent('mk:state', { bubbles: true, detail: { state: next } }));
|
|
184
|
+
};
|
|
185
|
+
|
|
186
|
+
draw();
|
|
187
|
+
if (state !== 'idle') status.textContent = labels[state];
|
|
188
|
+
|
|
189
|
+
return {
|
|
190
|
+
setState,
|
|
191
|
+
get state() {
|
|
192
|
+
return state;
|
|
193
|
+
},
|
|
194
|
+
setLevel(level) {
|
|
195
|
+
const list = Array.isArray(level) ? level : dots.map(() => level);
|
|
196
|
+
dots.forEach((_, i) => {
|
|
197
|
+
const value = Number(list[i % Math.max(1, list.length)]);
|
|
198
|
+
levels[i] = Number.isFinite(value) ? Math.min(1, Math.max(0, value)) : 0;
|
|
199
|
+
});
|
|
200
|
+
},
|
|
201
|
+
async listen(source) {
|
|
202
|
+
stop();
|
|
203
|
+
const request = listening;
|
|
204
|
+
const Context = globalThis.AudioContext ?? (globalThis as unknown as { webkitAudioContext?: typeof AudioContext }).webkitAudioContext;
|
|
205
|
+
if (!Context || scope.disposed) return () => {};
|
|
206
|
+
audio ??= new Context();
|
|
207
|
+
if (audio.state === 'suspended') await audio.resume().catch(() => {});
|
|
208
|
+
if (scope.disposed || request !== listening) return () => {};
|
|
209
|
+
const node = audio.createAnalyser();
|
|
210
|
+
node.fftSize = 256;
|
|
211
|
+
node.smoothingTimeConstant = 0.55;
|
|
212
|
+
let input: AudioNode;
|
|
213
|
+
if (source instanceof HTMLMediaElement) {
|
|
214
|
+
const existing = mediaSources.get(source) ?? audio.createMediaElementSource(source);
|
|
215
|
+
mediaSources.set(source, existing);
|
|
216
|
+
// Followed before: its direct line to the speakers goes, the analyser's takes over.
|
|
217
|
+
existing.disconnect();
|
|
218
|
+
input = existing;
|
|
219
|
+
// A playing element must still be heard.
|
|
220
|
+
node.connect(audio.destination);
|
|
221
|
+
} else input = audio.createMediaStreamSource(source);
|
|
222
|
+
input.connect(node);
|
|
223
|
+
analyser = node;
|
|
224
|
+
bins = new Uint8Array(new ArrayBuffer(node.frequencyBinCount));
|
|
225
|
+
const mine = () => {
|
|
226
|
+
try {
|
|
227
|
+
input.disconnect(node);
|
|
228
|
+
node.disconnect();
|
|
229
|
+
} catch {
|
|
230
|
+
// Already disconnected.
|
|
231
|
+
}
|
|
232
|
+
// The element keeps playing through the speakers.
|
|
233
|
+
if (source instanceof HTMLMediaElement && audio) input.connect(audio.destination);
|
|
234
|
+
};
|
|
235
|
+
release = mine;
|
|
236
|
+
return () => {
|
|
237
|
+
if (release === mine) stop();
|
|
238
|
+
};
|
|
239
|
+
},
|
|
240
|
+
stop,
|
|
241
|
+
destroy: () => scope.dispose(),
|
|
242
|
+
};
|
|
243
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
import { useRef, type HTMLAttributes, type MutableRefObject } from 'react';
|
|
3
|
+
import { useIsomorphicLayoutEffect } from '../../react/use-recipe';
|
|
4
|
+
import { createVoiceDots, type VoiceDotsInstance, type VoiceDotsOptions } from './engine';
|
|
5
|
+
|
|
6
|
+
export interface VoiceDotsProps extends HTMLAttributes<HTMLDivElement>, VoiceDotsOptions {
|
|
7
|
+
/** A microphone stream or a playing <audio>/<video> to follow. */
|
|
8
|
+
source?: MediaStream | HTMLMediaElement | null;
|
|
9
|
+
/** The voice's level, 0…1 (one or one per dot), when the app measures it. */
|
|
10
|
+
level?: number | number[];
|
|
11
|
+
instanceRef?: MutableRefObject<VoiceDotsInstance | null>;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/** <VoiceDots state={listening ? 'listening' : 'idle'} source={micStream} /> */
|
|
15
|
+
export function VoiceDots({ state = 'idle', count, colorful, size, labels, source, level, instanceRef, className, ...props }: VoiceDotsProps) {
|
|
16
|
+
const ref = useRef<HTMLDivElement>(null);
|
|
17
|
+
const instance = useRef<VoiceDotsInstance | null>(null);
|
|
18
|
+
useIsomorphicLayoutEffect(() => {
|
|
19
|
+
const created = createVoiceDots(ref.current, { state, count, colorful, size, labels });
|
|
20
|
+
instance.current = created;
|
|
21
|
+
if (instanceRef) instanceRef.current = created;
|
|
22
|
+
return () => {
|
|
23
|
+
created.destroy();
|
|
24
|
+
instance.current = null;
|
|
25
|
+
if (instanceRef) instanceRef.current = null;
|
|
26
|
+
};
|
|
27
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
28
|
+
}, [count, colorful, size]);
|
|
29
|
+
useIsomorphicLayoutEffect(() => {
|
|
30
|
+
if (instance.current && instance.current.state !== state) instance.current.setState(state);
|
|
31
|
+
}, [state]);
|
|
32
|
+
useIsomorphicLayoutEffect(() => {
|
|
33
|
+
const dots = instance.current;
|
|
34
|
+
if (!dots || !source) return;
|
|
35
|
+
let stop: (() => void) | null = null;
|
|
36
|
+
let gone = false;
|
|
37
|
+
void dots.listen(source).then((done) => {
|
|
38
|
+
if (gone) done();
|
|
39
|
+
else stop = done;
|
|
40
|
+
}).catch((error: unknown) => {
|
|
41
|
+
if (!gone) ref.current?.dispatchEvent(new CustomEvent('mk:error', { bubbles: true, detail: { error } }));
|
|
42
|
+
});
|
|
43
|
+
return () => {
|
|
44
|
+
gone = true;
|
|
45
|
+
stop?.();
|
|
46
|
+
};
|
|
47
|
+
}, [source, count, colorful, size]);
|
|
48
|
+
useIsomorphicLayoutEffect(() => {
|
|
49
|
+
if (level !== undefined) instance.current?.setLevel(level);
|
|
50
|
+
}, [level]);
|
|
51
|
+
return <div {...props} ref={ref} className={['mk-voice-dots', className].filter(Boolean).join(' ')} />;
|
|
52
|
+
}
|