@zyncat/ui 0.8.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/LICENSE +21 -0
- package/README.md +133 -0
- package/dist/alert.css +132 -0
- package/dist/alert.d.ts +46 -0
- package/dist/alert.js +80 -0
- package/dist/alert.js.map +1 -0
- package/dist/avatar-group.d.ts +24 -0
- package/dist/avatar-group.js +23 -0
- package/dist/avatar-group.js.map +1 -0
- package/dist/avatar.css +200 -0
- package/dist/avatar.d.ts +35 -0
- package/dist/avatar.js +67 -0
- package/dist/avatar.js.map +1 -0
- package/dist/badge.css +182 -0
- package/dist/badge.d.ts +34 -0
- package/dist/badge.js +4 -0
- package/dist/badge.js.map +1 -0
- package/dist/button.css +269 -0
- package/dist/button.d.ts +44 -0
- package/dist/button.js +4 -0
- package/dist/button.js.map +1 -0
- package/dist/checkbox.css +177 -0
- package/dist/checkbox.d.ts +35 -0
- package/dist/checkbox.js +5 -0
- package/dist/checkbox.js.map +1 -0
- package/dist/chunk-2ERBBFZS.js +12 -0
- package/dist/chunk-2ERBBFZS.js.map +1 -0
- package/dist/chunk-2HORSLXJ.js +191 -0
- package/dist/chunk-2HORSLXJ.js.map +1 -0
- package/dist/chunk-36WVAQKL.js +40 -0
- package/dist/chunk-36WVAQKL.js.map +1 -0
- package/dist/chunk-3SBCBYB7.js +575 -0
- package/dist/chunk-3SBCBYB7.js.map +1 -0
- package/dist/chunk-4R2GQO43.js +125 -0
- package/dist/chunk-4R2GQO43.js.map +1 -0
- package/dist/chunk-64UU4LGU.js +258 -0
- package/dist/chunk-64UU4LGU.js.map +1 -0
- package/dist/chunk-7SZCJ7WI.js +35 -0
- package/dist/chunk-7SZCJ7WI.js.map +1 -0
- package/dist/chunk-BIDVNLJO.js +38 -0
- package/dist/chunk-BIDVNLJO.js.map +1 -0
- package/dist/chunk-CONSWNIV.js +17 -0
- package/dist/chunk-CONSWNIV.js.map +1 -0
- package/dist/chunk-EQ32F2QJ.js +75 -0
- package/dist/chunk-EQ32F2QJ.js.map +1 -0
- package/dist/chunk-GY7JOPUQ.js +111 -0
- package/dist/chunk-GY7JOPUQ.js.map +1 -0
- package/dist/chunk-I4XQ4PV2.js +30 -0
- package/dist/chunk-I4XQ4PV2.js.map +1 -0
- package/dist/chunk-JSKXZMLH.js +67 -0
- package/dist/chunk-JSKXZMLH.js.map +1 -0
- package/dist/chunk-L3Q3VDIW.js +295 -0
- package/dist/chunk-L3Q3VDIW.js.map +1 -0
- package/dist/chunk-LDC3RIYA.js +62 -0
- package/dist/chunk-LDC3RIYA.js.map +1 -0
- package/dist/chunk-MKIQFK7S.js +43 -0
- package/dist/chunk-MKIQFK7S.js.map +1 -0
- package/dist/chunk-MMRIQNEN.js +20 -0
- package/dist/chunk-MMRIQNEN.js.map +1 -0
- package/dist/chunk-MXOPCFWR.js +59 -0
- package/dist/chunk-MXOPCFWR.js.map +1 -0
- package/dist/chunk-NEF5XAHG.js +17 -0
- package/dist/chunk-NEF5XAHG.js.map +1 -0
- package/dist/chunk-NNXJCLTV.js +22 -0
- package/dist/chunk-NNXJCLTV.js.map +1 -0
- package/dist/chunk-O4N2OBTR.js +94 -0
- package/dist/chunk-O4N2OBTR.js.map +1 -0
- package/dist/chunk-PMECHR3A.js +38 -0
- package/dist/chunk-PMECHR3A.js.map +1 -0
- package/dist/chunk-QDK66S4G.js +44 -0
- package/dist/chunk-QDK66S4G.js.map +1 -0
- package/dist/chunk-QGNC7LH5.js +8 -0
- package/dist/chunk-QGNC7LH5.js.map +1 -0
- package/dist/chunk-QMSJZCK3.js +398 -0
- package/dist/chunk-QMSJZCK3.js.map +1 -0
- package/dist/chunk-S5ZTZANS.js +57 -0
- package/dist/chunk-S5ZTZANS.js.map +1 -0
- package/dist/chunk-S7B34VGN.js +14 -0
- package/dist/chunk-S7B34VGN.js.map +1 -0
- package/dist/chunk-TCUCYFT7.js +25 -0
- package/dist/chunk-TCUCYFT7.js.map +1 -0
- package/dist/chunk-TYHR4VNM.js +193 -0
- package/dist/chunk-TYHR4VNM.js.map +1 -0
- package/dist/chunk-U5QQZC3U.js +56 -0
- package/dist/chunk-U5QQZC3U.js.map +1 -0
- package/dist/chunk-UE63OOVG.js +13 -0
- package/dist/chunk-UE63OOVG.js.map +1 -0
- package/dist/chunk-VTJTGVZZ.js +22 -0
- package/dist/chunk-VTJTGVZZ.js.map +1 -0
- package/dist/chunk-W3SHPKSB.js +49 -0
- package/dist/chunk-W3SHPKSB.js.map +1 -0
- package/dist/chunk-XZAEIQDR.js +188 -0
- package/dist/chunk-XZAEIQDR.js.map +1 -0
- package/dist/chunk-YGGCQNNU.js +92 -0
- package/dist/chunk-YGGCQNNU.js.map +1 -0
- package/dist/collapse.css +88 -0
- package/dist/collapse.d.ts +46 -0
- package/dist/collapse.js +5 -0
- package/dist/collapse.js.map +1 -0
- package/dist/count-badge.d.ts +13 -0
- package/dist/count-badge.js +13 -0
- package/dist/count-badge.js.map +1 -0
- package/dist/date-field.d.ts +25 -0
- package/dist/date-field.js +87 -0
- package/dist/date-field.js.map +1 -0
- package/dist/date-picker.css +595 -0
- package/dist/date-range-field.d.ts +33 -0
- package/dist/date-range-field.js +404 -0
- package/dist/date-range-field.js.map +1 -0
- package/dist/datetime-field.d.ts +29 -0
- package/dist/datetime-field.js +155 -0
- package/dist/datetime-field.js.map +1 -0
- package/dist/dialog.css +180 -0
- package/dist/dialog.d.ts +42 -0
- package/dist/dialog.js +128 -0
- package/dist/dialog.js.map +1 -0
- package/dist/dom-props-C0jrllBw.d.ts +5 -0
- package/dist/field-chrome-CMPOQSLs.d.ts +28 -0
- package/dist/field-shell-Cuda-feL.d.ts +24 -0
- package/dist/glide.d.ts +15 -0
- package/dist/glide.js +5 -0
- package/dist/glide.js.map +1 -0
- package/dist/icon.css +33 -0
- package/dist/input.css +333 -0
- package/dist/mcp.js +359 -0
- package/dist/motion-devtools.css +265 -0
- package/dist/motion-devtools.d.ts +51 -0
- package/dist/motion-devtools.js +354 -0
- package/dist/motion-devtools.js.map +1 -0
- package/dist/motion-scale-Canq4stZ.d.ts +18 -0
- package/dist/motion-tokens.d.ts +28 -0
- package/dist/motion-tokens.js +3 -0
- package/dist/motion-tokens.js.map +1 -0
- package/dist/multi-select.d.ts +44 -0
- package/dist/multi-select.js +109 -0
- package/dist/multi-select.js.map +1 -0
- package/dist/number-field.d.ts +36 -0
- package/dist/number-field.js +112 -0
- package/dist/number-field.js.map +1 -0
- package/dist/odometer.css +62 -0
- package/dist/otp-field.d.ts +31 -0
- package/dist/otp-field.js +98 -0
- package/dist/otp-field.js.map +1 -0
- package/dist/overlay.css +23 -0
- package/dist/pagination.css +43 -0
- package/dist/pagination.d.ts +29 -0
- package/dist/pagination.js +107 -0
- package/dist/pagination.js.map +1 -0
- package/dist/popover.css +69 -0
- package/dist/popover.d.ts +35 -0
- package/dist/popover.js +15 -0
- package/dist/popover.js.map +1 -0
- package/dist/radio-group.css +300 -0
- package/dist/radio-group.d.ts +52 -0
- package/dist/radio-group.js +115 -0
- package/dist/radio-group.js.map +1 -0
- package/dist/select.css +416 -0
- package/dist/select.d.ts +49 -0
- package/dist/select.js +99 -0
- package/dist/select.js.map +1 -0
- package/dist/sheet.css +26 -0
- package/dist/sheet.d.ts +31 -0
- package/dist/sheet.js +14 -0
- package/dist/sheet.js.map +1 -0
- package/dist/status-badge.d.ts +19 -0
- package/dist/status-badge.js +72 -0
- package/dist/status-badge.js.map +1 -0
- package/dist/table.css +348 -0
- package/dist/table.d.ts +75 -0
- package/dist/table.js +262 -0
- package/dist/table.js.map +1 -0
- package/dist/tabs.css +208 -0
- package/dist/tabs.d.ts +71 -0
- package/dist/tabs.js +181 -0
- package/dist/tabs.js.map +1 -0
- package/dist/tag.css +248 -0
- package/dist/tag.d.ts +52 -0
- package/dist/tag.js +72 -0
- package/dist/tag.js.map +1 -0
- package/dist/text-field.d.ts +28 -0
- package/dist/text-field.js +69 -0
- package/dist/text-field.js.map +1 -0
- package/dist/textarea.css +219 -0
- package/dist/textarea.d.ts +38 -0
- package/dist/textarea.js +144 -0
- package/dist/textarea.js.map +1 -0
- package/dist/time-field.d.ts +28 -0
- package/dist/time-field.js +66 -0
- package/dist/time-field.js.map +1 -0
- package/dist/timing-7JA_zQHP.d.ts +27 -0
- package/dist/toast-store.d.ts +86 -0
- package/dist/toast-store.js +3 -0
- package/dist/toast-store.js.map +1 -0
- package/dist/toast.css +257 -0
- package/dist/toast.d.ts +12 -0
- package/dist/toast.js +310 -0
- package/dist/toast.js.map +1 -0
- package/dist/toggle-tag.d.ts +34 -0
- package/dist/toggle-tag.js +57 -0
- package/dist/toggle-tag.js.map +1 -0
- package/dist/toggle.css +152 -0
- package/dist/toggle.d.ts +31 -0
- package/dist/toggle.js +75 -0
- package/dist/toggle.js.map +1 -0
- package/dist/tooltip.css +86 -0
- package/dist/tooltip.d.ts +30 -0
- package/dist/tooltip.js +268 -0
- package/dist/tooltip.js.map +1 -0
- package/dist/types-SGMShK7R.d.ts +22 -0
- package/llms.txt +196 -0
- package/next.d.mts +7 -0
- package/next.mjs +59 -0
- package/package.json +257 -0
- package/src/components/composites/alert/Alert.tsx +141 -0
- package/src/components/composites/alert/alert.css +132 -0
- package/src/components/composites/date-picker/DateField.tsx +80 -0
- package/src/components/composites/date-picker/DateRangeField.tsx +110 -0
- package/src/components/composites/date-picker/DateTimeField.tsx +162 -0
- package/src/components/composites/date-picker/TimeField.tsx +78 -0
- package/src/components/composites/date-picker/calendar-panel.tsx +216 -0
- package/src/components/composites/date-picker/date-picker.css +595 -0
- package/src/components/composites/date-picker/date-utils.ts +56 -0
- package/src/components/composites/date-picker/field-shell.tsx +85 -0
- package/src/components/composites/date-picker/range-panel.tsx +363 -0
- package/src/components/composites/date-picker/time-core.tsx +290 -0
- package/src/components/composites/date-picker/use-day-focus.ts +35 -0
- package/src/components/composites/dialog/Dialog.tsx +198 -0
- package/src/components/composites/dialog/dialog.css +180 -0
- package/src/components/composites/pagination/Pagination.tsx +131 -0
- package/src/components/composites/pagination/pagination.css +43 -0
- package/src/components/composites/popover/Popover.tsx +141 -0
- package/src/components/composites/popover/popover.css +69 -0
- package/src/components/composites/select/MultiSelect.tsx +133 -0
- package/src/components/composites/select/Select.tsx +122 -0
- package/src/components/composites/select/core/index.ts +4 -0
- package/src/components/composites/select/core/menu.tsx +100 -0
- package/src/components/composites/select/core/panel.tsx +154 -0
- package/src/components/composites/select/core/trigger.tsx +74 -0
- package/src/components/composites/select/core/types.ts +39 -0
- package/src/components/composites/select/core/use-listbox.ts +182 -0
- package/src/components/composites/select/select.css +416 -0
- package/src/components/composites/sheet/Sheet.tsx +135 -0
- package/src/components/composites/sheet/sheet.css +26 -0
- package/src/components/composites/sheet/use-sheet-drag.ts +126 -0
- package/src/components/composites/table/Table.tsx +377 -0
- package/src/components/composites/table/table.css +348 -0
- package/src/components/composites/tabs/Tabs.tsx +278 -0
- package/src/components/composites/tabs/tabs.css +208 -0
- package/src/components/composites/toast/Toast.tsx +380 -0
- package/src/components/composites/toast/toast-store.ts +295 -0
- package/src/components/composites/toast/toast.css +257 -0
- package/src/components/composites/tooltip/Tooltip.tsx +110 -0
- package/src/components/composites/tooltip/tooltip-host.tsx +184 -0
- package/src/components/composites/tooltip/tooltip-store.ts +81 -0
- package/src/components/composites/tooltip/tooltip.css +86 -0
- package/src/components/dev/MotionDevtools.tsx +200 -0
- package/src/components/dev/motion-devtools.css +265 -0
- package/src/components/dev/slowmo-engine.ts +230 -0
- package/src/components/internal/glass/glass.css +100 -0
- package/src/components/internal/glass/glint.ts +9 -0
- package/src/components/internal/hooks/use-controllable.ts +21 -0
- package/src/components/internal/hooks/use-scroll-edges.ts +46 -0
- package/src/components/internal/icon/Icon.tsx +66 -0
- package/src/components/internal/icon/IconSlot.tsx +10 -0
- package/src/components/internal/icon/icon.css +33 -0
- package/src/components/internal/overlay/focus.ts +68 -0
- package/src/components/internal/overlay/layer.tsx +149 -0
- package/src/components/internal/overlay/modal.tsx +141 -0
- package/src/components/internal/overlay/overlay.css +23 -0
- package/src/components/internal/overlay/position.ts +103 -0
- package/src/components/internal/utils/cx.ts +3 -0
- package/src/components/internal/utils/token-px.ts +8 -0
- package/src/components/primitives/avatar/Avatar.tsx +120 -0
- package/src/components/primitives/avatar/AvatarGroup.tsx +56 -0
- package/src/components/primitives/avatar/avatar.css +200 -0
- package/src/components/primitives/badge/Badge.tsx +75 -0
- package/src/components/primitives/badge/CountBadge.tsx +20 -0
- package/src/components/primitives/badge/StatusBadge.tsx +111 -0
- package/src/components/primitives/badge/badge.css +182 -0
- package/src/components/primitives/badge/odometer.css +62 -0
- package/src/components/primitives/badge/odometer.tsx +36 -0
- package/src/components/primitives/button/Button.tsx +102 -0
- package/src/components/primitives/button/button.css +269 -0
- package/src/components/primitives/checkbox/Checkbox.tsx +76 -0
- package/src/components/primitives/checkbox/check-glyph.tsx +28 -0
- package/src/components/primitives/checkbox/checkbox.css +177 -0
- package/src/components/primitives/collapse/Collapse.tsx +86 -0
- package/src/components/primitives/collapse/collapse.css +88 -0
- package/src/components/primitives/collapse/collapse.typecheck.tsx +29 -0
- package/src/components/primitives/input/NumberField.tsx +139 -0
- package/src/components/primitives/input/OtpField.tsx +134 -0
- package/src/components/primitives/input/TextField.tsx +110 -0
- package/src/components/primitives/input/field-chrome.tsx +83 -0
- package/src/components/primitives/input/input.css +333 -0
- package/src/components/primitives/radio-group/RadioGroup.tsx +183 -0
- package/src/components/primitives/radio-group/radio-group.css +300 -0
- package/src/components/primitives/tag/Tag.tsx +143 -0
- package/src/components/primitives/tag/ToggleTag.tsx +97 -0
- package/src/components/primitives/tag/tag.css +248 -0
- package/src/components/primitives/textarea/Textarea.tsx +190 -0
- package/src/components/primitives/textarea/textarea.css +219 -0
- package/src/components/primitives/toggle/Toggle.tsx +105 -0
- package/src/components/primitives/toggle/toggle.css +152 -0
- package/src/css.d.ts +1 -0
- package/src/dom-props.ts +1 -0
- package/src/engine/animate.ts +141 -0
- package/src/engine/drag.ts +53 -0
- package/src/engine/ease.ts +155 -0
- package/src/engine/flip.ts +59 -0
- package/src/engine/index.ts +13 -0
- package/src/mcp/server.ts +420 -0
- package/src/motion/element.tsx +53 -0
- package/src/motion/flip.tsx +87 -0
- package/src/motion/glide.tsx +47 -0
- package/src/motion/motion-timing.ts +33 -0
- package/src/motion/presence-context.ts +18 -0
- package/src/motion/presence.tsx +91 -0
- package/src/motion/timing.ts +49 -0
- package/src/motion/use-motion.ts +64 -0
- package/src/styles.css +26 -0
- package/src/tokens/avatar.css +20 -0
- package/src/tokens/color.css +58 -0
- package/src/tokens/elevation.css +22 -0
- package/src/tokens/fonts.css +3 -0
- package/src/tokens/glass.css +20 -0
- package/src/tokens/icons.css +7 -0
- package/src/tokens/layers.css +6 -0
- package/src/tokens/motion-scale.ts +17 -0
- package/src/tokens/motion-tokens.ts +92 -0
- package/src/tokens/motion.css +49 -0
- package/src/tokens/radius.css +11 -0
- package/src/tokens/semantic.css +65 -0
- package/src/tokens/spacing.css +26 -0
- package/src/tokens/typography.css +52 -0
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { useLayoutEffect, useRef, type RefObject } from 'react';
|
|
4
|
+
import {
|
|
5
|
+
dropShared,
|
|
6
|
+
flip,
|
|
7
|
+
keepShared,
|
|
8
|
+
measure,
|
|
9
|
+
readShared,
|
|
10
|
+
type Box,
|
|
11
|
+
type FlipOptions,
|
|
12
|
+
type Playback,
|
|
13
|
+
} from '../engine';
|
|
14
|
+
import { UIMotion } from '../tokens/motion-tokens';
|
|
15
|
+
|
|
16
|
+
export type FlipTuning = FlipOptions;
|
|
17
|
+
|
|
18
|
+
export function useSelfFlip<T extends HTMLElement>(tuning: FlipTuning = {}, enabled = true): RefObject<T | null> {
|
|
19
|
+
const ref = useRef<T | null>(null);
|
|
20
|
+
const previous = useRef<Box | null>(null);
|
|
21
|
+
const playing = useRef<Playback | null>(null);
|
|
22
|
+
const { scale, timing } = tuning;
|
|
23
|
+
|
|
24
|
+
useLayoutEffect(() => {
|
|
25
|
+
const el = ref.current;
|
|
26
|
+
if (!el || !enabled) return;
|
|
27
|
+
const from = playing.current ? measure(el) : previous.current;
|
|
28
|
+
playing.current?.stop();
|
|
29
|
+
previous.current = measure(el);
|
|
30
|
+
const next = from && !UIMotion.reduced ? flip(el, from, { scale, timing }) : null;
|
|
31
|
+
playing.current = next;
|
|
32
|
+
next?.finished.then(() => {
|
|
33
|
+
if (playing.current === next) playing.current = null;
|
|
34
|
+
});
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
useLayoutEffect(
|
|
38
|
+
() => () => {
|
|
39
|
+
playing.current?.stop();
|
|
40
|
+
},
|
|
41
|
+
[],
|
|
42
|
+
);
|
|
43
|
+
|
|
44
|
+
return ref;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export function useSharedFlip<T extends HTMLElement>(
|
|
48
|
+
id: string,
|
|
49
|
+
tuning: FlipTuning = {},
|
|
50
|
+
enabled = true,
|
|
51
|
+
): RefObject<T | null> {
|
|
52
|
+
const ref = useRef<T | null>(null);
|
|
53
|
+
const playing = useRef<{ play: Playback; el: T } | null>(null);
|
|
54
|
+
const { scale, timing } = tuning;
|
|
55
|
+
|
|
56
|
+
useLayoutEffect(() => {
|
|
57
|
+
const el = ref.current;
|
|
58
|
+
if (!el || !enabled) return;
|
|
59
|
+
const live = playing.current;
|
|
60
|
+
const from = live && live.el === el ? measure(el) : readShared(id);
|
|
61
|
+
live?.play.stop();
|
|
62
|
+
keepShared(id, measure(el));
|
|
63
|
+
const next = from && !UIMotion.reduced ? flip(el, from, { scale, timing }) : null;
|
|
64
|
+
playing.current = next ? { play: next, el } : null;
|
|
65
|
+
next?.finished.then(() => {
|
|
66
|
+
if (playing.current?.play === next) playing.current = null;
|
|
67
|
+
});
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
useLayoutEffect(() => {
|
|
71
|
+
if (!enabled) return;
|
|
72
|
+
return () => {
|
|
73
|
+
const live = playing.current;
|
|
74
|
+
if (live) {
|
|
75
|
+
const box = measure(live.el);
|
|
76
|
+
if (box.width && box.height) keepShared(id, box);
|
|
77
|
+
live.play.stop();
|
|
78
|
+
}
|
|
79
|
+
const mine = readShared(id);
|
|
80
|
+
queueMicrotask(() => {
|
|
81
|
+
if (readShared(id) === mine) dropShared(id);
|
|
82
|
+
});
|
|
83
|
+
};
|
|
84
|
+
}, [id, enabled]);
|
|
85
|
+
|
|
86
|
+
return ref;
|
|
87
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { useRef, type RefObject } from 'react';
|
|
4
|
+
import { animate } from '../engine';
|
|
5
|
+
import { UIMotion as SM } from '../tokens/motion-tokens';
|
|
6
|
+
|
|
7
|
+
export type GlideApi = ReturnType<typeof useGlide>;
|
|
8
|
+
|
|
9
|
+
export function useGlide<T extends HTMLElement>(containerRef: RefObject<T | null>) {
|
|
10
|
+
const ref = useRef<HTMLSpanElement | null>(null);
|
|
11
|
+
const visible = useRef(false);
|
|
12
|
+
|
|
13
|
+
function enter(target: HTMLElement | null) {
|
|
14
|
+
const box = containerRef.current,
|
|
15
|
+
pill = ref.current;
|
|
16
|
+
if (!box || !pill || !target) return;
|
|
17
|
+
const was = pill.getBoundingClientRect();
|
|
18
|
+
const b = box.getBoundingClientRect();
|
|
19
|
+
const t = target.getBoundingClientRect();
|
|
20
|
+
const s = box.offsetWidth ? b.width / box.offsetWidth : 1;
|
|
21
|
+
pill.style.translate = `${(t.left - b.left) / s + box.scrollLeft}px ${(t.top - b.top) / s + box.scrollTop}px`;
|
|
22
|
+
pill.style.width = `${t.width / s}px`;
|
|
23
|
+
pill.style.height = `${t.height / s}px`;
|
|
24
|
+
const now = pill.getBoundingClientRect();
|
|
25
|
+
animate(pill, { opacity: [1], timing: { duration: SM.dur.fast, ease: SM.ease.standard } });
|
|
26
|
+
const glided = visible.current && !SM.reduced;
|
|
27
|
+
visible.current = true;
|
|
28
|
+
if (!glided) return;
|
|
29
|
+
const timing = { ...SM.t.settle, fill: 'none' as const };
|
|
30
|
+
animate(
|
|
31
|
+
pill,
|
|
32
|
+
{ x: [was.left - now.left, 0], y: [was.top - now.top, 0], timing, composite: 'add' },
|
|
33
|
+
{ width: [was.width, now.width], height: [was.height, now.height], timing },
|
|
34
|
+
);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function leave() {
|
|
38
|
+
visible.current = false;
|
|
39
|
+
if (ref.current) animate(ref.current, { opacity: [0], timing: { duration: SM.dur.fast, ease: SM.ease.exit } });
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
return { ref, enter, leave };
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export function GlidePill({ className, glide }: { className?: string; glide: GlideApi }) {
|
|
46
|
+
return <span ref={glide.ref} className={className} aria-hidden="true" />;
|
|
47
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { UIMotion, type MotionTransition } from '../tokens/motion-tokens';
|
|
2
|
+
import type { DurationToken, EaseToken } from '../tokens/motion-scale';
|
|
3
|
+
import { resolveDirection, type DisableableAnimation } from './timing';
|
|
4
|
+
|
|
5
|
+
export interface DirectionDefault {
|
|
6
|
+
duration: DurationToken;
|
|
7
|
+
ease: EaseToken;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export interface MotionTimingDefaults {
|
|
11
|
+
open: DirectionDefault;
|
|
12
|
+
close: DirectionDefault;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export interface MotionTimings {
|
|
16
|
+
open: MotionTransition;
|
|
17
|
+
close: MotionTransition;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function resolveMotionTiming(
|
|
21
|
+
animation: DisableableAnimation | undefined,
|
|
22
|
+
defaults: MotionTimingDefaults,
|
|
23
|
+
): MotionTimings {
|
|
24
|
+
if (animation === null) return { open: { duration: 0 }, close: { duration: 0 } };
|
|
25
|
+
|
|
26
|
+
const build = (dir: 'open' | 'close'): MotionTransition => {
|
|
27
|
+
const durToken = (animation && resolveDirection(animation.duration, dir)) || defaults[dir].duration;
|
|
28
|
+
const easeToken = (animation && resolveDirection(animation.ease, dir)) || defaults[dir].ease;
|
|
29
|
+
return { duration: UIMotion.dur[durToken], ease: UIMotion.ease[easeToken] };
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
return { open: build('open'), close: build('close') };
|
|
33
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { createContext, useContext } from 'react';
|
|
4
|
+
|
|
5
|
+
export interface PresenceState {
|
|
6
|
+
/** False once the child has been removed from `children` but is still mounted for its exit. */
|
|
7
|
+
isPresent: boolean;
|
|
8
|
+
/** False for children present at the Presence's own first paint when `initial={false}`. */
|
|
9
|
+
initial: boolean;
|
|
10
|
+
/** Called by the child when its exit has finished; lets Presence drop it from the tree. */
|
|
11
|
+
safeToRemove: () => void;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
const DETACHED: PresenceState = { isPresent: true, initial: true, safeToRemove: () => {} };
|
|
15
|
+
|
|
16
|
+
export const PresenceContext = createContext<PresenceState>(DETACHED);
|
|
17
|
+
|
|
18
|
+
export const usePresence = (): PresenceState => useContext(PresenceContext);
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
Children,
|
|
5
|
+
Fragment,
|
|
6
|
+
createElement,
|
|
7
|
+
isValidElement,
|
|
8
|
+
useEffect,
|
|
9
|
+
useLayoutEffect,
|
|
10
|
+
useRef,
|
|
11
|
+
useState,
|
|
12
|
+
type ReactElement,
|
|
13
|
+
type ReactNode,
|
|
14
|
+
} from 'react';
|
|
15
|
+
import { PresenceContext } from './presence-context';
|
|
16
|
+
|
|
17
|
+
interface Entry {
|
|
18
|
+
key: string;
|
|
19
|
+
node: ReactElement;
|
|
20
|
+
exiting: boolean;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function toEntries(children: ReactNode): Entry[] {
|
|
24
|
+
const out: Entry[] = [];
|
|
25
|
+
Children.forEach(children, (child, index) => {
|
|
26
|
+
if (isValidElement(child)) out.push({ key: String(child.key ?? index), node: child, exiting: false });
|
|
27
|
+
});
|
|
28
|
+
return out;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export interface PresenceProps {
|
|
32
|
+
/** Keyed children. A child animates out only if it renders a `<Motion>` carrying `exit`. */
|
|
33
|
+
children?: ReactNode;
|
|
34
|
+
/** Let children present on the first paint play their `animate`. @default true */
|
|
35
|
+
initial?: boolean;
|
|
36
|
+
/** `wait` holds entering children back until the outgoing ones have left. @default 'sync' */
|
|
37
|
+
mode?: 'sync' | 'wait';
|
|
38
|
+
/** Fires once the last exiting child has been removed. */
|
|
39
|
+
onExitComplete?: () => void;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export function Presence({ children, initial = true, mode = 'sync', onExitComplete }: PresenceProps) {
|
|
43
|
+
const incoming = toEntries(children);
|
|
44
|
+
const signature = incoming.map((entry) => entry.key).join('|');
|
|
45
|
+
const [shown, setShown] = useState<Entry[]>(incoming);
|
|
46
|
+
const bornAtMount = useRef(new Set(incoming.map((entry) => entry.key)));
|
|
47
|
+
const wasExiting = useRef(false);
|
|
48
|
+
const complete = useRef(onExitComplete);
|
|
49
|
+
complete.current = onExitComplete;
|
|
50
|
+
|
|
51
|
+
useLayoutEffect(() => {
|
|
52
|
+
setShown((previous) => {
|
|
53
|
+
const live = new Set(incoming.map((entry) => entry.key));
|
|
54
|
+
const merged: Entry[] = incoming.map((entry) => ({ ...entry }));
|
|
55
|
+
previous.forEach((entry, index) => {
|
|
56
|
+
if (live.has(entry.key)) return;
|
|
57
|
+
merged.splice(index, 0, entry.exiting ? entry : { ...entry, exiting: true });
|
|
58
|
+
});
|
|
59
|
+
return merged;
|
|
60
|
+
});
|
|
61
|
+
}, [signature]); // eslint-disable-line react-hooks/exhaustive-deps
|
|
62
|
+
|
|
63
|
+
const leaving = shown.some((entry) => entry.exiting);
|
|
64
|
+
|
|
65
|
+
useEffect(() => {
|
|
66
|
+
if (wasExiting.current && !leaving) complete.current?.();
|
|
67
|
+
wasExiting.current = leaving;
|
|
68
|
+
}, [leaving]);
|
|
69
|
+
|
|
70
|
+
const fresh = new Map(incoming.map((entry) => [entry.key, entry.node]));
|
|
71
|
+
const visible = mode === 'wait' && leaving ? shown.filter((entry) => entry.exiting) : shown;
|
|
72
|
+
|
|
73
|
+
return createElement(
|
|
74
|
+
Fragment,
|
|
75
|
+
null,
|
|
76
|
+
visible.map((entry) =>
|
|
77
|
+
createElement(
|
|
78
|
+
PresenceContext.Provider,
|
|
79
|
+
{
|
|
80
|
+
key: entry.key,
|
|
81
|
+
value: {
|
|
82
|
+
isPresent: !entry.exiting,
|
|
83
|
+
initial: initial || !bornAtMount.current.has(entry.key),
|
|
84
|
+
safeToRemove: () => setShown((previous) => previous.filter((e) => e.key !== entry.key || !e.exiting)),
|
|
85
|
+
},
|
|
86
|
+
},
|
|
87
|
+
fresh.get(entry.key) ?? entry.node,
|
|
88
|
+
),
|
|
89
|
+
),
|
|
90
|
+
);
|
|
91
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import type { CSSProperties } from 'react';
|
|
2
|
+
import type { DurationToken, EaseToken } from '../tokens/motion-scale';
|
|
3
|
+
|
|
4
|
+
export type TimingDirection = 'open' | 'close';
|
|
5
|
+
/** One token for both directions, or split per direction; an omitted direction keeps its default. */
|
|
6
|
+
export type DirectionalToken<Token extends string> = Token | { [D in TimingDirection]?: Token };
|
|
7
|
+
|
|
8
|
+
/** Timing of an open/close transition, constrained to the motion token vocabulary -
|
|
9
|
+
* see DurationToken / EaseToken for what each token means. */
|
|
10
|
+
export interface AnimationTiming {
|
|
11
|
+
/** Duration token(s); an omitted field or direction keeps the component's default. */
|
|
12
|
+
duration?: DirectionalToken<DurationToken>;
|
|
13
|
+
/** Ease token(s); an omitted field or direction keeps the component's default. */
|
|
14
|
+
ease?: DirectionalToken<EaseToken>;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/** The `animation` prop shape: tokens to retime, omit for the component's defaults, or
|
|
18
|
+
* `null` to disable the transition entirely (it plays instantly). */
|
|
19
|
+
export type DisableableAnimation = AnimationTiming | null;
|
|
20
|
+
|
|
21
|
+
/** Extend this to expose the timing API on a component; redeclare `animation`
|
|
22
|
+
* to document the component's own default tokens. */
|
|
23
|
+
export interface TimingProps {
|
|
24
|
+
/** Transition timing - motion tokens only, each field a single token or { open, close };
|
|
25
|
+
* `null` disables the transition (it plays instantly). */
|
|
26
|
+
animation?: DisableableAnimation;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function resolveDirection<Token extends string>(
|
|
30
|
+
timing: DirectionalToken<Token> | undefined,
|
|
31
|
+
dir: TimingDirection,
|
|
32
|
+
): Token | undefined {
|
|
33
|
+
return typeof timing === 'object' ? timing[dir] : timing;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export function timingVars(prefix: string, animation: DisableableAnimation | undefined): CSSProperties | undefined {
|
|
37
|
+
if (animation === undefined) return undefined;
|
|
38
|
+
if (animation === null) {
|
|
39
|
+
return { [`--${prefix}-dur-open`]: '0ms', [`--${prefix}-dur-close`]: '0ms' } as CSSProperties;
|
|
40
|
+
}
|
|
41
|
+
const vars: Record<string, string> = {};
|
|
42
|
+
for (const dir of ['open', 'close'] as const) {
|
|
43
|
+
const d = resolveDirection(animation.duration, dir);
|
|
44
|
+
const e = resolveDirection(animation.ease, dir);
|
|
45
|
+
if (d) vars[`--${prefix}-dur-${dir}`] = `var(--duration-${d})`;
|
|
46
|
+
if (e) vars[`--${prefix}-ease-${dir}`] = `var(--ease-${e})`;
|
|
47
|
+
}
|
|
48
|
+
return Object.keys(vars).length ? (vars as CSSProperties) : undefined;
|
|
49
|
+
}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { useLayoutEffect, useRef, type RefObject } from 'react';
|
|
4
|
+
import { animate, type Layer, type Playback } from '../engine';
|
|
5
|
+
import { usePresence } from './presence-context';
|
|
6
|
+
|
|
7
|
+
export type Plays = Playback | Playback[] | null | undefined | void;
|
|
8
|
+
export type MotionSpec = Layer | Layer[] | ((el: HTMLElement) => Plays);
|
|
9
|
+
|
|
10
|
+
const toList = (plays: Plays): Playback[] => (!plays ? [] : Array.isArray(plays) ? plays : [plays]);
|
|
11
|
+
|
|
12
|
+
export function run(spec: MotionSpec | undefined, el: HTMLElement): Playback[] {
|
|
13
|
+
if (!spec) return [];
|
|
14
|
+
if (typeof spec === 'function') return toList(spec(el));
|
|
15
|
+
return [Array.isArray(spec) ? animate(el, ...spec) : animate(el, spec)];
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const allSettled = (plays: Playback[]): Promise<void> =>
|
|
19
|
+
plays.length ? Promise.all(plays.map((play) => play.finished)).then((): void => {}) : Promise.resolve();
|
|
20
|
+
|
|
21
|
+
export interface MotionSpecs {
|
|
22
|
+
/** Plays on mount, and again whenever `deps` change. */
|
|
23
|
+
animate?: MotionSpec;
|
|
24
|
+
/** Plays when a parent `<Presence>` drops this child; it stays mounted until this finishes. */
|
|
25
|
+
exit?: MotionSpec;
|
|
26
|
+
/** Play `animate` on mount. Falls back to the parent Presence's own `initial`. @default true */
|
|
27
|
+
initial?: boolean;
|
|
28
|
+
/** Replay `animate` when these change. Must keep a stable length. @default [] */
|
|
29
|
+
deps?: unknown[];
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/** Drives `<Motion>`; use it directly on elements you hold a ref to but do not render. */
|
|
33
|
+
export function useMotion(ref: RefObject<HTMLElement | null>, { animate, exit, initial, deps }: MotionSpecs): void {
|
|
34
|
+
const presence = usePresence();
|
|
35
|
+
const playing = useRef<Playback[]>([]);
|
|
36
|
+
const mounted = useRef(false);
|
|
37
|
+
const spec = useRef({ animate, exit, initial });
|
|
38
|
+
spec.current = { animate, exit, initial };
|
|
39
|
+
|
|
40
|
+
const stop = () => {
|
|
41
|
+
for (const play of playing.current) play.stop();
|
|
42
|
+
playing.current = [];
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
useLayoutEffect(() => {
|
|
46
|
+
const el = ref.current;
|
|
47
|
+
const atMount = !mounted.current;
|
|
48
|
+
mounted.current = true;
|
|
49
|
+
if (!el) return;
|
|
50
|
+
if (atMount && (spec.current.initial ?? presence.initial) === false) return;
|
|
51
|
+
stop();
|
|
52
|
+
playing.current = run(spec.current.animate, el);
|
|
53
|
+
}, deps ?? []); // eslint-disable-line react-hooks/exhaustive-deps
|
|
54
|
+
|
|
55
|
+
useLayoutEffect(() => {
|
|
56
|
+
if (presence.isPresent) return;
|
|
57
|
+
const el = ref.current;
|
|
58
|
+
stop();
|
|
59
|
+
playing.current = el ? run(spec.current.exit, el) : [];
|
|
60
|
+
allSettled(playing.current).then(presence.safeToRemove);
|
|
61
|
+
}, [presence.isPresent]); // eslint-disable-line react-hooks/exhaustive-deps
|
|
62
|
+
|
|
63
|
+
useLayoutEffect(() => stop, []);
|
|
64
|
+
}
|
package/src/styles.css
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/* Zyncat UI base layer - import ONCE at your app root: import '@zyncat/ui/styles.css';
|
|
2
|
+
Fonts, design tokens (:root variables) and the shared `glass` utility.
|
|
3
|
+
|
|
4
|
+
Per-component CSS is NOT here on purpose: every component imports its own
|
|
5
|
+
stylesheet, so the consumer's bundler code-splits and lazy-loads it with the
|
|
6
|
+
component. This file is only the always-needed global layer.
|
|
7
|
+
Order matters: fonts - primitives - semantics - shared utilities. */
|
|
8
|
+
|
|
9
|
+
@import './tokens/fonts.css';
|
|
10
|
+
|
|
11
|
+
@import './tokens/color.css';
|
|
12
|
+
@import './tokens/typography.css';
|
|
13
|
+
@import './tokens/spacing.css';
|
|
14
|
+
@import './tokens/radius.css';
|
|
15
|
+
@import './tokens/elevation.css';
|
|
16
|
+
@import './tokens/motion.css';
|
|
17
|
+
@import './tokens/glass.css';
|
|
18
|
+
@import './tokens/icons.css';
|
|
19
|
+
@import './tokens/layers.css';
|
|
20
|
+
@import './tokens/avatar.css';
|
|
21
|
+
|
|
22
|
+
@import './tokens/semantic.css';
|
|
23
|
+
|
|
24
|
+
/* Cross-cutting frosted-surface utility shared by several unrelated components
|
|
25
|
+
(badge, toast, select, icon); kept global instead of duplicated per component. */
|
|
26
|
+
@import './components/internal/glass/glass.css';
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/* Avatar tokens - reserved identity palette + lit-from-above face finish. */
|
|
2
|
+
|
|
3
|
+
:root {
|
|
4
|
+
--avatar-1-bg: oklch(0.945 0.04 250);
|
|
5
|
+
--avatar-1-fg: oklch(0.45 0.08 250);
|
|
6
|
+
--avatar-2-bg: oklch(0.945 0.04 292);
|
|
7
|
+
--avatar-2-fg: oklch(0.45 0.08 292);
|
|
8
|
+
--avatar-3-bg: oklch(0.945 0.04 334);
|
|
9
|
+
--avatar-3-fg: oklch(0.45 0.08 334);
|
|
10
|
+
--avatar-4-bg: oklch(0.945 0.04 5);
|
|
11
|
+
--avatar-4-fg: oklch(0.45 0.08 5);
|
|
12
|
+
--avatar-5-bg: oklch(0.945 0.04 55);
|
|
13
|
+
--avatar-5-fg: oklch(0.45 0.08 55);
|
|
14
|
+
--avatar-6-bg: oklch(0.945 0.04 125);
|
|
15
|
+
--avatar-6-fg: oklch(0.45 0.08 125);
|
|
16
|
+
|
|
17
|
+
/* Surface finish - white sheen from top, cool near-black shade from bottom (never pure black). */
|
|
18
|
+
--avatar-sheen: oklch(1 0 0 / 0.35);
|
|
19
|
+
--avatar-shade: oklch(0.225 0.012 264 / 0.05);
|
|
20
|
+
}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
/* Color primitives - the closed neutral + status ramps. */
|
|
2
|
+
|
|
3
|
+
:root {
|
|
4
|
+
--gray-0: oklch(1 0 0);
|
|
5
|
+
--gray-25: oklch(0.992 0.0015 264);
|
|
6
|
+
--gray-50: oklch(0.984 0.003 264);
|
|
7
|
+
--gray-100: oklch(0.968 0.004 264);
|
|
8
|
+
--gray-150: oklch(0.948 0.005 264);
|
|
9
|
+
--gray-200: oklch(0.922 0.006 264);
|
|
10
|
+
--gray-300: oklch(0.872 0.008 264);
|
|
11
|
+
--gray-400: oklch(0.765 0.01 264);
|
|
12
|
+
--gray-500: oklch(0.642 0.012 264);
|
|
13
|
+
--gray-600: oklch(0.532 0.013 264);
|
|
14
|
+
--gray-700: oklch(0.422 0.013 264);
|
|
15
|
+
--gray-800: oklch(0.305 0.012 264);
|
|
16
|
+
--gray-900: oklch(0.225 0.01 264);
|
|
17
|
+
--gray-950: oklch(0.165 0.008 264);
|
|
18
|
+
|
|
19
|
+
--teal-50: oklch(0.972 0.02 198);
|
|
20
|
+
--teal-100: oklch(0.935 0.04 198);
|
|
21
|
+
--teal-200: oklch(0.88 0.066 198);
|
|
22
|
+
--teal-300: oklch(0.795 0.092 198);
|
|
23
|
+
--teal-400: oklch(0.705 0.112 198);
|
|
24
|
+
--teal-500: oklch(0.63 0.118 198);
|
|
25
|
+
--teal-600: oklch(0.56 0.114 198);
|
|
26
|
+
--teal-700: oklch(0.478 0.1 198);
|
|
27
|
+
--teal-800: oklch(0.4 0.082 198);
|
|
28
|
+
--teal-900: oklch(0.32 0.062 198);
|
|
29
|
+
|
|
30
|
+
--green-50: oklch(0.965 0.028 152);
|
|
31
|
+
--green-500: oklch(0.62 0.13 152);
|
|
32
|
+
--green-600: oklch(0.548 0.122 152);
|
|
33
|
+
--green-700: oklch(0.462 0.1 152);
|
|
34
|
+
|
|
35
|
+
--amber-50: oklch(0.972 0.034 75);
|
|
36
|
+
--amber-500: oklch(0.76 0.14 75);
|
|
37
|
+
--amber-600: oklch(0.7 0.142 75);
|
|
38
|
+
--amber-700: oklch(0.56 0.118 75);
|
|
39
|
+
|
|
40
|
+
--red-50: oklch(0.968 0.02 27);
|
|
41
|
+
--red-300: oklch(0.8 0.09 27);
|
|
42
|
+
--red-400: oklch(0.64 0.2 27);
|
|
43
|
+
--red-500: oklch(0.602 0.196 27);
|
|
44
|
+
--red-600: oklch(0.545 0.196 27);
|
|
45
|
+
--red-700: oklch(0.478 0.172 27);
|
|
46
|
+
|
|
47
|
+
/* Wash tints - glassy active/selected fills that let the surface show through; low alpha per hue.
|
|
48
|
+
-strong is the press step of the gray wash (hover 6% < press 10%). */
|
|
49
|
+
--gray-wash: oklch(0.42 0.012 264 / 0.06);
|
|
50
|
+
--gray-wash-strong: oklch(0.42 0.012 264 / 0.1);
|
|
51
|
+
--teal-wash: oklch(0.63 0.118 198 / 0.05);
|
|
52
|
+
--green-wash: oklch(0.62 0.13 152 / 0.1);
|
|
53
|
+
--amber-wash: oklch(0.76 0.14 75 / 0.12);
|
|
54
|
+
--red-wash: oklch(0.602 0.196 27 / 0.1);
|
|
55
|
+
|
|
56
|
+
/* Cool near-black shadow ink - pure black looks muddy on a cool canvas. */
|
|
57
|
+
--shadow-rgb: 13 18 30;
|
|
58
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/* Elevation - two-layer shadows, focus/selection rings, border widths. */
|
|
2
|
+
|
|
3
|
+
:root {
|
|
4
|
+
--shadow-xs: 0 1px 1px rgb(var(--shadow-rgb) / 0.04);
|
|
5
|
+
--shadow-sm: 0 1px 2px rgb(var(--shadow-rgb) / 0.05), 0 1px 1px rgb(var(--shadow-rgb) / 0.04);
|
|
6
|
+
--shadow-md: 0 2px 4px rgb(var(--shadow-rgb) / 0.04), 0 6px 12px rgb(var(--shadow-rgb) / 0.07);
|
|
7
|
+
--shadow-lg: 0 4px 8px rgb(var(--shadow-rgb) / 0.04), 0 12px 28px rgb(var(--shadow-rgb) / 0.1);
|
|
8
|
+
--shadow-xl: 0 8px 16px rgb(var(--shadow-rgb) / 0.06), 0 24px 48px rgb(var(--shadow-rgb) / 0.14);
|
|
9
|
+
|
|
10
|
+
/* Borders - one hairline (1px); weight comes from the border color, not thickness; 2px for focus/selected. */
|
|
11
|
+
--border-hairline: 1px;
|
|
12
|
+
--border-emphasis: 1.5px;
|
|
13
|
+
|
|
14
|
+
/* Focus & selection rings - box-shadows (not outlines) so they follow border-radius. */
|
|
15
|
+
|
|
16
|
+
/* Derive from --accent so the ring tracks the active accent and theme, never a fixed hue. */
|
|
17
|
+
--ring-accent: 0 0 0 3px color-mix(in oklab, var(--accent) 32%, transparent);
|
|
18
|
+
--ring-danger: 0 0 0 3px oklch(0.602 0.196 27 / 0.3);
|
|
19
|
+
--ring-warning: 0 0 0 3px color-mix(in oklab, var(--warning) 30%, transparent);
|
|
20
|
+
--ring-success: 0 0 0 3px color-mix(in oklab, var(--success) 30%, transparent);
|
|
21
|
+
--ring-offset: 0 0 0 2px var(--bg-surface), 0 0 0 4px color-mix(in oklab, var(--accent) 40%, transparent);
|
|
22
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/* Glass tokens - structural frosted-surface pieces; per-tone tints live in semantic.css. */
|
|
2
|
+
|
|
3
|
+
:root {
|
|
4
|
+
--glass-blur: 9px;
|
|
5
|
+
--glass-blur-strong: 16px;
|
|
6
|
+
--glass-saturate: 1.5;
|
|
7
|
+
|
|
8
|
+
--glass-sheen: linear-gradient(
|
|
9
|
+
145deg,
|
|
10
|
+
rgb(255 255 255 / 0.55) 0%,
|
|
11
|
+
rgb(255 255 255 / 0.1) 24%,
|
|
12
|
+
rgb(255 255 255 / 0) 46%
|
|
13
|
+
);
|
|
14
|
+
--glass-sheen-rest: 0.8;
|
|
15
|
+
--glass-sheen-hover: 1;
|
|
16
|
+
|
|
17
|
+
--glass-highlight: inset 0 1px 0 0 rgb(255 255 255 / 0.55);
|
|
18
|
+
--glass-shadow: 0 1px 2px rgb(var(--shadow-rgb) / 0.05), 0 2px 5px rgb(var(--shadow-rgb) / 0.05);
|
|
19
|
+
--glass-shadow-hover: 0 2px 4px rgb(var(--shadow-rgb) / 0.06), 0 8px 18px rgb(var(--shadow-rgb) / 0.1);
|
|
20
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/** Names of the UI-transition duration scale (`--duration-*`):
|
|
2
|
+
* - `fast` 140ms - micro feedback: hover washes, presses, control tint/border/shadow.
|
|
3
|
+
* - `base` 200ms - standard element transitions: fades, toggles, badge morphs.
|
|
4
|
+
* - `slow` 300ms - layout-scale movement: Collapse, panel resizes, reordering.
|
|
5
|
+
* - `slower` 450ms - large-surface movement: dialogs, sheets, page-scale reveals.
|
|
6
|
+
* - `slowest` 900ms - hero-scale movement: card expansion, container transforms. The scale's
|
|
7
|
+
* ceiling: anything longer is choreography and belongs to Motion, not a transition token. */
|
|
8
|
+
export type DurationToken = 'fast' | 'base' | 'slow' | 'slower' | 'slowest';
|
|
9
|
+
|
|
10
|
+
/** Names of the brand easing curves (`--ease-*`):
|
|
11
|
+
* - `standard` cubic-bezier(0.2, 0, 0, 1) - brisk start, soft landing; the default for state changes.
|
|
12
|
+
* - `entrance` cubic-bezier(0.16, 1, 0.3, 1) - hard deceleration; content arriving on screen.
|
|
13
|
+
* - `exit` cubic-bezier(0.4, 0, 1, 1) - accelerates away; content leaving (pair with a faster duration).
|
|
14
|
+
* - `spring` cubic-bezier(0.34, 1.4, 0.5, 1) - overshoots then settles; playful confirmations.
|
|
15
|
+
* - `glide` cubic-bezier(0.55, 0, 0.15, 1) - symmetric in-out travel; a persistent element moving
|
|
16
|
+
* between positions, never enter/exit. */
|
|
17
|
+
export type EaseToken = 'standard' | 'entrance' | 'exit' | 'spring' | 'glide';
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
export interface MotionTransition {
|
|
2
|
+
type?: 'spring' | 'tween' | 'inertia' | 'keyframes';
|
|
3
|
+
duration?: number;
|
|
4
|
+
ease?:
|
|
5
|
+
| Bezier
|
|
6
|
+
| 'linear'
|
|
7
|
+
| 'easeIn'
|
|
8
|
+
| 'easeOut'
|
|
9
|
+
| 'easeInOut'
|
|
10
|
+
| 'circIn'
|
|
11
|
+
| 'circOut'
|
|
12
|
+
| 'circInOut'
|
|
13
|
+
| 'backIn'
|
|
14
|
+
| 'backOut'
|
|
15
|
+
| 'backInOut'
|
|
16
|
+
| 'anticipate';
|
|
17
|
+
bounce?: number;
|
|
18
|
+
visualDuration?: number;
|
|
19
|
+
delay?: number;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export type Bezier = [number, number, number, number];
|
|
23
|
+
|
|
24
|
+
import type { DurationToken, EaseToken } from './motion-scale';
|
|
25
|
+
export type { DurationToken, EaseToken } from './motion-scale';
|
|
26
|
+
|
|
27
|
+
export interface MotionTokens {
|
|
28
|
+
/** seconds - fast 0.14 - base 0.2 - slow 0.3 - slower 0.45 - slowest 0.9 */
|
|
29
|
+
dur: Record<DurationToken, number>;
|
|
30
|
+
ease: Record<EaseToken, Bezier>;
|
|
31
|
+
/** ready-made Motion transitions */
|
|
32
|
+
t: { enter: MotionTransition; exit: MotionTransition; layout: MotionTransition; settle: MotionTransition };
|
|
33
|
+
reduced: boolean;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const DEFAULT_DUR: MotionTokens['dur'] = { fast: 0.14, base: 0.2, slow: 0.3, slower: 0.45, slowest: 0.9 };
|
|
37
|
+
const DEFAULT_EASE: MotionTokens['ease'] = {
|
|
38
|
+
standard: [0.2, 0, 0, 1],
|
|
39
|
+
entrance: [0.16, 1, 0.3, 1],
|
|
40
|
+
exit: [0.4, 0, 1, 1],
|
|
41
|
+
spring: [0.34, 1.4, 0.5, 1],
|
|
42
|
+
glide: [0.55, 0, 0.15, 1],
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
function build(dur: MotionTokens['dur'], ease: MotionTokens['ease']): MotionTokens {
|
|
46
|
+
const reduced = dur.base <= 0.005;
|
|
47
|
+
return {
|
|
48
|
+
dur,
|
|
49
|
+
ease,
|
|
50
|
+
reduced,
|
|
51
|
+
t: {
|
|
52
|
+
enter: { duration: dur.base, ease: ease.entrance },
|
|
53
|
+
exit: { duration: dur.fast, ease: ease.exit },
|
|
54
|
+
layout: { duration: dur.slow, ease: ease.entrance },
|
|
55
|
+
settle: reduced ? { duration: 0 } : { type: 'spring', visualDuration: dur.fast, bounce: 0.25 },
|
|
56
|
+
},
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function readFromDom(): MotionTokens {
|
|
61
|
+
const cs = getComputedStyle(document.documentElement);
|
|
62
|
+
const seconds = (name: string, fallback: number): number => {
|
|
63
|
+
const v = cs.getPropertyValue(name).trim();
|
|
64
|
+
const n = v.slice(-2) === 'ms' ? parseFloat(v) / 1000 : parseFloat(v);
|
|
65
|
+
return isNaN(n) ? fallback : n;
|
|
66
|
+
};
|
|
67
|
+
const bezier = (name: string, fallback: Bezier): Bezier => {
|
|
68
|
+
const m = cs.getPropertyValue(name).match(/-?[\d.]+/g);
|
|
69
|
+
return m && m.length === 4 ? (m.map(Number) as Bezier) : fallback;
|
|
70
|
+
};
|
|
71
|
+
return build(
|
|
72
|
+
{
|
|
73
|
+
fast: seconds('--duration-fast', DEFAULT_DUR.fast),
|
|
74
|
+
base: seconds('--duration-base', DEFAULT_DUR.base),
|
|
75
|
+
slow: seconds('--duration-slow', DEFAULT_DUR.slow),
|
|
76
|
+
slower: seconds('--duration-slower', DEFAULT_DUR.slower),
|
|
77
|
+
slowest: seconds('--duration-slowest', DEFAULT_DUR.slowest),
|
|
78
|
+
},
|
|
79
|
+
{
|
|
80
|
+
standard: bezier('--ease-standard', DEFAULT_EASE.standard),
|
|
81
|
+
entrance: bezier('--ease-entrance', DEFAULT_EASE.entrance),
|
|
82
|
+
exit: bezier('--ease-exit', DEFAULT_EASE.exit),
|
|
83
|
+
spring: bezier('--ease-spring', DEFAULT_EASE.spring),
|
|
84
|
+
glide: bezier('--ease-glide', DEFAULT_EASE.glide),
|
|
85
|
+
},
|
|
86
|
+
);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
export const UIMotion: MotionTokens =
|
|
90
|
+
typeof document !== 'undefined' ? readFromDom() : build(DEFAULT_DUR, DEFAULT_EASE);
|
|
91
|
+
|
|
92
|
+
export default UIMotion;
|