@yewdiel21/motion-kit 0.10.0 → 0.14.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +155 -93
- package/dist/auto/index.d.ts +53 -3
- package/dist/auto/index.js +101 -3
- package/dist/auto/index.js.map +1 -1
- package/dist/{chunk-IKQ5FODN.js → chunk-6P4P7NU6.js} +2 -218
- package/dist/chunk-6P4P7NU6.js.map +1 -0
- package/dist/chunk-CU6SXW3E.js +20334 -0
- package/dist/chunk-CU6SXW3E.js.map +1 -0
- package/dist/{chunk-XAKC2QY7.js → chunk-EY4BJKC4.js} +3 -28
- package/dist/chunk-EY4BJKC4.js.map +1 -0
- package/dist/chunk-QP4QJOEN.js +72 -0
- package/dist/chunk-QP4QJOEN.js.map +1 -0
- package/dist/css/components/activity-feed.css +240 -0
- package/dist/css/components/attachment.css +266 -0
- package/dist/css/components/breadcrumb.css +110 -0
- package/dist/css/components/chart-tooltip.css +168 -0
- package/dist/css/components/chart.css +296 -0
- package/dist/css/components/code-block.css +264 -0
- package/dist/css/components/color-area.css +82 -0
- package/dist/css/components/color-field.css +67 -0
- package/dist/css/components/color-picker.css +131 -0
- package/dist/css/components/color-slider.css +113 -0
- package/dist/css/components/command-search.css +252 -0
- package/dist/css/components/context-menu.css +23 -0
- package/dist/css/components/credit-card.css +288 -0
- package/dist/css/components/data-table.css +591 -0
- package/dist/css/components/date-range-picker.css +168 -0
- package/dist/css/components/date-time-picker.css +178 -0
- package/dist/css/components/drawer.css +58 -0
- package/dist/css/components/editable-table.css +201 -0
- package/dist/css/components/empty-state.css +61 -0
- package/dist/css/components/file-explorer.css +633 -0
- package/dist/css/components/filter-builder.css +232 -0
- package/dist/css/components/hold-confirm.css +87 -0
- package/dist/css/components/hover-card.css +13 -7
- package/dist/css/components/image-preview.css +195 -0
- package/dist/css/components/json-viewer.css +272 -0
- package/dist/css/components/like-button.css +78 -0
- package/dist/css/components/link-preview.css +275 -0
- package/dist/css/components/loading-button.css +77 -0
- package/dist/css/components/multi-select.css +267 -0
- package/dist/css/components/notification-bell.css +127 -0
- package/dist/css/components/password-strength.css +183 -0
- package/dist/css/components/popover.css +25 -0
- package/dist/css/components/progress.css +140 -0
- package/dist/css/components/questionnaire.css +217 -0
- package/dist/css/components/range-calendar.css +186 -0
- package/dist/css/components/range-slider.css +192 -0
- package/dist/css/components/resizable-panel.css +243 -0
- package/dist/css/components/rich-text-editor.css +284 -0
- package/dist/css/components/scratch-card.css +75 -0
- package/dist/css/components/skeleton.css +108 -0
- package/dist/css/components/split-view.css +19 -0
- package/dist/css/components/star-rating.css +80 -0
- package/dist/css/components/stat-card.css +164 -0
- package/dist/css/components/swipe-actions.css +105 -0
- package/dist/css/components/tag-input.css +244 -0
- package/dist/css/components/time-field.css +88 -0
- package/dist/css/components/timeline.css +219 -0
- package/dist/css/components/tooltip.css +79 -0
- package/dist/css/components/tree-view.css +210 -0
- package/dist/css/components/virtual-list.css +195 -0
- package/dist/css/tokens.css +185 -181
- package/dist/elements/index.d.ts +674 -5
- package/dist/elements/index.js +1361 -8
- package/dist/elements/index.js.map +1 -1
- package/dist/{engine-GDGwQnJE.d.ts → engine-Cn6IhE3z.d.ts} +1 -1
- package/dist/engine-QECQnGz1.d.ts +33 -0
- package/dist/index.css +9104 -189
- package/dist/index.d.ts +89 -5
- package/dist/index.js +243 -9
- package/dist/index.js.map +1 -1
- package/dist/react/index.d.ts +579 -23
- package/dist/react/index.js +2042 -7
- package/dist/react/index.js.map +1 -1
- package/dist/{reduced-BmBzKH94.d.ts → reduced-BBUusld6.d.ts} +9 -2
- package/dist/reveal-DEvp6U6T.d.ts +2381 -0
- package/dist/{store-DYb3tTsZ.d.ts → store-DjkTBX7f.d.ts} +2 -2
- package/package.json +1 -1
- package/registry.json +406 -1
- package/src/auto/auto-init.ts +98 -0
- package/src/components/activity-feed/README.md +56 -0
- package/src/components/activity-feed/activity-feed.css +239 -0
- package/src/components/activity-feed/element.ts +36 -0
- package/src/components/activity-feed/engine.ts +351 -0
- package/src/components/activity-feed/react.tsx +32 -0
- package/src/components/attachment/README.md +27 -0
- package/src/components/attachment/attachment.css +265 -0
- package/src/components/attachment/element.ts +50 -0
- package/src/components/attachment/engine.ts +469 -0
- package/src/components/attachment/react.tsx +65 -0
- package/src/components/breadcrumb/README.md +38 -0
- package/src/components/breadcrumb/breadcrumb.css +109 -0
- package/src/components/breadcrumb/element.ts +27 -0
- package/src/components/breadcrumb/engine.ts +249 -0
- package/src/components/breadcrumb/react.tsx +30 -0
- package/src/components/chart/README.md +24 -0
- package/src/components/chart/chart.css +295 -0
- package/src/components/chart/element.ts +41 -0
- package/src/components/chart/engine.ts +811 -0
- package/src/components/chart/react.tsx +50 -0
- package/src/components/chart-tooltip/README.md +53 -0
- package/src/components/chart-tooltip/chart-tooltip.css +167 -0
- package/src/components/chart-tooltip/element.ts +37 -0
- package/src/components/chart-tooltip/engine.ts +442 -0
- package/src/components/chart-tooltip/react.tsx +47 -0
- package/src/components/code-block/README.md +19 -0
- package/src/components/code-block/code-block.css +263 -0
- package/src/components/code-block/element.ts +34 -0
- package/src/components/code-block/engine.ts +254 -0
- package/src/components/code-block/highlight.ts +244 -0
- package/src/components/code-block/react.tsx +94 -0
- package/src/components/color-area/README.md +50 -0
- package/src/components/color-area/color-area.css +81 -0
- package/src/components/color-area/element.ts +26 -0
- package/src/components/color-area/engine.ts +246 -0
- package/src/components/color-area/react.tsx +42 -0
- package/src/components/color-field/README.md +43 -0
- package/src/components/color-field/color-field.css +66 -0
- package/src/components/color-field/element.ts +26 -0
- package/src/components/color-field/engine.ts +174 -0
- package/src/components/color-field/react.tsx +42 -0
- package/src/components/color-picker/README.md +45 -0
- package/src/components/color-picker/color-picker.css +130 -0
- package/src/components/color-picker/element.ts +45 -0
- package/src/components/color-picker/engine.ts +314 -0
- package/src/components/color-picker/react.tsx +47 -0
- package/src/components/color-slider/README.md +55 -0
- package/src/components/color-slider/color-slider.css +112 -0
- package/src/components/color-slider/element.ts +26 -0
- package/src/components/color-slider/engine.ts +234 -0
- package/src/components/color-slider/react.tsx +45 -0
- package/src/components/command-search/README.md +15 -0
- package/src/components/command-search/command-search.css +251 -0
- package/src/components/command-search/element.ts +36 -0
- package/src/components/command-search/engine.ts +356 -0
- package/src/components/command-search/match.ts +53 -0
- package/src/components/command-search/react.tsx +66 -0
- package/src/components/context-menu/README.md +31 -0
- package/src/components/context-menu/context-menu.css +22 -0
- package/src/components/context-menu/element.ts +40 -0
- package/src/components/context-menu/engine.ts +153 -0
- package/src/components/context-menu/react.tsx +57 -0
- package/src/components/credit-card/README.md +20 -0
- package/src/components/credit-card/card.ts +77 -0
- package/src/components/credit-card/credit-card.css +287 -0
- package/src/components/credit-card/element.ts +33 -0
- package/src/components/credit-card/engine.ts +410 -0
- package/src/components/credit-card/react.tsx +36 -0
- package/src/components/data-table/README.md +83 -0
- package/src/components/data-table/data-table.css +590 -0
- package/src/components/data-table/element.ts +54 -0
- package/src/components/data-table/engine.ts +1059 -0
- package/src/components/data-table/react.tsx +94 -0
- package/src/components/date-range-picker/README.md +51 -0
- package/src/components/date-range-picker/date-range-picker.css +167 -0
- package/src/components/date-range-picker/element.ts +52 -0
- package/src/components/date-range-picker/engine.ts +352 -0
- package/src/components/date-range-picker/react.tsx +57 -0
- package/src/components/date-time-picker/README.md +48 -0
- package/src/components/date-time-picker/date-time-picker.css +177 -0
- package/src/components/date-time-picker/element.ts +47 -0
- package/src/components/date-time-picker/engine.ts +350 -0
- package/src/components/date-time-picker/react.tsx +54 -0
- package/src/components/dialog/README.md +103 -102
- package/src/components/dialog/element.ts +123 -118
- package/src/components/dialog/engine.ts +449 -432
- package/src/components/drawer/README.md +28 -0
- package/src/components/drawer/drawer.css +57 -0
- package/src/components/drawer/element.ts +21 -0
- package/src/components/drawer/engine.ts +56 -0
- package/src/components/drawer/react.tsx +85 -0
- package/src/components/editable-table/README.md +67 -0
- package/src/components/editable-table/editable-table.css +200 -0
- package/src/components/editable-table/element.ts +45 -0
- package/src/components/editable-table/engine.ts +775 -0
- package/src/components/editable-table/react.tsx +79 -0
- package/src/components/empty-state/README.md +41 -0
- package/src/components/empty-state/element.ts +23 -0
- package/src/components/empty-state/empty-state.css +60 -0
- package/src/components/empty-state/engine.ts +232 -0
- package/src/components/empty-state/react.tsx +34 -0
- package/src/components/file-explorer/README.md +67 -0
- package/src/components/file-explorer/element.ts +39 -0
- package/src/components/file-explorer/engine.ts +1307 -0
- package/src/components/file-explorer/file-explorer.css +632 -0
- package/src/components/file-explorer/react.tsx +77 -0
- package/src/components/filter-builder/README.md +47 -0
- package/src/components/filter-builder/element.ts +34 -0
- package/src/components/filter-builder/engine.ts +365 -0
- package/src/components/filter-builder/filter-builder.css +231 -0
- package/src/components/filter-builder/react.tsx +36 -0
- package/src/components/hold-confirm/README.md +13 -0
- package/src/components/hold-confirm/element.ts +24 -0
- package/src/components/hold-confirm/engine.ts +216 -0
- package/src/components/hold-confirm/hold-confirm.css +86 -0
- package/src/components/hold-confirm/react.tsx +28 -0
- package/src/components/hover-card/README.md +6 -2
- package/src/components/hover-card/engine.ts +68 -6
- package/src/components/hover-card/hover-card.css +13 -7
- package/src/components/image-preview/README.md +16 -0
- package/src/components/image-preview/element.ts +26 -0
- package/src/components/image-preview/engine.ts +325 -0
- package/src/components/image-preview/image-preview.css +194 -0
- package/src/components/image-preview/react.tsx +43 -0
- package/src/components/json-viewer/README.md +56 -0
- package/src/components/json-viewer/element.ts +37 -0
- package/src/components/json-viewer/engine.ts +440 -0
- package/src/components/json-viewer/json-viewer.css +271 -0
- package/src/components/json-viewer/react.tsx +56 -0
- package/src/components/like-button/README.md +13 -0
- package/src/components/like-button/element.ts +47 -0
- package/src/components/like-button/engine.ts +145 -0
- package/src/components/like-button/like-button.css +77 -0
- package/src/components/like-button/react.tsx +39 -0
- package/src/components/link-preview/README.md +19 -0
- package/src/components/link-preview/element.ts +29 -0
- package/src/components/link-preview/engine.ts +192 -0
- package/src/components/link-preview/link-preview.css +274 -0
- package/src/components/link-preview/react.tsx +101 -0
- package/src/components/loading-button/README.md +45 -0
- package/src/components/loading-button/element.ts +34 -0
- package/src/components/loading-button/engine.ts +201 -0
- package/src/components/loading-button/loading-button.css +76 -0
- package/src/components/loading-button/react.tsx +43 -0
- package/src/components/multi-select/README.md +48 -0
- package/src/components/multi-select/element.ts +28 -0
- package/src/components/multi-select/engine.ts +294 -0
- package/src/components/multi-select/multi-select.css +266 -0
- package/src/components/multi-select/react.tsx +39 -0
- package/src/components/notification-bell/README.md +14 -0
- package/src/components/notification-bell/element.ts +48 -0
- package/src/components/notification-bell/engine.ts +202 -0
- package/src/components/notification-bell/notification-bell.css +126 -0
- package/src/components/notification-bell/react.tsx +57 -0
- package/src/components/password-strength/README.md +17 -0
- package/src/components/password-strength/element.ts +22 -0
- package/src/components/password-strength/engine.ts +212 -0
- package/src/components/password-strength/password-strength.css +182 -0
- package/src/components/password-strength/react.tsx +36 -0
- package/src/components/password-strength/score.ts +62 -0
- package/src/components/popover/README.md +44 -0
- package/src/components/popover/element.ts +31 -0
- package/src/components/popover/engine.ts +114 -0
- package/src/components/popover/popover.css +24 -0
- package/src/components/popover/react.tsx +50 -0
- package/src/components/progress/README.md +17 -0
- package/src/components/progress/element.ts +30 -0
- package/src/components/progress/engine.ts +173 -0
- package/src/components/progress/progress.css +139 -0
- package/src/components/progress/react.tsx +35 -0
- package/src/components/questionnaire/README.md +32 -0
- package/src/components/questionnaire/element.ts +118 -0
- package/src/components/questionnaire/engine.ts +255 -0
- package/src/components/questionnaire/questionnaire.css +216 -0
- package/src/components/questionnaire/react.tsx +140 -0
- package/src/components/range-calendar/README.md +58 -0
- package/src/components/range-calendar/element.ts +42 -0
- package/src/components/range-calendar/engine.ts +390 -0
- package/src/components/range-calendar/range-calendar.css +185 -0
- package/src/components/range-calendar/react.tsx +56 -0
- package/src/components/range-slider/README.md +23 -0
- package/src/components/range-slider/element.ts +53 -0
- package/src/components/range-slider/engine.ts +316 -0
- package/src/components/range-slider/range-slider.css +191 -0
- package/src/components/range-slider/react.tsx +66 -0
- package/src/components/resizable-panel/README.md +61 -0
- package/src/components/resizable-panel/element.ts +36 -0
- package/src/components/resizable-panel/engine.ts +551 -0
- package/src/components/resizable-panel/react.tsx +74 -0
- package/src/components/resizable-panel/resizable-panel.css +242 -0
- package/src/components/rich-text-editor/README.md +57 -0
- package/src/components/rich-text-editor/element.ts +34 -0
- package/src/components/rich-text-editor/engine.ts +673 -0
- package/src/components/rich-text-editor/react.tsx +60 -0
- package/src/components/rich-text-editor/rich-text-editor.css +283 -0
- package/src/components/scratch-card/README.md +15 -0
- package/src/components/scratch-card/element.ts +30 -0
- package/src/components/scratch-card/engine.ts +257 -0
- package/src/components/scratch-card/react.tsx +40 -0
- package/src/components/scratch-card/scratch-card.css +74 -0
- package/src/components/skeleton/README.md +15 -0
- package/src/components/skeleton/element.ts +32 -0
- package/src/components/skeleton/engine.ts +104 -0
- package/src/components/skeleton/react.tsx +41 -0
- package/src/components/skeleton/skeleton.css +107 -0
- package/src/components/split-view/README.md +50 -0
- package/src/components/split-view/element.ts +34 -0
- package/src/components/split-view/engine.ts +114 -0
- package/src/components/split-view/react.tsx +63 -0
- package/src/components/split-view/split-view.css +18 -0
- package/src/components/star-rating/README.md +14 -0
- package/src/components/star-rating/element.ts +26 -0
- package/src/components/star-rating/engine.ts +217 -0
- package/src/components/star-rating/react.tsx +33 -0
- package/src/components/star-rating/star-rating.css +79 -0
- package/src/components/stat-card/README.md +42 -0
- package/src/components/stat-card/element.ts +40 -0
- package/src/components/stat-card/engine.ts +283 -0
- package/src/components/stat-card/react.tsx +36 -0
- package/src/components/stat-card/stat-card.css +163 -0
- package/src/components/swipe-actions/README.md +19 -0
- package/src/components/swipe-actions/element.ts +26 -0
- package/src/components/swipe-actions/engine.ts +260 -0
- package/src/components/swipe-actions/react.tsx +80 -0
- package/src/components/swipe-actions/swipe-actions.css +104 -0
- package/src/components/tag-input/README.md +42 -0
- package/src/components/tag-input/element.ts +39 -0
- package/src/components/tag-input/engine.ts +313 -0
- package/src/components/tag-input/react.tsx +54 -0
- package/src/components/tag-input/tag-input.css +243 -0
- package/src/components/time-field/README.md +54 -0
- package/src/components/time-field/element.ts +38 -0
- package/src/components/time-field/engine.ts +110 -0
- package/src/components/time-field/react.tsx +45 -0
- package/src/components/time-field/time-field.css +87 -0
- package/src/components/timeline/README.md +57 -0
- package/src/components/timeline/element.ts +31 -0
- package/src/components/timeline/engine.ts +300 -0
- package/src/components/timeline/react.tsx +59 -0
- package/src/components/timeline/timeline.css +218 -0
- package/src/components/tooltip/README.md +46 -0
- package/src/components/tooltip/element.ts +22 -0
- package/src/components/tooltip/engine.ts +288 -0
- package/src/components/tooltip/react.tsx +30 -0
- package/src/components/tooltip/tooltip.css +78 -0
- package/src/components/tree-view/README.md +61 -0
- package/src/components/tree-view/element.ts +32 -0
- package/src/components/tree-view/engine.ts +569 -0
- package/src/components/tree-view/react.tsx +78 -0
- package/src/components/tree-view/tree-view.css +209 -0
- package/src/components/virtual-list/README.md +81 -0
- package/src/components/virtual-list/element.ts +42 -0
- package/src/components/virtual-list/engine.ts +678 -0
- package/src/components/virtual-list/react.tsx +101 -0
- package/src/components/virtual-list/virtual-list.css +194 -0
- package/src/core/chips.ts +196 -0
- package/src/core/color.ts +304 -0
- package/src/core/date-range.ts +67 -0
- package/src/core/dates.ts +11 -1
- package/src/core/files.ts +107 -0
- package/src/core/filter.ts +229 -0
- package/src/core/gsap.ts +80 -65
- package/src/core/listbox.ts +332 -0
- package/src/core/mark.ts +29 -0
- package/src/core/position.ts +89 -0
- package/src/core/relative-time.ts +77 -0
- package/src/core/segment-field.ts +373 -0
- package/src/core/segments.ts +153 -0
- package/src/core/tree.ts +116 -0
- package/src/core/virtual.ts +79 -0
- package/src/css/tokens.css +186 -182
- package/src/elements/index.ts +147 -0
- package/src/index.ts +72 -0
- package/src/motion/micro.ts +248 -0
- package/src/motion/sheet.ts +88 -65
- package/src/motion/surface.ts +9 -0
- package/src/react/index.ts +57 -0
- package/dist/chunk-IKQ5FODN.js.map +0 -1
- package/dist/chunk-J6OCFNGU.js +0 -177
- package/dist/chunk-J6OCFNGU.js.map +0 -1
- package/dist/chunk-TFNRIZHU.js +0 -3977
- package/dist/chunk-TFNRIZHU.js.map +0 -1
- package/dist/chunk-XAKC2QY7.js.map +0 -1
- package/dist/reveal-jsX-uwUz.d.ts +0 -625
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
import { useRef, type HTMLAttributes } from 'react';
|
|
3
|
+
import { useIsomorphicLayoutEffect } from '../../react/use-recipe';
|
|
4
|
+
import { createTimeline, type TimelineEvent, type TimelineInstance, type TimelineOptions } from './engine';
|
|
5
|
+
|
|
6
|
+
export interface TimelineProps extends Omit<HTMLAttributes<HTMLDivElement>, 'children'>, Omit<TimelineOptions, 'events'> {
|
|
7
|
+
events: TimelineEvent[];
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* <Timeline events={history} group />. Changing `events` updates in place: an event added at either end grows in,
|
|
12
|
+
* one that changes state recolours its dot, one that goes folds away.
|
|
13
|
+
*/
|
|
14
|
+
export function Timeline({ events, layout, group, relative, locale, label, className, ...props }: TimelineProps) {
|
|
15
|
+
const ref = useRef<HTMLDivElement>(null);
|
|
16
|
+
const instance = useRef<TimelineInstance | null>(null);
|
|
17
|
+
const shown = useRef<TimelineEvent[]>(events);
|
|
18
|
+
useIsomorphicLayoutEffect(() => {
|
|
19
|
+
const created = createTimeline(ref.current, { events: shown.current, layout, group, relative, locale, label });
|
|
20
|
+
instance.current = created;
|
|
21
|
+
return () => {
|
|
22
|
+
created.destroy();
|
|
23
|
+
instance.current = null;
|
|
24
|
+
};
|
|
25
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
26
|
+
}, [layout, group, relative, locale, label]);
|
|
27
|
+
const key = JSON.stringify(events);
|
|
28
|
+
useIsomorphicLayoutEffect(() => {
|
|
29
|
+
const timeline = instance.current;
|
|
30
|
+
const before = shown.current;
|
|
31
|
+
shown.current = events;
|
|
32
|
+
if (!timeline || JSON.stringify(timeline.events) === key) return;
|
|
33
|
+
const ids = new Set(before.map((event) => event.id));
|
|
34
|
+
const fresh = events.filter((event) => !ids.has(event.id));
|
|
35
|
+
const kept = events.filter((event) => ids.has(event.id));
|
|
36
|
+
const gone = before.filter((old) => !events.some((event) => event.id === old.id));
|
|
37
|
+
const stayed = before.filter((old) => !gone.includes(old));
|
|
38
|
+
const sameOrder = kept.every((event, i) => stayed[i]?.id === event.id);
|
|
39
|
+
/** The events that stayed take their changes (a new state recolours a dot). */
|
|
40
|
+
const patch = () => {
|
|
41
|
+
for (const event of kept) {
|
|
42
|
+
const old = before.find((entry) => entry.id === event.id);
|
|
43
|
+
// Keys the new version dropped are cleared too.
|
|
44
|
+
if (old && JSON.stringify(old) !== JSON.stringify(event)) timeline.update(event.id, { ...Object.fromEntries(Object.keys(old).map((name) => [name, undefined])), ...event });
|
|
45
|
+
}
|
|
46
|
+
};
|
|
47
|
+
const first = events[0]?.id;
|
|
48
|
+
const last = events[events.length - 1]?.id;
|
|
49
|
+
// One event added at an end grows in; one taken out folds away; anything else is redrawn.
|
|
50
|
+
if (sameOrder && !gone.length && fresh.length === 1 && (fresh[0]!.id === first || fresh[0]!.id === last)) {
|
|
51
|
+
patch();
|
|
52
|
+
timeline.add(fresh[0]!, fresh[0]!.id === first ? 'start' : 'end');
|
|
53
|
+
} else if (sameOrder && !fresh.length && gone.length <= 1) {
|
|
54
|
+
patch();
|
|
55
|
+
if (gone[0]) timeline.remove(gone[0].id);
|
|
56
|
+
} else timeline.setEvents(events);
|
|
57
|
+
}, [key]);
|
|
58
|
+
return <div {...props} ref={ref} className={['mk-timeline', className].filter(Boolean).join(' ')} />;
|
|
59
|
+
}
|
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
/* timeline — a 2 px rail (outline-soft) behind 2rem dots, its done stretch in the accent; dots by state (done: accent,
|
|
2
|
+
current: accent-soft with a ring that breathes, upcoming: dashed, error: error); a muted time, a 600 title, a muted
|
|
3
|
+
description; days are chips on the rail. 'alternate' puts the events on both sides of a centred rail when the timeline
|
|
4
|
+
is 520 px wide or more. */
|
|
5
|
+
.mk-timeline,
|
|
6
|
+
mk-timeline {
|
|
7
|
+
container-type: inline-size;
|
|
8
|
+
display: block;
|
|
9
|
+
min-width: 0;
|
|
10
|
+
color: var(--mk-text);
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
[data-mk-tl-wrap] {
|
|
14
|
+
position: relative;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
[data-mk-timeline-list] {
|
|
18
|
+
display: grid;
|
|
19
|
+
gap: 1.1rem;
|
|
20
|
+
margin: 0;
|
|
21
|
+
padding: 0;
|
|
22
|
+
list-style: none;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
[data-mk-tl-rail] {
|
|
26
|
+
position: absolute;
|
|
27
|
+
left: calc(1rem - 1px);
|
|
28
|
+
width: 2px;
|
|
29
|
+
overflow: hidden;
|
|
30
|
+
border-radius: 2px;
|
|
31
|
+
background: var(--mk-outline-soft);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
[data-mk-tl-rail][hidden] {
|
|
35
|
+
display: none;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
[data-mk-tl-progress] {
|
|
39
|
+
display: block;
|
|
40
|
+
width: 100%;
|
|
41
|
+
border-radius: inherit;
|
|
42
|
+
background: var(--mk-accent);
|
|
43
|
+
transition: height 0.5s cubic-bezier(0.38, 0.49, 0, 1);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
[data-mk-tl-item] {
|
|
47
|
+
display: grid;
|
|
48
|
+
grid-template-columns: 2rem minmax(0, 1fr);
|
|
49
|
+
align-items: start;
|
|
50
|
+
column-gap: 0.85rem;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
[data-mk-tl-dot] {
|
|
54
|
+
position: relative;
|
|
55
|
+
z-index: 1;
|
|
56
|
+
display: grid;
|
|
57
|
+
place-items: center;
|
|
58
|
+
width: 2rem;
|
|
59
|
+
height: 2rem;
|
|
60
|
+
border-radius: 50%;
|
|
61
|
+
background: var(--mk-accent);
|
|
62
|
+
color: var(--mk-on-accent);
|
|
63
|
+
box-shadow: 0 0 0 4px var(--mk-surface-1, var(--mk-bg));
|
|
64
|
+
font-size: 0.8125rem;
|
|
65
|
+
font-weight: 700;
|
|
66
|
+
line-height: 1;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
[data-mk-tl-dot]:empty::after {
|
|
70
|
+
content: '';
|
|
71
|
+
width: 0.5rem;
|
|
72
|
+
height: 0.5rem;
|
|
73
|
+
border-radius: 50%;
|
|
74
|
+
background: currentColor;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
[data-mk-tl-item][data-status='current'] [data-mk-tl-dot] {
|
|
78
|
+
background: var(--mk-accent-soft);
|
|
79
|
+
color: var(--mk-on-accent-soft, var(--mk-text));
|
|
80
|
+
animation: mk-tl-breathe 2.2s ease-in-out infinite;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
[data-mk-tl-item][data-status='upcoming'] [data-mk-tl-dot] {
|
|
84
|
+
background: var(--mk-surface-1, var(--mk-bg));
|
|
85
|
+
color: var(--mk-muted);
|
|
86
|
+
outline: 2px dashed var(--mk-outline);
|
|
87
|
+
outline-offset: -2px;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
[data-mk-tl-item][data-status='error'] [data-mk-tl-dot] {
|
|
91
|
+
background: var(--mk-error, #c62828);
|
|
92
|
+
color: var(--mk-on-error, #fff);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
@keyframes mk-tl-breathe {
|
|
96
|
+
0%,
|
|
97
|
+
100% {
|
|
98
|
+
box-shadow: 0 0 0 4px var(--mk-surface-1, var(--mk-bg)), 0 0 0 4px color-mix(in srgb, var(--mk-accent) 45%, transparent);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
50% {
|
|
102
|
+
box-shadow: 0 0 0 4px var(--mk-surface-1, var(--mk-bg)), 0 0 0 9px color-mix(in srgb, var(--mk-accent) 0%, transparent);
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
[data-mk-tl-body] {
|
|
107
|
+
position: relative;
|
|
108
|
+
isolation: isolate;
|
|
109
|
+
display: grid;
|
|
110
|
+
justify-self: start;
|
|
111
|
+
gap: 0.2rem;
|
|
112
|
+
min-width: 0;
|
|
113
|
+
padding-top: 0.25rem;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/* A new event: a soft accent highlight behind it that fades. */
|
|
117
|
+
[data-mk-tl-fresh] [data-mk-tl-body]::before {
|
|
118
|
+
content: '';
|
|
119
|
+
position: absolute;
|
|
120
|
+
inset: -0.125rem -0.625rem -0.375rem;
|
|
121
|
+
z-index: -1;
|
|
122
|
+
border-radius: 12px;
|
|
123
|
+
background: var(--mk-accent-soft);
|
|
124
|
+
animation: mk-tl-fresh 1.8s ease-out both;
|
|
125
|
+
pointer-events: none;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
@keyframes mk-tl-fresh {
|
|
129
|
+
from {
|
|
130
|
+
opacity: 0.75;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
to {
|
|
134
|
+
opacity: 0;
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
[data-mk-tl-time],
|
|
139
|
+
[data-mk-tl-meta] {
|
|
140
|
+
color: var(--mk-muted);
|
|
141
|
+
font-size: 0.75rem;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
[data-mk-tl-title] {
|
|
145
|
+
font-weight: 600;
|
|
146
|
+
line-height: 1.35;
|
|
147
|
+
overflow-wrap: anywhere;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
[data-mk-tl-item][data-status='upcoming'] [data-mk-tl-title] {
|
|
151
|
+
color: var(--mk-muted);
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
[data-mk-tl-description] {
|
|
155
|
+
margin: 0;
|
|
156
|
+
color: var(--mk-muted);
|
|
157
|
+
font-size: 0.875rem;
|
|
158
|
+
line-height: 1.5;
|
|
159
|
+
overflow-wrap: anywhere;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/* A day is a small chip on the rail (it hides the line behind it). */
|
|
163
|
+
[data-mk-tl-day] {
|
|
164
|
+
position: relative;
|
|
165
|
+
z-index: 1;
|
|
166
|
+
justify-self: start;
|
|
167
|
+
margin-top: 0.35rem;
|
|
168
|
+
padding: 0.25rem 0.625rem;
|
|
169
|
+
border-radius: 8px;
|
|
170
|
+
background: var(--mk-surface-3);
|
|
171
|
+
color: var(--mk-muted);
|
|
172
|
+
font-size: 0.6875rem;
|
|
173
|
+
font-weight: 700;
|
|
174
|
+
letter-spacing: 0.04em;
|
|
175
|
+
text-transform: uppercase;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
@container (min-width: 520px) {
|
|
179
|
+
:is(.mk-timeline, mk-timeline)[data-mk-layout='alternate'] [data-mk-tl-rail] {
|
|
180
|
+
left: calc(50% - 1px);
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
:is(.mk-timeline, mk-timeline)[data-mk-layout='alternate'] [data-mk-tl-item] {
|
|
184
|
+
grid-template-columns: minmax(0, 1fr) 2rem minmax(0, 1fr);
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
:is(.mk-timeline, mk-timeline)[data-mk-layout='alternate'] [data-mk-tl-dot] {
|
|
188
|
+
grid-row: 1;
|
|
189
|
+
grid-column: 2;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
:is(.mk-timeline, mk-timeline)[data-mk-layout='alternate'] [data-mk-tl-item][data-side='start'] [data-mk-tl-body] {
|
|
193
|
+
grid-row: 1;
|
|
194
|
+
grid-column: 1;
|
|
195
|
+
justify-self: end;
|
|
196
|
+
text-align: end;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
:is(.mk-timeline, mk-timeline)[data-mk-layout='alternate'] [data-mk-tl-item][data-side='end'] [data-mk-tl-body] {
|
|
200
|
+
grid-row: 1;
|
|
201
|
+
grid-column: 3;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
:is(.mk-timeline, mk-timeline)[data-mk-layout='alternate'] [data-mk-tl-day] {
|
|
205
|
+
justify-self: center;
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
@media (prefers-reduced-motion: reduce) {
|
|
210
|
+
[data-mk-tl-item][data-status='current'] [data-mk-tl-dot] {
|
|
211
|
+
animation: none;
|
|
212
|
+
box-shadow: 0 0 0 4px var(--mk-surface-1, var(--mk-bg)), 0 0 0 6px color-mix(in srgb, var(--mk-accent) 45%, transparent);
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
[data-mk-tl-progress] {
|
|
216
|
+
transition: none;
|
|
217
|
+
}
|
|
218
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
# tooltip
|
|
2
|
+
|
|
3
|
+
Descripción breve junto a su disparador. Aparece cuando el puntero se detiene sobre él (400 ms), en cuanto el foco del teclado llega (al momento) o con una pulsación larga en pantallas táctiles (500 ms). Toma el lado preferido, cambia al opuesto si no cabe, se mantiene dentro de la ventana y apunta con una flecha al centro del disparador. Vive en la capa superior (API `popover`), así que ningún `overflow` lo recorta.
|
|
4
|
+
|
|
5
|
+
Movimiento (línea Melon Mind): sale de la flecha con un pop (escala .85, 4 px hacia el disparador, blur 4 → 0, .22 s) y se va en .12 s. Si pasas a otro tooltip en menos de .4 s, aparece al instante con un fundido simple («caliente»). El texto nuevo cambia con un fundido.
|
|
6
|
+
|
|
7
|
+
## HTML / Astro
|
|
8
|
+
|
|
9
|
+
```html
|
|
10
|
+
<mk-tooltip class="mk-tooltip" content="Copiar enlace">
|
|
11
|
+
<button class="mk-button mk-button--icon" aria-label="Copiar">⧉</button>
|
|
12
|
+
</mk-tooltip>
|
|
13
|
+
|
|
14
|
+
<!-- Sin content usa el title del disparador (y lo quita, para que no salga el del navegador). -->
|
|
15
|
+
<mk-tooltip placement="right"><button title="Ajustes">⚙</button></mk-tooltip>
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
## React
|
|
19
|
+
|
|
20
|
+
```tsx
|
|
21
|
+
<Tooltip content={copied ? 'Copiado' : 'Copiar enlace'}>
|
|
22
|
+
<button onClick={copy}>⧉</button>
|
|
23
|
+
</Tooltip>
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
El primer elemento enfocable dentro es el disparador (o `[data-mk-tooltip-trigger]`). Si no hay ninguno, el propio contenedor recibe `tabindex="0"`.
|
|
27
|
+
|
|
28
|
+
## Opciones
|
|
29
|
+
|
|
30
|
+
| Opción | Atributo | Por defecto | Qué hace |
|
|
31
|
+
|---|---|---|---|
|
|
32
|
+
| `content` | `content` | el `title` del disparador | Texto del tooltip. |
|
|
33
|
+
| `placement` | `placement` | `'top'` | `top`, `bottom`, `left`, `right` o `auto` (el lado con más espacio). |
|
|
34
|
+
| `openDelay` | `open-delay` | `400` | Espera al señalar, en ms. |
|
|
35
|
+
| `closeDelay` | `close-delay` | `100` | Espera al salir, en ms. |
|
|
36
|
+
| `interactive` | `interactive` | `false` | El puntero puede entrar en el tooltip (enlaces, texto seleccionable). |
|
|
37
|
+
| `arrow` | `arrow` | `true` | Flecha hacia el disparador. |
|
|
38
|
+
| `onOpenChange(open)` | — | — | Al abrirse o cerrarse. |
|
|
39
|
+
|
|
40
|
+
## API y eventos
|
|
41
|
+
|
|
42
|
+
`open()`, `close()`, `setContent(texto)` (un texto vacío lo oculta), `isOpen`, `destroy()`. Evento `mk:open-change` con `{ open }`.
|
|
43
|
+
|
|
44
|
+
## Accesibilidad
|
|
45
|
+
|
|
46
|
+
`role="tooltip"` y `aria-describedby` en el disparador. Escape lo oculta sin mover el foco (WCAG 1.4.13). Con movimiento reducido aparece y desaparece sin animación. Variable CSS: `--mk-tooltip-bg` (cristal invertido).
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { MkElement, defineElement, type OptionType } from '../../elements/base';
|
|
2
|
+
import { createTooltip, type TooltipInstance } from './engine';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* <mk-tooltip class="mk-tooltip" content="Copiar enlace"><button>…</button></mk-tooltip>
|
|
6
|
+
* Without `content`, the trigger's title. Emits mk:open-change ({open}).
|
|
7
|
+
*/
|
|
8
|
+
export class MkTooltipElement extends MkElement<TooltipInstance> {
|
|
9
|
+
static options: Record<string, OptionType> = { content: 'string', placement: 'string', openDelay: 'number', closeDelay: 'number', interactive: 'boolean', arrow: 'boolean' };
|
|
10
|
+
|
|
11
|
+
protected create(options: Record<string, unknown>) {
|
|
12
|
+
return createTooltip(this, options);
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
attributeChangedCallback(name: string, previous: string | null, next: string | null) {
|
|
16
|
+
if (previous === next || !this.instance) return;
|
|
17
|
+
if (name === 'content') this.instance.setContent(next ?? '');
|
|
18
|
+
else super.attributeChangedCallback(name, previous, next);
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export const defineTooltip = () => defineElement('mk-tooltip', MkTooltipElement);
|
|
@@ -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
|
+
}
|