@yewdiel21/motion-kit 0.9.1 → 0.14.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +72 -0
- package/dist/auto/index.d.ts +72 -3
- package/dist/auto/index.js +139 -3
- package/dist/auto/index.js.map +1 -1
- package/dist/{chunk-BFC27F2G.js → chunk-6P4P7NU6.js} +2 -218
- package/dist/chunk-6P4P7NU6.js.map +1 -0
- package/dist/chunk-CU6SXW3E.js +20334 -0
- package/dist/chunk-CU6SXW3E.js.map +1 -0
- package/dist/{chunk-XDWBVFHQ.js → chunk-EY4BJKC4.js} +1203 -29
- package/dist/chunk-EY4BJKC4.js.map +1 -0
- package/dist/chunk-QP4QJOEN.js +72 -0
- package/dist/chunk-QP4QJOEN.js.map +1 -0
- package/dist/css/components/activity-feed.css +240 -0
- package/dist/css/components/attachment.css +266 -0
- package/dist/css/components/breadcrumb.css +110 -0
- package/dist/css/components/calendar.css +159 -0
- package/dist/css/components/chart-tooltip.css +168 -0
- package/dist/css/components/chart.css +296 -0
- package/dist/css/components/code-block.css +264 -0
- package/dist/css/components/color-area.css +82 -0
- package/dist/css/components/color-field.css +67 -0
- package/dist/css/components/color-picker.css +131 -0
- package/dist/css/components/color-slider.css +113 -0
- package/dist/css/components/command-search.css +252 -0
- package/dist/css/components/context-menu.css +23 -0
- package/dist/css/components/credit-card.css +288 -0
- package/dist/css/components/data-table.css +591 -0
- package/dist/css/components/date-range-picker.css +168 -0
- package/dist/css/components/date-time-picker.css +178 -0
- package/dist/css/components/drawer.css +58 -0
- package/dist/css/components/editable-table.css +201 -0
- package/dist/css/components/empty-state.css +61 -0
- package/dist/css/components/file-explorer.css +633 -0
- package/dist/css/components/filter-builder.css +232 -0
- package/dist/css/components/hold-confirm.css +87 -0
- package/dist/css/components/hover-card.css +20 -0
- package/dist/css/components/image-preview.css +195 -0
- package/dist/css/components/input-otp.css +10 -0
- package/dist/css/components/json-viewer.css +272 -0
- package/dist/css/components/like-button.css +78 -0
- package/dist/css/components/link-preview.css +275 -0
- package/dist/css/components/loading-button.css +77 -0
- package/dist/css/components/multi-select.css +267 -0
- package/dist/css/components/notification-bell.css +127 -0
- package/dist/css/components/pagination.css +11 -0
- package/dist/css/components/password-strength.css +183 -0
- package/dist/css/components/popover.css +25 -0
- package/dist/css/components/progress.css +140 -0
- package/dist/css/components/quantity.css +16 -0
- package/dist/css/components/questionnaire.css +217 -0
- package/dist/css/components/range-calendar.css +186 -0
- package/dist/css/components/range-slider.css +192 -0
- package/dist/css/components/resizable-panel.css +243 -0
- package/dist/css/components/rich-text-editor.css +284 -0
- package/dist/css/components/scratch-card.css +75 -0
- package/dist/css/components/skeleton.css +108 -0
- package/dist/css/components/slot-picker.css +126 -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/studio-dock.css +35 -0
- package/dist/css/components/swipe-actions.css +105 -0
- package/dist/css/components/tag-input.css +244 -0
- package/dist/css/components/time-field.css +88 -0
- package/dist/css/components/timeline.css +219 -0
- package/dist/css/components/tooltip.css +79 -0
- package/dist/css/components/trailer.css +37 -0
- package/dist/css/components/tree-view.css +210 -0
- package/dist/css/components/virtual-list.css +195 -0
- package/dist/css/controls.css +65 -0
- package/dist/css/tokens.css +185 -181
- package/dist/elements/index.d.ts +742 -5
- package/dist/elements/index.js +1527 -8
- package/dist/elements/index.js.map +1 -1
- package/dist/{engine-GDGwQnJE.d.ts → engine-Cn6IhE3z.d.ts} +1 -1
- package/dist/engine-QECQnGz1.d.ts +33 -0
- package/dist/index.css +10113 -181
- package/dist/index.d.ts +89 -5
- package/dist/index.js +285 -9
- package/dist/index.js.map +1 -1
- package/dist/react/index.d.ts +678 -23
- package/dist/react/index.js +2389 -7
- package/dist/react/index.js.map +1 -1
- package/dist/{reduced-B98f8LPs.d.ts → reduced-BBUusld6.d.ts} +206 -2
- package/dist/reveal-DEvp6U6T.d.ts +2381 -0
- package/dist/{store-pHiUvxP8.d.ts → store-DjkTBX7f.d.ts} +2 -2
- package/package.json +1 -1
- package/registry.json +583 -104
- package/skills/motion-kit/SKILL.md +209 -189
- package/src/auto/auto-init.ts +136 -0
- package/src/components/activity-feed/README.md +56 -0
- package/src/components/activity-feed/activity-feed.css +239 -0
- package/src/components/activity-feed/element.ts +36 -0
- package/src/components/activity-feed/engine.ts +351 -0
- package/src/components/activity-feed/react.tsx +32 -0
- package/src/components/attachment/README.md +27 -0
- package/src/components/attachment/attachment.css +265 -0
- package/src/components/attachment/element.ts +50 -0
- package/src/components/attachment/engine.ts +469 -0
- package/src/components/attachment/react.tsx +65 -0
- package/src/components/breadcrumb/README.md +38 -0
- package/src/components/breadcrumb/breadcrumb.css +109 -0
- package/src/components/breadcrumb/element.ts +27 -0
- package/src/components/breadcrumb/engine.ts +249 -0
- package/src/components/breadcrumb/react.tsx +30 -0
- package/src/components/calendar/README.md +82 -0
- package/src/components/calendar/calendar.css +158 -0
- package/src/components/calendar/element.ts +27 -0
- package/src/components/calendar/engine.ts +412 -0
- package/src/components/calendar/react.tsx +69 -0
- package/src/components/chart/README.md +24 -0
- package/src/components/chart/chart.css +295 -0
- package/src/components/chart/element.ts +41 -0
- package/src/components/chart/engine.ts +811 -0
- package/src/components/chart/react.tsx +50 -0
- package/src/components/chart-tooltip/README.md +53 -0
- package/src/components/chart-tooltip/chart-tooltip.css +167 -0
- package/src/components/chart-tooltip/element.ts +37 -0
- package/src/components/chart-tooltip/engine.ts +442 -0
- package/src/components/chart-tooltip/react.tsx +47 -0
- package/src/components/code-block/README.md +19 -0
- package/src/components/code-block/code-block.css +263 -0
- package/src/components/code-block/element.ts +34 -0
- package/src/components/code-block/engine.ts +254 -0
- package/src/components/code-block/highlight.ts +244 -0
- package/src/components/code-block/react.tsx +94 -0
- package/src/components/color-area/README.md +50 -0
- package/src/components/color-area/color-area.css +81 -0
- package/src/components/color-area/element.ts +26 -0
- package/src/components/color-area/engine.ts +246 -0
- package/src/components/color-area/react.tsx +42 -0
- package/src/components/color-field/README.md +43 -0
- package/src/components/color-field/color-field.css +66 -0
- package/src/components/color-field/element.ts +26 -0
- package/src/components/color-field/engine.ts +174 -0
- package/src/components/color-field/react.tsx +42 -0
- package/src/components/color-picker/README.md +45 -0
- package/src/components/color-picker/color-picker.css +130 -0
- package/src/components/color-picker/element.ts +45 -0
- package/src/components/color-picker/engine.ts +314 -0
- package/src/components/color-picker/react.tsx +47 -0
- package/src/components/color-slider/README.md +55 -0
- package/src/components/color-slider/color-slider.css +112 -0
- package/src/components/color-slider/element.ts +26 -0
- package/src/components/color-slider/engine.ts +234 -0
- package/src/components/color-slider/react.tsx +45 -0
- package/src/components/command-search/README.md +15 -0
- package/src/components/command-search/command-search.css +251 -0
- package/src/components/command-search/element.ts +36 -0
- package/src/components/command-search/engine.ts +356 -0
- package/src/components/command-search/match.ts +53 -0
- package/src/components/command-search/react.tsx +66 -0
- package/src/components/context-menu/README.md +31 -0
- package/src/components/context-menu/context-menu.css +22 -0
- package/src/components/context-menu/element.ts +40 -0
- package/src/components/context-menu/engine.ts +153 -0
- package/src/components/context-menu/react.tsx +57 -0
- package/src/components/credit-card/README.md +20 -0
- package/src/components/credit-card/card.ts +77 -0
- package/src/components/credit-card/credit-card.css +287 -0
- package/src/components/credit-card/element.ts +33 -0
- package/src/components/credit-card/engine.ts +410 -0
- package/src/components/credit-card/react.tsx +36 -0
- package/src/components/data-table/README.md +83 -0
- package/src/components/data-table/data-table.css +590 -0
- package/src/components/data-table/element.ts +54 -0
- package/src/components/data-table/engine.ts +1059 -0
- package/src/components/data-table/react.tsx +94 -0
- package/src/components/date-range-picker/README.md +51 -0
- package/src/components/date-range-picker/date-range-picker.css +167 -0
- package/src/components/date-range-picker/element.ts +52 -0
- package/src/components/date-range-picker/engine.ts +352 -0
- package/src/components/date-range-picker/react.tsx +57 -0
- package/src/components/date-time-picker/README.md +48 -0
- package/src/components/date-time-picker/date-time-picker.css +177 -0
- package/src/components/date-time-picker/element.ts +47 -0
- package/src/components/date-time-picker/engine.ts +350 -0
- package/src/components/date-time-picker/react.tsx +54 -0
- package/src/components/dialog/README.md +103 -102
- package/src/components/dialog/element.ts +123 -118
- package/src/components/dialog/engine.ts +449 -432
- package/src/components/drawer/README.md +28 -0
- package/src/components/drawer/drawer.css +57 -0
- package/src/components/drawer/element.ts +21 -0
- package/src/components/drawer/engine.ts +56 -0
- package/src/components/drawer/react.tsx +85 -0
- package/src/components/editable-table/README.md +67 -0
- package/src/components/editable-table/editable-table.css +200 -0
- package/src/components/editable-table/element.ts +45 -0
- package/src/components/editable-table/engine.ts +775 -0
- package/src/components/editable-table/react.tsx +79 -0
- package/src/components/empty-state/README.md +41 -0
- package/src/components/empty-state/element.ts +23 -0
- package/src/components/empty-state/empty-state.css +60 -0
- package/src/components/empty-state/engine.ts +232 -0
- package/src/components/empty-state/react.tsx +34 -0
- package/src/components/file-explorer/README.md +67 -0
- package/src/components/file-explorer/element.ts +39 -0
- package/src/components/file-explorer/engine.ts +1307 -0
- package/src/components/file-explorer/file-explorer.css +632 -0
- package/src/components/file-explorer/react.tsx +77 -0
- package/src/components/filter-builder/README.md +47 -0
- package/src/components/filter-builder/element.ts +34 -0
- package/src/components/filter-builder/engine.ts +365 -0
- package/src/components/filter-builder/filter-builder.css +231 -0
- package/src/components/filter-builder/react.tsx +36 -0
- package/src/components/hold-confirm/README.md +13 -0
- package/src/components/hold-confirm/element.ts +24 -0
- package/src/components/hold-confirm/engine.ts +216 -0
- package/src/components/hold-confirm/hold-confirm.css +86 -0
- package/src/components/hold-confirm/react.tsx +28 -0
- package/src/components/hover-card/README.md +20 -0
- package/src/components/hover-card/element.ts +8 -0
- package/src/components/hover-card/engine.ts +131 -0
- package/src/components/hover-card/hover-card.css +19 -0
- package/src/components/hover-card/react.tsx +19 -0
- package/src/components/image-preview/README.md +16 -0
- package/src/components/image-preview/element.ts +26 -0
- package/src/components/image-preview/engine.ts +325 -0
- package/src/components/image-preview/image-preview.css +194 -0
- package/src/components/image-preview/react.tsx +43 -0
- package/src/components/input-otp/README.md +18 -0
- package/src/components/input-otp/element.ts +14 -0
- package/src/components/input-otp/engine.ts +106 -0
- package/src/components/input-otp/input-otp.css +9 -0
- package/src/components/input-otp/react.tsx +21 -0
- package/src/components/json-viewer/README.md +56 -0
- package/src/components/json-viewer/element.ts +37 -0
- package/src/components/json-viewer/engine.ts +440 -0
- package/src/components/json-viewer/json-viewer.css +271 -0
- package/src/components/json-viewer/react.tsx +56 -0
- package/src/components/like-button/README.md +13 -0
- package/src/components/like-button/element.ts +47 -0
- package/src/components/like-button/engine.ts +145 -0
- package/src/components/like-button/like-button.css +77 -0
- package/src/components/like-button/react.tsx +39 -0
- package/src/components/link-preview/README.md +19 -0
- package/src/components/link-preview/element.ts +29 -0
- package/src/components/link-preview/engine.ts +192 -0
- package/src/components/link-preview/link-preview.css +274 -0
- package/src/components/link-preview/react.tsx +101 -0
- package/src/components/loading-button/README.md +45 -0
- package/src/components/loading-button/element.ts +34 -0
- package/src/components/loading-button/engine.ts +201 -0
- package/src/components/loading-button/loading-button.css +76 -0
- package/src/components/loading-button/react.tsx +43 -0
- package/src/components/multi-select/README.md +48 -0
- package/src/components/multi-select/element.ts +28 -0
- package/src/components/multi-select/engine.ts +294 -0
- package/src/components/multi-select/multi-select.css +266 -0
- package/src/components/multi-select/react.tsx +39 -0
- package/src/components/notification-bell/README.md +14 -0
- package/src/components/notification-bell/element.ts +48 -0
- package/src/components/notification-bell/engine.ts +202 -0
- package/src/components/notification-bell/notification-bell.css +126 -0
- package/src/components/notification-bell/react.tsx +57 -0
- package/src/components/pagination/README.md +17 -0
- package/src/components/pagination/element.ts +14 -0
- package/src/components/pagination/engine.ts +102 -0
- package/src/components/pagination/pagination.css +10 -0
- package/src/components/pagination/react.tsx +22 -0
- package/src/components/password-strength/README.md +17 -0
- package/src/components/password-strength/element.ts +22 -0
- package/src/components/password-strength/engine.ts +212 -0
- package/src/components/password-strength/password-strength.css +182 -0
- package/src/components/password-strength/react.tsx +36 -0
- package/src/components/password-strength/score.ts +62 -0
- package/src/components/popover/README.md +44 -0
- package/src/components/popover/element.ts +31 -0
- package/src/components/popover/engine.ts +114 -0
- package/src/components/popover/popover.css +24 -0
- package/src/components/popover/react.tsx +50 -0
- package/src/components/progress/README.md +17 -0
- package/src/components/progress/element.ts +30 -0
- package/src/components/progress/engine.ts +173 -0
- package/src/components/progress/progress.css +139 -0
- package/src/components/progress/react.tsx +35 -0
- package/src/components/quantity/README.md +20 -0
- package/src/components/quantity/element.ts +14 -0
- package/src/components/quantity/engine.ts +85 -0
- package/src/components/quantity/quantity.css +15 -0
- package/src/components/quantity/react.tsx +21 -0
- package/src/components/questionnaire/README.md +32 -0
- package/src/components/questionnaire/element.ts +118 -0
- package/src/components/questionnaire/engine.ts +255 -0
- package/src/components/questionnaire/questionnaire.css +216 -0
- package/src/components/questionnaire/react.tsx +140 -0
- package/src/components/range-calendar/README.md +58 -0
- package/src/components/range-calendar/element.ts +42 -0
- package/src/components/range-calendar/engine.ts +390 -0
- package/src/components/range-calendar/range-calendar.css +185 -0
- package/src/components/range-calendar/react.tsx +56 -0
- package/src/components/range-slider/README.md +23 -0
- package/src/components/range-slider/element.ts +53 -0
- package/src/components/range-slider/engine.ts +316 -0
- package/src/components/range-slider/range-slider.css +191 -0
- package/src/components/range-slider/react.tsx +66 -0
- package/src/components/resizable-panel/README.md +61 -0
- package/src/components/resizable-panel/element.ts +36 -0
- package/src/components/resizable-panel/engine.ts +551 -0
- package/src/components/resizable-panel/react.tsx +74 -0
- package/src/components/resizable-panel/resizable-panel.css +242 -0
- package/src/components/rich-text-editor/README.md +57 -0
- package/src/components/rich-text-editor/element.ts +34 -0
- package/src/components/rich-text-editor/engine.ts +673 -0
- package/src/components/rich-text-editor/react.tsx +60 -0
- package/src/components/rich-text-editor/rich-text-editor.css +283 -0
- package/src/components/scratch-card/README.md +15 -0
- package/src/components/scratch-card/element.ts +30 -0
- package/src/components/scratch-card/engine.ts +257 -0
- package/src/components/scratch-card/react.tsx +40 -0
- package/src/components/scratch-card/scratch-card.css +74 -0
- package/src/components/skeleton/README.md +15 -0
- package/src/components/skeleton/element.ts +32 -0
- package/src/components/skeleton/engine.ts +104 -0
- package/src/components/skeleton/react.tsx +41 -0
- package/src/components/skeleton/skeleton.css +107 -0
- package/src/components/slot-picker/README.md +68 -0
- package/src/components/slot-picker/element.ts +27 -0
- package/src/components/slot-picker/engine.ts +412 -0
- package/src/components/slot-picker/react.tsx +76 -0
- package/src/components/slot-picker/slot-picker.css +125 -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/studio-dock/README.md +44 -0
- package/src/components/studio-dock/element.ts +11 -0
- package/src/components/studio-dock/engine.ts +212 -0
- package/src/components/studio-dock/react.tsx +23 -0
- package/src/components/studio-dock/studio-dock.css +34 -0
- package/src/components/swipe-actions/README.md +19 -0
- package/src/components/swipe-actions/element.ts +26 -0
- package/src/components/swipe-actions/engine.ts +260 -0
- package/src/components/swipe-actions/react.tsx +80 -0
- package/src/components/swipe-actions/swipe-actions.css +104 -0
- package/src/components/tag-input/README.md +42 -0
- package/src/components/tag-input/element.ts +39 -0
- package/src/components/tag-input/engine.ts +313 -0
- package/src/components/tag-input/react.tsx +54 -0
- package/src/components/tag-input/tag-input.css +243 -0
- package/src/components/time-field/README.md +54 -0
- package/src/components/time-field/element.ts +38 -0
- package/src/components/time-field/engine.ts +110 -0
- package/src/components/time-field/react.tsx +45 -0
- package/src/components/time-field/time-field.css +87 -0
- package/src/components/timeline/README.md +57 -0
- package/src/components/timeline/element.ts +31 -0
- package/src/components/timeline/engine.ts +300 -0
- package/src/components/timeline/react.tsx +59 -0
- package/src/components/timeline/timeline.css +218 -0
- package/src/components/tooltip/README.md +46 -0
- package/src/components/tooltip/element.ts +22 -0
- package/src/components/tooltip/engine.ts +288 -0
- package/src/components/tooltip/react.tsx +30 -0
- package/src/components/tooltip/tooltip.css +78 -0
- package/src/components/trailer/README.md +28 -0
- package/src/components/trailer/element.ts +13 -0
- package/src/components/trailer/engine.ts +189 -0
- package/src/components/trailer/react.tsx +16 -0
- package/src/components/trailer/trailer.css +36 -0
- package/src/components/tree-view/README.md +61 -0
- package/src/components/tree-view/element.ts +32 -0
- package/src/components/tree-view/engine.ts +569 -0
- package/src/components/tree-view/react.tsx +78 -0
- package/src/components/tree-view/tree-view.css +209 -0
- package/src/components/virtual-list/README.md +81 -0
- package/src/components/virtual-list/element.ts +42 -0
- package/src/components/virtual-list/engine.ts +678 -0
- package/src/components/virtual-list/react.tsx +101 -0
- package/src/components/virtual-list/virtual-list.css +194 -0
- package/src/core/chips.ts +196 -0
- package/src/core/color.ts +304 -0
- package/src/core/date-range.ts +67 -0
- package/src/core/dates.ts +108 -0
- 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/controls/README.md +14 -0
- package/src/css/controls/status.css +63 -0
- package/src/css/tokens.css +186 -182
- package/src/elements/index.ts +171 -0
- package/src/index.ts +97 -0
- package/src/motion/micro.ts +248 -0
- package/src/motion/sheet.ts +88 -65
- package/src/motion/surface.ts +9 -0
- package/src/react/index.ts +78 -0
- package/src/react/use-action-button.ts +6 -0
- package/src/react/use-editorial-reveal.ts +7 -0
- package/src/react/use-film-hero.ts +6 -0
- package/src/react/use-font-cycle.ts +7 -0
- package/src/react/use-image-zoom.ts +5 -0
- package/src/react/use-joined-cta.ts +5 -0
- package/src/react/use-local-clock.ts +5 -0
- package/src/react/use-scroll-dock.ts +5 -0
- package/src/react/use-sticker.ts +5 -0
- package/src/react/use-studio-dock.ts +8 -0
- package/src/react/use-thumbnail-nav.ts +7 -0
- package/src/react/use-trailer.ts +6 -0
- package/src/react/use-weight-hover.ts +5 -0
- package/src/recipes/action-button/README.md +31 -0
- package/src/recipes/action-button/action-button.css +113 -0
- package/src/recipes/action-button/create-action-button.ts +56 -0
- package/src/recipes/editorial-reveal/README.md +35 -0
- package/src/recipes/editorial-reveal/create-editorial-reveal.ts +91 -0
- package/src/recipes/editorial-reveal/editorial-reveal.css +17 -0
- package/src/recipes/film-hero/README.md +23 -0
- package/src/recipes/film-hero/create-film-hero.ts +65 -0
- package/src/recipes/film-hero/film-hero.css +29 -0
- package/src/recipes/font-cycle/README.md +11 -0
- package/src/recipes/font-cycle/create-font-cycle.ts +126 -0
- package/src/recipes/font-cycle/font-cycle.css +36 -0
- package/src/recipes/image-zoom/README.md +16 -0
- package/src/recipes/image-zoom/create-image-zoom.ts +27 -0
- package/src/recipes/image-zoom/image-zoom.css +7 -0
- package/src/recipes/joined-cta/README.md +13 -0
- package/src/recipes/joined-cta/create-joined-cta.ts +22 -0
- package/src/recipes/joined-cta/joined-cta.css +17 -0
- package/src/recipes/local-clock/README.md +31 -0
- package/src/recipes/local-clock/create-local-clock.ts +61 -0
- package/src/recipes/scroll-dock/README.md +46 -0
- package/src/recipes/scroll-dock/create-scroll-dock.ts +72 -0
- package/src/recipes/scroll-dock/scroll-dock.css +70 -0
- package/src/recipes/sticker/README.md +59 -0
- package/src/recipes/sticker/create-sticker.ts +490 -0
- package/src/recipes/sticker/sticker.css +112 -0
- package/src/recipes/thumbnail-nav/README.md +16 -0
- package/src/recipes/thumbnail-nav/create-thumbnail-nav.ts +165 -0
- package/src/recipes/thumbnail-nav/thumbnail-nav.css +108 -0
- package/src/recipes/weight-hover/README.md +37 -0
- package/src/recipes/weight-hover/create-weight-hover.ts +136 -0
- package/src/recipes/weight-hover/weight-hover.css +7 -0
- package/dist/chunk-BFC27F2G.js.map +0 -1
- package/dist/chunk-J6OCFNGU.js +0 -177
- package/dist/chunk-J6OCFNGU.js.map +0 -1
- package/dist/chunk-JQMJQSXD.js +0 -2374
- package/dist/chunk-JQMJQSXD.js.map +0 -1
- package/dist/chunk-XDWBVFHQ.js.map +0 -1
- package/dist/reveal-C0hCDcB_.d.ts +0 -415
|
@@ -0,0 +1,288 @@
|
|
|
1
|
+
import { gsap, EASE, registerMotion } from '../../core/gsap';
|
|
2
|
+
import { requestFrame } from '../../core/frame';
|
|
3
|
+
import { reduced } from '../../core/reduced';
|
|
4
|
+
import { createScope } from '../../core/scope';
|
|
5
|
+
import { inert, type Instance } from '../../core/types';
|
|
6
|
+
import { originOf, place, type Placement } from '../../core/position';
|
|
7
|
+
|
|
8
|
+
/*
|
|
9
|
+
* Tooltip: a short description beside its trigger, shown when the pointer
|
|
10
|
+
* rests on it (400 ms), when keyboard focus reaches it (at once) or on a long
|
|
11
|
+
* press (500 ms, touch). It takes the preferred side, flips when that side has
|
|
12
|
+
* no room, stays inside the viewport and points an arrow at the trigger's
|
|
13
|
+
* centre; it lives in the top layer (popover API) so no overflow clips it.
|
|
14
|
+
* Motion (Melon Mind): it pops from the arrow (scale .85, 4 px toward the
|
|
15
|
+
* trigger, blur 4 → 0, .22 s, pop curve) and fades out in .12 s; moving to
|
|
16
|
+
* another tooltip within .4 s shows it at once with a plain fade (warm), and
|
|
17
|
+
* new text cross-fades. Escape hides it (WCAG 1.4.13); with `interactive` the
|
|
18
|
+
* pointer can move onto it. The trigger is described by it (aria-describedby).
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
export interface TooltipOptions {
|
|
22
|
+
/** Text of the bubble (default: the trigger's title, which is removed so the browser's own does not show). */
|
|
23
|
+
content?: string;
|
|
24
|
+
/** Preferred side (default 'top'); 'auto' takes the roomiest. */
|
|
25
|
+
placement?: Placement;
|
|
26
|
+
/** Hover delay in ms (default 400). Focus shows it at once. */
|
|
27
|
+
openDelay?: number;
|
|
28
|
+
/** Delay before hiding when the pointer leaves, in ms (default 100). */
|
|
29
|
+
closeDelay?: number;
|
|
30
|
+
/** The pointer can move onto the bubble (links, selectable text). */
|
|
31
|
+
interactive?: boolean;
|
|
32
|
+
/** Arrow pointing at the trigger (default true). */
|
|
33
|
+
arrow?: boolean;
|
|
34
|
+
onOpenChange?: (open: boolean) => void;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export interface TooltipInstance extends Instance {
|
|
38
|
+
open(): void;
|
|
39
|
+
close(): void;
|
|
40
|
+
/** New text; an open tooltip cross-fades to it. Empty text hides it. */
|
|
41
|
+
setContent(text: string): void;
|
|
42
|
+
readonly isOpen: boolean;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const FOCUSABLE = 'a[href], button, input, select, textarea, summary, [tabindex]:not([tabindex="-1"]), [contenteditable="true"]';
|
|
46
|
+
/** A tooltip shown within this long after another one hid skips the delay and the pop. */
|
|
47
|
+
const WARM = 400;
|
|
48
|
+
let sequence = 0;
|
|
49
|
+
let current: { hide(instant: boolean): void } | null = null;
|
|
50
|
+
let lastHidden = -Infinity;
|
|
51
|
+
const now = () => (typeof performance !== 'undefined' ? performance.now() : Date.now());
|
|
52
|
+
|
|
53
|
+
export function createTooltip(root: HTMLElement | null, options: TooltipOptions = {}): TooltipInstance {
|
|
54
|
+
if (!root) return inert({ open() {}, close() {}, setContent() {}, isOpen: false });
|
|
55
|
+
registerMotion();
|
|
56
|
+
const scope = createScope();
|
|
57
|
+
const trigger = root.querySelector<HTMLElement>('[data-mk-tooltip-trigger]') ?? (root.matches(FOCUSABLE) ? root : root.querySelector<HTMLElement>(FOCUSABLE)) ?? root;
|
|
58
|
+
const title = trigger.getAttribute('title');
|
|
59
|
+
if (title !== null) scope.attr(trigger, 'title', null);
|
|
60
|
+
let text = options.content ?? title ?? '';
|
|
61
|
+
// Keyboard users must be able to reach what the tooltip describes.
|
|
62
|
+
if (!trigger.matches(FOCUSABLE)) scope.attr(trigger, 'tabindex', '0');
|
|
63
|
+
const openDelay = Number.isFinite(options.openDelay) ? Math.max(0, options.openDelay!) : 400;
|
|
64
|
+
const closeDelay = Number.isFinite(options.closeDelay) ? Math.max(0, options.closeDelay!) : 100;
|
|
65
|
+
const placement = options.placement ?? 'top';
|
|
66
|
+
const withArrow = options.arrow !== false;
|
|
67
|
+
|
|
68
|
+
const bubble = document.createElement('div');
|
|
69
|
+
bubble.dataset.mkTooltipBubble = '';
|
|
70
|
+
bubble.setAttribute('role', 'tooltip');
|
|
71
|
+
bubble.id = `mk-tooltip-${++sequence}`;
|
|
72
|
+
if (options.interactive) bubble.dataset.mkInteractive = '';
|
|
73
|
+
const label = document.createElement('span');
|
|
74
|
+
label.dataset.mkTooltipText = '';
|
|
75
|
+
label.textContent = text;
|
|
76
|
+
bubble.append(label);
|
|
77
|
+
if (withArrow) {
|
|
78
|
+
const arrow = document.createElement('span');
|
|
79
|
+
arrow.dataset.mkTooltipArrow = '';
|
|
80
|
+
arrow.setAttribute('aria-hidden', 'true');
|
|
81
|
+
bubble.append(arrow);
|
|
82
|
+
}
|
|
83
|
+
// The top layer (popover API) escapes every overflow; without it the bubble hangs from <body>.
|
|
84
|
+
const topLayer = typeof bubble.showPopover === 'function';
|
|
85
|
+
if (topLayer) {
|
|
86
|
+
bubble.setAttribute('popover', 'manual');
|
|
87
|
+
root.append(bubble);
|
|
88
|
+
} else {
|
|
89
|
+
bubble.hidden = true;
|
|
90
|
+
document.body.append(bubble);
|
|
91
|
+
}
|
|
92
|
+
scope.add(() => bubble.remove());
|
|
93
|
+
scope.attr(trigger, 'aria-describedby', [trigger.getAttribute('aria-describedby'), bubble.id].filter(Boolean).join(' '));
|
|
94
|
+
scope.attr(root, 'data-mk-open', null);
|
|
95
|
+
scope.attr(root, 'data-mk-ready', '');
|
|
96
|
+
|
|
97
|
+
const effects = new Set<gsap.core.Animation>();
|
|
98
|
+
const keep = (animation: gsap.core.Animation | null) => {
|
|
99
|
+
if (!animation) return;
|
|
100
|
+
effects.add(animation);
|
|
101
|
+
// Finished ones leave as new ones arrive: the animation's own onComplete stays untouched.
|
|
102
|
+
for (const effect of effects) if (effect.totalProgress() === 1) effects.delete(effect);
|
|
103
|
+
};
|
|
104
|
+
let motion: gsap.core.Animation | null = null;
|
|
105
|
+
scope.add(() => {
|
|
106
|
+
motion?.kill();
|
|
107
|
+
for (const effect of effects) effect.kill();
|
|
108
|
+
effects.clear();
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
let isOpen = false;
|
|
112
|
+
let timer: ReturnType<typeof setTimeout> | undefined;
|
|
113
|
+
const cancel = () => {
|
|
114
|
+
clearTimeout(timer);
|
|
115
|
+
timer = undefined;
|
|
116
|
+
};
|
|
117
|
+
const later = (fn: () => void, ms: number) => {
|
|
118
|
+
cancel();
|
|
119
|
+
timer = setTimeout(fn, ms);
|
|
120
|
+
};
|
|
121
|
+
scope.add(cancel);
|
|
122
|
+
|
|
123
|
+
const shown = () => (topLayer ? bubble.matches(':popover-open') : !bubble.hidden);
|
|
124
|
+
const reveal = () => {
|
|
125
|
+
if (shown()) return;
|
|
126
|
+
if (topLayer) bubble.showPopover();
|
|
127
|
+
else bubble.hidden = false;
|
|
128
|
+
};
|
|
129
|
+
const conceal = () => {
|
|
130
|
+
if (!shown()) return;
|
|
131
|
+
if (topLayer) bubble.hidePopover();
|
|
132
|
+
else bubble.hidden = true;
|
|
133
|
+
};
|
|
134
|
+
|
|
135
|
+
const position = () => {
|
|
136
|
+
const anchor = trigger.getBoundingClientRect();
|
|
137
|
+
const box = bubble.getBoundingClientRect();
|
|
138
|
+
const placed = place(anchor, { width: box.width, height: box.height }, { placement, gap: withArrow ? 10 : 6 });
|
|
139
|
+
bubble.style.left = `${placed.x}px`;
|
|
140
|
+
bubble.style.top = `${placed.y}px`;
|
|
141
|
+
bubble.dataset.mkSide = placed.side;
|
|
142
|
+
bubble.style.setProperty('--mk-tooltip-arrow', `${placed.arrow}px`);
|
|
143
|
+
return placed;
|
|
144
|
+
};
|
|
145
|
+
let pending: (() => void) | null = null;
|
|
146
|
+
const follow = () => {
|
|
147
|
+
if (!isOpen || pending) return;
|
|
148
|
+
pending = requestFrame(() => {
|
|
149
|
+
pending = null;
|
|
150
|
+
if (isOpen) position();
|
|
151
|
+
});
|
|
152
|
+
};
|
|
153
|
+
scope.add(() => pending?.());
|
|
154
|
+
|
|
155
|
+
const announce = (open: boolean) => {
|
|
156
|
+
if (open) root.setAttribute('data-mk-open', '');
|
|
157
|
+
else root.removeAttribute('data-mk-open');
|
|
158
|
+
options.onOpenChange?.(open);
|
|
159
|
+
root.dispatchEvent(new CustomEvent('mk:open-change', { bubbles: true, detail: { open } }));
|
|
160
|
+
};
|
|
161
|
+
|
|
162
|
+
const controller = {
|
|
163
|
+
hide: (instant: boolean) => hide(instant),
|
|
164
|
+
};
|
|
165
|
+
|
|
166
|
+
const show = () => {
|
|
167
|
+
cancel();
|
|
168
|
+
if (isOpen || scope.disposed || !text) return;
|
|
169
|
+
const warm = current !== null || now() - lastHidden < WARM;
|
|
170
|
+
if (current && current !== controller) current.hide(true);
|
|
171
|
+
current = controller;
|
|
172
|
+
isOpen = true;
|
|
173
|
+
motion?.kill();
|
|
174
|
+
reveal();
|
|
175
|
+
const placed = position();
|
|
176
|
+
if (!reduced()) {
|
|
177
|
+
if (warm) motion = gsap.fromTo(bubble, { opacity: 0 }, { opacity: 1, duration: 0.1, ease: 'power1.out', clearProps: 'opacity' });
|
|
178
|
+
else {
|
|
179
|
+
const toward = 4;
|
|
180
|
+
const offset = placed.side === 'top' ? { x: 0, y: toward } : placed.side === 'bottom' ? { x: 0, y: -toward } : placed.side === 'left' ? { x: toward, y: 0 } : { x: -toward, y: 0 };
|
|
181
|
+
gsap.set(bubble, { transformOrigin: originOf(placed) });
|
|
182
|
+
motion = gsap.fromTo(
|
|
183
|
+
bubble,
|
|
184
|
+
{ opacity: 0, scale: 0.85, filter: 'blur(4px)', ...offset },
|
|
185
|
+
{ opacity: 1, scale: 1, filter: 'blur(0px)', x: 0, y: 0, duration: 0.22, ease: EASE.pop, clearProps: 'opacity,scale,filter,transform,transformOrigin' },
|
|
186
|
+
);
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
announce(true);
|
|
190
|
+
};
|
|
191
|
+
|
|
192
|
+
const hide = (instant = false) => {
|
|
193
|
+
cancel();
|
|
194
|
+
if (!isOpen) return;
|
|
195
|
+
isOpen = false;
|
|
196
|
+
if (current === controller) current = null;
|
|
197
|
+
lastHidden = now();
|
|
198
|
+
motion?.kill();
|
|
199
|
+
const finish = () => {
|
|
200
|
+
conceal();
|
|
201
|
+
gsap.set(bubble, { clearProps: 'opacity,scale,filter,transform,transformOrigin' });
|
|
202
|
+
};
|
|
203
|
+
if (instant || reduced()) finish();
|
|
204
|
+
else motion = gsap.to(bubble, { opacity: 0, scale: 0.96, duration: 0.12, ease: 'power2.in', onComplete: finish });
|
|
205
|
+
announce(false);
|
|
206
|
+
};
|
|
207
|
+
scope.add(() => {
|
|
208
|
+
if (current === controller) current = null;
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
/** The last input: focus after a click is not keyboard focus. */
|
|
212
|
+
let pointerInput = false;
|
|
213
|
+
scope.on(document, 'pointerdown', () => (pointerInput = true), true);
|
|
214
|
+
scope.on(document, 'keydown', () => (pointerInput = false), true);
|
|
215
|
+
|
|
216
|
+
scope.on(trigger, 'pointerenter', (event) => {
|
|
217
|
+
if ((event as PointerEvent).pointerType === 'touch') return;
|
|
218
|
+
later(show, current || now() - lastHidden < WARM ? 0 : openDelay);
|
|
219
|
+
});
|
|
220
|
+
scope.on(trigger, 'pointerleave', (event) => {
|
|
221
|
+
if ((event as PointerEvent).pointerType === 'touch') return;
|
|
222
|
+
later(() => hide(), closeDelay);
|
|
223
|
+
});
|
|
224
|
+
// Touch: a long press shows it for a moment.
|
|
225
|
+
scope.on(trigger, 'pointerdown', (event) => {
|
|
226
|
+
if ((event as PointerEvent).pointerType === 'touch') later(show, 500);
|
|
227
|
+
});
|
|
228
|
+
for (const type of ['pointerup', 'pointercancel']) {
|
|
229
|
+
scope.on(trigger, type, (event) => {
|
|
230
|
+
if ((event as PointerEvent).pointerType !== 'touch') return;
|
|
231
|
+
if (isOpen) later(() => hide(), 1500);
|
|
232
|
+
else cancel();
|
|
233
|
+
});
|
|
234
|
+
}
|
|
235
|
+
if (options.interactive) {
|
|
236
|
+
scope.on(bubble, 'pointerenter', cancel);
|
|
237
|
+
scope.on(bubble, 'pointerleave', () => later(() => hide(), closeDelay));
|
|
238
|
+
}
|
|
239
|
+
scope.on(trigger, 'focusin', () => {
|
|
240
|
+
if (!pointerInput) show();
|
|
241
|
+
});
|
|
242
|
+
scope.on(trigger, 'focusout', (event) => {
|
|
243
|
+
if (options.interactive && bubble.contains((event as FocusEvent).relatedTarget as Node | null)) return;
|
|
244
|
+
hide();
|
|
245
|
+
});
|
|
246
|
+
scope.on(document, 'keydown', (event) => {
|
|
247
|
+
if ((event as KeyboardEvent).key === 'Escape' && isOpen) hide();
|
|
248
|
+
});
|
|
249
|
+
scope.on(window, 'resize', follow);
|
|
250
|
+
scope.on(window, 'scroll', follow, { capture: true, passive: true });
|
|
251
|
+
scope.add(() => {
|
|
252
|
+
if (isOpen) conceal();
|
|
253
|
+
});
|
|
254
|
+
|
|
255
|
+
return {
|
|
256
|
+
open: show,
|
|
257
|
+
close: () => hide(),
|
|
258
|
+
setContent(next) {
|
|
259
|
+
if (scope.disposed) return;
|
|
260
|
+
text = next;
|
|
261
|
+
if (!text) {
|
|
262
|
+
label.textContent = '';
|
|
263
|
+
hide();
|
|
264
|
+
return;
|
|
265
|
+
}
|
|
266
|
+
if (!isOpen || reduced() || label.textContent === text) {
|
|
267
|
+
label.textContent = text;
|
|
268
|
+
if (isOpen) position();
|
|
269
|
+
return;
|
|
270
|
+
}
|
|
271
|
+
gsap.killTweensOf(label);
|
|
272
|
+
keep(
|
|
273
|
+
gsap
|
|
274
|
+
.timeline()
|
|
275
|
+
.to(label, { opacity: 0, filter: 'blur(3px)', duration: 0.1, ease: 'power1.in' })
|
|
276
|
+
.call(() => {
|
|
277
|
+
label.textContent = text;
|
|
278
|
+
if (isOpen) position();
|
|
279
|
+
})
|
|
280
|
+
.to(label, { opacity: 1, filter: 'blur(0px)', duration: 0.16, ease: 'power1.out', clearProps: 'opacity,filter' }),
|
|
281
|
+
);
|
|
282
|
+
},
|
|
283
|
+
get isOpen() {
|
|
284
|
+
return isOpen;
|
|
285
|
+
},
|
|
286
|
+
destroy: () => scope.dispose(),
|
|
287
|
+
};
|
|
288
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
import { useRef, type HTMLAttributes } from 'react';
|
|
3
|
+
import { useIsomorphicLayoutEffect, useLatest } from '../../react/use-recipe';
|
|
4
|
+
import { createTooltip, type TooltipInstance, type TooltipOptions } from './engine';
|
|
5
|
+
|
|
6
|
+
export interface TooltipProps extends HTMLAttributes<HTMLSpanElement>, TooltipOptions {}
|
|
7
|
+
|
|
8
|
+
/** <Tooltip content="Copiar enlace"><button>…</button></Tooltip>: the first focusable child is the trigger. */
|
|
9
|
+
export function Tooltip({ content, placement, openDelay, closeDelay, interactive, arrow, onOpenChange, className, children, ...props }: TooltipProps) {
|
|
10
|
+
const ref = useRef<HTMLSpanElement>(null);
|
|
11
|
+
const instance = useRef<TooltipInstance | null>(null);
|
|
12
|
+
const change = useLatest(onOpenChange);
|
|
13
|
+
useIsomorphicLayoutEffect(() => {
|
|
14
|
+
const created = createTooltip(ref.current, { content, placement, openDelay, closeDelay, interactive, arrow, onOpenChange: (open) => change.current?.(open) });
|
|
15
|
+
instance.current = created;
|
|
16
|
+
return () => {
|
|
17
|
+
created.destroy();
|
|
18
|
+
instance.current = null;
|
|
19
|
+
};
|
|
20
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
21
|
+
}, [placement, openDelay, closeDelay, interactive, arrow]);
|
|
22
|
+
useIsomorphicLayoutEffect(() => {
|
|
23
|
+
instance.current?.setContent(content ?? '');
|
|
24
|
+
}, [content]);
|
|
25
|
+
return (
|
|
26
|
+
<span {...props} ref={ref} className={['mk-tooltip', className].filter(Boolean).join(' ')}>
|
|
27
|
+
{children}
|
|
28
|
+
</span>
|
|
29
|
+
);
|
|
30
|
+
}
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
/* tooltip — Melon Mind's inverse glass in a compact 8 px bubble: .8125rem text, at most 18rem wide, a 12 × 6 px arrow
|
|
2
|
+
that points at the trigger's centre. It lives in the top layer, so the reset below undoes the popover UA styles. */
|
|
3
|
+
.mk-tooltip,
|
|
4
|
+
mk-tooltip {
|
|
5
|
+
display: inline-flex;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
[data-mk-tooltip-bubble] {
|
|
9
|
+
--mk-tooltip-bg: var(--mk-glass-inverse, rgb(24 24 24 / 0.9));
|
|
10
|
+
position: fixed;
|
|
11
|
+
inset: auto;
|
|
12
|
+
z-index: 2147483000;
|
|
13
|
+
box-sizing: border-box;
|
|
14
|
+
width: max-content;
|
|
15
|
+
max-width: min(18rem, calc(100vw - 16px));
|
|
16
|
+
margin: 0;
|
|
17
|
+
padding: 0.4rem 0.65rem;
|
|
18
|
+
border: 0;
|
|
19
|
+
border-radius: 8px;
|
|
20
|
+
overflow: visible;
|
|
21
|
+
background: var(--mk-tooltip-bg);
|
|
22
|
+
color: var(--mk-on-glass-inverse, #fff);
|
|
23
|
+
box-shadow: 0 8px 24px -10px color-mix(in srgb, var(--mk-shadow-color, #000) 45%, transparent);
|
|
24
|
+
font: 500 0.8125rem/1.35 var(--mk-font, system-ui, sans-serif);
|
|
25
|
+
text-align: start;
|
|
26
|
+
overflow-wrap: anywhere;
|
|
27
|
+
pointer-events: none;
|
|
28
|
+
-webkit-backdrop-filter: blur(12px);
|
|
29
|
+
backdrop-filter: blur(12px);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
[data-mk-tooltip-bubble][data-mk-interactive] {
|
|
33
|
+
pointer-events: auto;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
[data-mk-tooltip-bubble][hidden] {
|
|
37
|
+
display: none;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/* A triangle outside the bubble's edge (no overlap, so the translucent glass is not doubled). */
|
|
41
|
+
[data-mk-tooltip-arrow] {
|
|
42
|
+
position: absolute;
|
|
43
|
+
background: var(--mk-tooltip-bg);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
[data-mk-tooltip-bubble][data-mk-side='top'] > [data-mk-tooltip-arrow],
|
|
47
|
+
[data-mk-tooltip-bubble][data-mk-side='bottom'] > [data-mk-tooltip-arrow] {
|
|
48
|
+
left: calc(var(--mk-tooltip-arrow, 50%) - 6px);
|
|
49
|
+
width: 12px;
|
|
50
|
+
height: 6px;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
[data-mk-tooltip-bubble][data-mk-side='top'] > [data-mk-tooltip-arrow] {
|
|
54
|
+
top: 100%;
|
|
55
|
+
clip-path: polygon(0 0, 100% 0, 50% 100%);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
[data-mk-tooltip-bubble][data-mk-side='bottom'] > [data-mk-tooltip-arrow] {
|
|
59
|
+
bottom: 100%;
|
|
60
|
+
clip-path: polygon(50% 0, 100% 100%, 0 100%);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
[data-mk-tooltip-bubble][data-mk-side='left'] > [data-mk-tooltip-arrow],
|
|
64
|
+
[data-mk-tooltip-bubble][data-mk-side='right'] > [data-mk-tooltip-arrow] {
|
|
65
|
+
top: calc(var(--mk-tooltip-arrow, 50%) - 6px);
|
|
66
|
+
width: 6px;
|
|
67
|
+
height: 12px;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
[data-mk-tooltip-bubble][data-mk-side='left'] > [data-mk-tooltip-arrow] {
|
|
71
|
+
left: 100%;
|
|
72
|
+
clip-path: polygon(0 0, 100% 50%, 0 100%);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
[data-mk-tooltip-bubble][data-mk-side='right'] > [data-mk-tooltip-arrow] {
|
|
76
|
+
right: 100%;
|
|
77
|
+
clip-path: polygon(100% 0, 100% 100%, 0 50%);
|
|
78
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
# trailer
|
|
2
|
+
|
|
3
|
+
Modal de video que compone el motor `dialog`: top layer, bloqueo del fondo, Escape y retorno de foco. Medios escritos por el consumidor; no inyecta YouTube, rastreadores ni videos remotos.
|
|
4
|
+
|
|
5
|
+
```html
|
|
6
|
+
<button data-mk-open="preview">Ver trailer</button>
|
|
7
|
+
<dialog class="mk-trailer" id="preview">
|
|
8
|
+
<header class="mk-trailer__header">
|
|
9
|
+
<h2 data-mk-part="title">Trailer</h2>
|
|
10
|
+
<button data-mk-part="close">Cerrar</button>
|
|
11
|
+
</header>
|
|
12
|
+
<video controls playsinline preload="metadata" src="/trailer.webm">
|
|
13
|
+
<track kind="captions" src="/captions.vtt" srclang="es" label="Español" />
|
|
14
|
+
</video>
|
|
15
|
+
</dialog>
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
`createTrailer(dialog, { triggers:true, autoplay:false, closeOnBackdrop:true, onOpen, onClose })` devuelve `open(origin?)`, `close()`, `toggle(origin?)`, `play():Promise<boolean>`, `pause()`, `seek(seconds)`, `setVolume(0..1)`, `isOpen`, `destroy()`.
|
|
19
|
+
|
|
20
|
+
React: `<Trailer id="preview" options={…}>…</Trailer>` o `useTrailer(ref, options, deps?)` sobre un `<dialog>`. Custom element: `<mk-trailer><dialog id="preview">…</dialog></mk-trailer>`, `defineTrailer()`. La estructura permanece en light DOM. Los callbacks/options de los hooks se leen al crear; usar `deps` cuando cambien.
|
|
21
|
+
|
|
22
|
+
Controles opcionales: botones `data-mk-trailer-play`, `mute`, `fullscreen`, `captions`; inputs range `data-mk-trailer-seek` y `volume`; output `data-mk-trailer-time`; párrafo `data-mk-trailer-status`. Sólo oculta los controles nativos si existen Play/Mute/Seek y, cuando hay tracks, un botón CC. `aria-label`, `aria-pressed`, duración, posición y mensajes de error se sincronizan. Los sliders conservan teclado nativo; fuera de ellos Space/K alterna, flechas buscan±5s y M cambia mute. Los subtítulos usan el primer track. Pantalla completa depende del soporte del navegador y de un gesto del usuario.
|
|
23
|
+
|
|
24
|
+
Autoplay está desactivado. Cerrar o destruir pausa el video y cancela solicitudes de play pendientes. `play()` absorbe rechazos del navegador y anuncia el error. `destroy()` devuelve los controles/atributos originales y retira listeners. La fuente puede fallar sin bloquear el cierre.
|
|
25
|
+
|
|
26
|
+
Pantalla completa se solicita sobre `[data-mk-trailer-stage]` (un wrapper normal alrededor de video y controles), o sobre el video si falta ese wrapper. Nunca sobre `<dialog>`, que la API no admite. Cerrar/destruir sale de la pantalla completa que pertenece a esta instancia.
|
|
27
|
+
|
|
28
|
+
El modal usa el morph accesible existente de motion-kit. **No copia** la apertura por bandas ni la disolución procedural de A24; el modelo de interacción y los controles se reconstruyeron para ser reutilizables. La demo usa SVG, captions y WebM silencioso originales de 6 s, sin assets de las referencias.
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { MkElement, defineElement, type OptionType } from '../../elements/base';
|
|
2
|
+
import { createTrailer, type TrailerInstance, type TrailerOptions } from './engine';
|
|
3
|
+
|
|
4
|
+
/** Wrap a consumer-authored native <dialog> containing <video>. */
|
|
5
|
+
export class MkTrailerElement extends MkElement<TrailerInstance> {
|
|
6
|
+
static options: Record<string, OptionType> = { autoplay: 'boolean', triggers: 'string', closeOnBackdrop: 'boolean' };
|
|
7
|
+
protected create(options: Record<string, unknown>) {
|
|
8
|
+
return createTrailer(this.querySelector('dialog'), options as TrailerOptions);
|
|
9
|
+
}
|
|
10
|
+
open(origin?: HTMLElement | null) { this.instance?.open(origin); }
|
|
11
|
+
close() { this.instance?.close(); }
|
|
12
|
+
}
|
|
13
|
+
export const defineTrailer = () => defineElement('mk-trailer', MkTrailerElement);
|
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
import { createScope } from '../../core/scope';
|
|
2
|
+
import { inert, type Instance } from '../../core/types';
|
|
3
|
+
import { createDialog } from '../dialog/engine';
|
|
4
|
+
|
|
5
|
+
export interface TrailerOptions {
|
|
6
|
+
triggers?: boolean | string;
|
|
7
|
+
autoplay?: boolean;
|
|
8
|
+
closeOnBackdrop?: boolean;
|
|
9
|
+
onOpen?: () => void;
|
|
10
|
+
onClose?: () => void;
|
|
11
|
+
}
|
|
12
|
+
export interface TrailerInstance extends Instance {
|
|
13
|
+
open(origin?: HTMLElement | null): void;
|
|
14
|
+
close(): void;
|
|
15
|
+
toggle(origin?: HTMLElement | null): void;
|
|
16
|
+
play(): Promise<boolean>;
|
|
17
|
+
pause(): void;
|
|
18
|
+
seek(seconds: number): void;
|
|
19
|
+
setVolume(volume: number): void;
|
|
20
|
+
readonly isOpen: boolean;
|
|
21
|
+
}
|
|
22
|
+
const time = (value: number) => {
|
|
23
|
+
const seconds = Number.isFinite(value) ? Math.max(0, Math.floor(value)) : 0;
|
|
24
|
+
return `${Math.floor(seconds / 60).toString().padStart(2, '0')}:${(seconds % 60).toString().padStart(2, '0')}`;
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
/** Native modal + consumer-owned video, with optional accessible custom transport controls. */
|
|
28
|
+
export function createTrailer(root: HTMLElement | null, options: TrailerOptions = {}): TrailerInstance {
|
|
29
|
+
const video = root?.querySelector<HTMLVideoElement>('video');
|
|
30
|
+
if (!root || !video || root.tagName !== 'DIALOG') return inert({ open() {}, close() {}, toggle() {}, play: async () => false, pause() {}, seek() {}, setVolume() {}, isOpen: false });
|
|
31
|
+
const scope = createScope();
|
|
32
|
+
const get = <T extends HTMLElement>(name: string) => root.querySelector<T>(`[data-mk-trailer-${name}]`);
|
|
33
|
+
const playButton = get<HTMLButtonElement>('play');
|
|
34
|
+
const muteButton = get<HTMLButtonElement>('mute');
|
|
35
|
+
const fullButton = get<HTMLButtonElement>('fullscreen');
|
|
36
|
+
// The Fullscreen API rejects HTMLDialogElement. A consumer can wrap media and
|
|
37
|
+
// transport in a stage; otherwise fullscreen just the native video element.
|
|
38
|
+
const fullTarget = get<HTMLElement>('stage') ?? video;
|
|
39
|
+
const captionsButton = get<HTMLButtonElement>('captions');
|
|
40
|
+
const seekInput = get<HTMLInputElement>('seek');
|
|
41
|
+
const volumeInput = get<HTMLInputElement>('volume');
|
|
42
|
+
const output = get<HTMLElement>('time');
|
|
43
|
+
const status = get<HTMLElement>('status');
|
|
44
|
+
const originalText = [output, status].filter((el): el is HTMLElement => !!el).map(el => [el, el.textContent] as const);
|
|
45
|
+
scope.add(() => originalText.forEach(([el, text]) => { el.textContent = text; }));
|
|
46
|
+
scope.attr(root, 'data-mk-trailer-ready', '');
|
|
47
|
+
scope.attr(root, 'data-mk-trailer-state', 'paused');
|
|
48
|
+
const controls = video.controls;
|
|
49
|
+
// Keep the browser's controls unless the consumer supplied a complete transport.
|
|
50
|
+
if (playButton && muteButton && seekInput && (!video.querySelector('track') || captionsButton)) video.controls = false;
|
|
51
|
+
scope.add(() => { video.controls = controls; });
|
|
52
|
+
for (const input of [seekInput, volumeInput]) {
|
|
53
|
+
if (!input) continue;
|
|
54
|
+
const initialValue = input.value;
|
|
55
|
+
scope.add(() => { input.value = initialValue; });
|
|
56
|
+
scope.attr(input, 'min', '0');
|
|
57
|
+
scope.attr(input, 'max', '1');
|
|
58
|
+
scope.attr(input, 'step', input === volumeInput ? '.01' : '.1');
|
|
59
|
+
scope.attr(input, 'aria-label', input.getAttribute('aria-label') ?? (input === volumeInput ? 'Volumen' : 'Posición del video'));
|
|
60
|
+
}
|
|
61
|
+
if (status) scope.attr(status, 'role', 'status');
|
|
62
|
+
const tell = (message: string) => { if (!scope.disposed && status) status.textContent = message; };
|
|
63
|
+
const sync = () => {
|
|
64
|
+
if (scope.disposed) return;
|
|
65
|
+
const duration = Number.isFinite(video.duration) ? Math.max(0, video.duration) : 0;
|
|
66
|
+
scope.attr(root, 'data-mk-trailer-state', video.ended ? 'ended' : video.paused ? 'paused' : 'playing');
|
|
67
|
+
if (playButton) {
|
|
68
|
+
scope.attr(playButton, 'aria-label', video.paused || video.ended ? 'Reproducir' : 'Pausar');
|
|
69
|
+
scope.attr(playButton, 'aria-pressed', String(!video.paused && !video.ended));
|
|
70
|
+
}
|
|
71
|
+
if (muteButton) {
|
|
72
|
+
scope.attr(muteButton, 'aria-label', video.muted ? 'Activar sonido' : 'Silenciar');
|
|
73
|
+
scope.attr(muteButton, 'aria-pressed', String(video.muted));
|
|
74
|
+
}
|
|
75
|
+
if (seekInput) {
|
|
76
|
+
scope.attr(seekInput, 'max', String(duration || 1));
|
|
77
|
+
scope.attr(seekInput, 'disabled', duration ? null : '');
|
|
78
|
+
scope.attr(seekInput, 'aria-valuetext', `${time(video.currentTime)} de ${time(duration)}`);
|
|
79
|
+
seekInput.value = String(Number.isFinite(video.currentTime) ? video.currentTime : 0);
|
|
80
|
+
}
|
|
81
|
+
if (volumeInput) volumeInput.value = String(video.muted ? 0 : video.volume);
|
|
82
|
+
if (captionsButton) {
|
|
83
|
+
scope.attr(captionsButton, 'disabled', video.textTracks.length ? null : '');
|
|
84
|
+
scope.attr(captionsButton, 'aria-pressed', String(video.textTracks[0]?.mode === 'showing'));
|
|
85
|
+
}
|
|
86
|
+
if (output) output.textContent = `${time(video.currentTime)} / ${time(duration)}`;
|
|
87
|
+
};
|
|
88
|
+
let playRequest = 0;
|
|
89
|
+
let wantsPlayback = false;
|
|
90
|
+
const pause = () => { if (!scope.disposed) { wantsPlayback = false; ++playRequest; video.pause(); sync(); } };
|
|
91
|
+
const play = async () => {
|
|
92
|
+
if (scope.disposed) return false;
|
|
93
|
+
wantsPlayback = true;
|
|
94
|
+
const request = ++playRequest;
|
|
95
|
+
try {
|
|
96
|
+
await video.play();
|
|
97
|
+
if (scope.disposed || request !== playRequest) {
|
|
98
|
+
// A stale request must not pause a newer play after a quick close/reopen.
|
|
99
|
+
if (scope.disposed || !wantsPlayback) video.pause();
|
|
100
|
+
return false;
|
|
101
|
+
}
|
|
102
|
+
tell('');
|
|
103
|
+
sync();
|
|
104
|
+
return true;
|
|
105
|
+
} catch {
|
|
106
|
+
if (scope.disposed || request !== playRequest) return false;
|
|
107
|
+
wantsPlayback = false;
|
|
108
|
+
tell('No se pudo reproducir el video. Revisa la fuente y vuelve a intentarlo.');
|
|
109
|
+
sync();
|
|
110
|
+
return false;
|
|
111
|
+
}
|
|
112
|
+
};
|
|
113
|
+
const seek = (seconds: number) => {
|
|
114
|
+
if (scope.disposed || !Number.isFinite(seconds) || !Number.isFinite(video.duration) || video.duration <= 0) return;
|
|
115
|
+
video.currentTime = Math.max(0, Math.min(video.duration, seconds));
|
|
116
|
+
sync();
|
|
117
|
+
};
|
|
118
|
+
const setVolume = (volume: number) => {
|
|
119
|
+
if (scope.disposed || !Number.isFinite(volume)) return;
|
|
120
|
+
video.volume = Math.max(0, Math.min(1, volume));
|
|
121
|
+
video.muted = volume <= 0;
|
|
122
|
+
sync();
|
|
123
|
+
};
|
|
124
|
+
const exitFullscreen = () => {
|
|
125
|
+
if (document.fullscreenElement !== fullTarget) return;
|
|
126
|
+
void document.exitFullscreen?.().catch(() => {});
|
|
127
|
+
};
|
|
128
|
+
const dialog = createDialog(root, {
|
|
129
|
+
triggers: options.triggers,
|
|
130
|
+
closeOnBackdrop: options.closeOnBackdrop ?? true,
|
|
131
|
+
drag: false,
|
|
132
|
+
autoResize: false,
|
|
133
|
+
animateContent: false,
|
|
134
|
+
onOpen: () => { sync(); if (options.autoplay) void play(); options.onOpen?.(); },
|
|
135
|
+
onClose: () => { ++playRequest; pause(); exitFullscreen(); options.onClose?.(); },
|
|
136
|
+
});
|
|
137
|
+
if (playButton) scope.on(playButton, 'click', () => video.paused || video.ended ? void play() : pause());
|
|
138
|
+
if (muteButton) scope.on(muteButton, 'click', () => { video.muted = !video.muted; sync(); });
|
|
139
|
+
if (seekInput) scope.on(seekInput, 'input', () => seek(Number(seekInput.value)));
|
|
140
|
+
if (volumeInput) scope.on(volumeInput, 'input', () => setVolume(Number(volumeInput.value)));
|
|
141
|
+
if (captionsButton) scope.on(captionsButton, 'click', () => {
|
|
142
|
+
const track = video.textTracks[0];
|
|
143
|
+
if (track) track.mode = track.mode === 'showing' ? 'hidden' : 'showing';
|
|
144
|
+
sync();
|
|
145
|
+
});
|
|
146
|
+
const fullscreen = async () => {
|
|
147
|
+
if (scope.disposed) return;
|
|
148
|
+
try {
|
|
149
|
+
if (document.fullscreenElement === fullTarget) await document.exitFullscreen?.();
|
|
150
|
+
else if (fullTarget.requestFullscreen) {
|
|
151
|
+
await fullTarget.requestFullscreen();
|
|
152
|
+
if (scope.disposed || !dialog.isOpen) exitFullscreen();
|
|
153
|
+
}
|
|
154
|
+
else tell('La pantalla completa no está disponible en este navegador.');
|
|
155
|
+
} catch { tell('No se pudo abrir la pantalla completa.'); }
|
|
156
|
+
};
|
|
157
|
+
if (fullButton) {
|
|
158
|
+
scope.attr(fullButton, 'aria-label', 'Pantalla completa');
|
|
159
|
+
scope.on(fullButton, 'click', () => { void fullscreen(); });
|
|
160
|
+
scope.on(document, 'fullscreenchange', () => {
|
|
161
|
+
scope.attr(fullButton, 'aria-label', document.fullscreenElement === fullTarget ? 'Salir de pantalla completa' : 'Pantalla completa');
|
|
162
|
+
});
|
|
163
|
+
}
|
|
164
|
+
for (const event of ['loadedmetadata', 'durationchange', 'timeupdate', 'play', 'pause', 'ended', 'volumechange']) scope.on(video, event, sync);
|
|
165
|
+
scope.on(video, 'error', () => { tell('El video no está disponible.'); scope.attr(root, 'data-mk-trailer-state', 'error'); });
|
|
166
|
+
scope.on(root, 'keydown', (event) => {
|
|
167
|
+
if (!dialog.isOpen || (event.target instanceof Element && event.target.closest('button, input, select, textarea, a, [contenteditable]'))) return;
|
|
168
|
+
if (event.key === ' ' || event.key.toLowerCase() === 'k') { event.preventDefault(); video.paused ? void play() : pause(); }
|
|
169
|
+
else if (event.key === 'ArrowRight' || event.key === 'ArrowLeft') { event.preventDefault(); seek(video.currentTime + (event.key === 'ArrowRight' ? 5 : -5)); }
|
|
170
|
+
else if (event.key.toLowerCase() === 'm') { video.muted = !video.muted; sync(); }
|
|
171
|
+
});
|
|
172
|
+
sync();
|
|
173
|
+
return {
|
|
174
|
+
open: (origin) => { if (!scope.disposed) dialog.open(origin); },
|
|
175
|
+
close: () => { if (!scope.disposed) dialog.close(); },
|
|
176
|
+
toggle: (origin) => { if (!scope.disposed) dialog.toggle(origin); },
|
|
177
|
+
play, pause, seek, setVolume,
|
|
178
|
+
get isOpen() { return dialog.isOpen; },
|
|
179
|
+
destroy() {
|
|
180
|
+
if (scope.disposed) return;
|
|
181
|
+
wantsPlayback = false;
|
|
182
|
+
++playRequest;
|
|
183
|
+
video.pause();
|
|
184
|
+
exitFullscreen();
|
|
185
|
+
dialog.destroy();
|
|
186
|
+
scope.dispose();
|
|
187
|
+
},
|
|
188
|
+
};
|
|
189
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
import { useRef, type DialogHTMLAttributes } from 'react';
|
|
3
|
+
import { useRecipe } from '../../react/use-recipe';
|
|
4
|
+
import { createTrailer, type TrailerOptions } from './engine';
|
|
5
|
+
|
|
6
|
+
export interface TrailerProps extends Omit<DialogHTMLAttributes<HTMLDialogElement>, 'open' | 'onClose'> {
|
|
7
|
+
options?: TrailerOptions;
|
|
8
|
+
}
|
|
9
|
+
/** Consumer supplies <video>, optional controls and tracks; no remote media is injected. */
|
|
10
|
+
export function Trailer({ options, className, children, ...props }: TrailerProps) {
|
|
11
|
+
const ref = useRef<HTMLDialogElement>(null);
|
|
12
|
+
useRecipe(ref, createTrailer, options);
|
|
13
|
+
return <dialog {...props} ref={ref} className={['mk-trailer', className].filter(Boolean).join(' ')}>{children}</dialog>;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export { useTrailer } from '../../react/use-trailer';
|