beez-ui 0.5.6 → 0.6.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/CHANGELOG.md +4 -0
- package/README.md +175 -152
- package/dist/components/animated-theme-toggler.js +6 -4
- package/dist/components/checkbox.js +4 -3
- package/dist/components/data-table.js +16 -8
- package/dist/components/dialog.js +11 -8
- package/dist/components/dropdown-menu.js +27 -14
- package/dist/components/form.js +1 -1
- package/dist/components/radio-group.js +1 -1
- package/dist/components/select.js +29 -16
- package/dist/components/sidebar.js +11 -7
- package/dist/components/tabs.js +12 -8
- package/dist/components/tooltip.js +1 -1
- package/dist/constants/motion.d.ts +4 -0
- package/dist/constants/motion.js +4 -0
- package/dist/motion/glide-indicator.d.ts +66 -0
- package/dist/motion/glide-indicator.js +252 -0
- package/dist/motion/glide-slot.d.ts +9 -0
- package/dist/motion/glide-slot.js +71 -0
- package/dist/motion/motion-slot.js +506 -179
- package/dist/motion/surface-keyframes.d.ts +13 -0
- package/dist/motion/surface-keyframes.js +205 -0
- package/dist/motion/tokens.d.ts +89 -6
- package/dist/motion/tokens.js +107 -6
- package/dist/motion/use-attached-element.d.ts +7 -0
- package/dist/motion/use-attached-element.js +22 -0
- package/dist/styles.css +1 -1
- package/package.json +2 -2
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
import { c as _c } from "react/compiler-runtime";
|
|
3
3
|
import * as React from "react";
|
|
4
4
|
import { Select as SelectPrimitive } from "radix-ui";
|
|
5
|
+
import { GlideSlot } from "../motion/glide-slot.js";
|
|
5
6
|
import { MotionSlot } from "../motion/motion-slot.js";
|
|
6
7
|
import { cn } from "../lib/utils.js";
|
|
7
8
|
import { ChevronDownIcon, CheckIcon, ChevronUpIcon } from "lucide-react";
|
|
@@ -115,7 +116,7 @@ function SelectTrigger(t0) {
|
|
|
115
116
|
const size = t1 === undefined ? "default" : t1;
|
|
116
117
|
let t2;
|
|
117
118
|
if ($[5] !== className) {
|
|
118
|
-
t2 = cn("flex w-fit items-center justify-between gap-1.5 rounded-lg border border-input bg-transparent py-2 pr-2 pl-2.5 text-sm whitespace-nowrap transition-colors outline-none select-none focus-visible:border-ring focus-visible:ring-3 focus-visible:ring-ring/50 disabled:cursor-not-allowed disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-3 aria-invalid:ring-destructive/20 data-placeholder:text-muted-foreground data-[size=default]:h-8 data-[size=sm]:h-7 data-[size=sm]:rounded-[min(var(--radius-md),10px)] *:data-[slot=select-value]:line-clamp-1 *:data-[slot=select-value]:flex *:data-[slot=select-value]:items-center *:data-[slot=select-value]:gap-1.5 dark:bg-input/30 dark:hover:bg-input/50 dark:aria-invalid:border-destructive/50 dark:aria-invalid:ring-destructive/40 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4", className);
|
|
119
|
+
t2 = cn("flex w-fit items-center justify-between gap-1.5 rounded-lg border data-[state=open]:*:data-[slot=select-icon]:rotate-180 border-input bg-transparent py-2 pr-2 pl-2.5 text-sm whitespace-nowrap transition-colors outline-none select-none focus-visible:border-ring focus-visible:ring-3 focus-visible:ring-ring/50 disabled:cursor-not-allowed disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-3 aria-invalid:ring-destructive/20 data-placeholder:text-muted-foreground data-[size=default]:h-8 data-[size=sm]:h-7 data-[size=sm]:rounded-[min(var(--radius-md),10px)] *:data-[slot=select-value]:line-clamp-1 *:data-[slot=select-value]:flex *:data-[slot=select-value]:items-center *:data-[slot=select-value]:gap-1.5 dark:bg-input/30 dark:hover:bg-input/50 dark:aria-invalid:border-destructive/50 dark:aria-invalid:ring-destructive/40 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4", className);
|
|
119
120
|
$[5] = className;
|
|
120
121
|
$[6] = t2;
|
|
121
122
|
} else {
|
|
@@ -125,7 +126,13 @@ function SelectTrigger(t0) {
|
|
|
125
126
|
if ($[7] === Symbol.for("react.memo_cache_sentinel")) {
|
|
126
127
|
t3 = /* @__PURE__ */ _jsx(SelectPrimitive.Icon, {
|
|
127
128
|
asChild: true,
|
|
128
|
-
children: /* @__PURE__ */ _jsx(
|
|
129
|
+
children: /* @__PURE__ */ _jsx(MotionSlot, {
|
|
130
|
+
kind: "rotate",
|
|
131
|
+
children: /* @__PURE__ */ _jsx(ChevronDownIcon, {
|
|
132
|
+
"data-slot": "select-icon",
|
|
133
|
+
className: "pointer-events-none size-4 text-muted-foreground"
|
|
134
|
+
})
|
|
135
|
+
})
|
|
129
136
|
});
|
|
130
137
|
$[7] = t3;
|
|
131
138
|
} else {
|
|
@@ -234,18 +241,21 @@ function SelectContent(t0) {
|
|
|
234
241
|
if ($[17] !== align || $[18] !== position || $[19] !== props || $[20] !== t3 || $[21] !== t5 || $[22] !== t9) {
|
|
235
242
|
t11 = /* @__PURE__ */ _jsx(SelectPrimitive.Portal, { children: /* @__PURE__ */ _jsx(MotionSlot, {
|
|
236
243
|
kind: "surface",
|
|
237
|
-
children: /* @__PURE__ */
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
244
|
+
children: /* @__PURE__ */ _jsx(GlideSlot, {
|
|
245
|
+
preset: "menu",
|
|
246
|
+
children: /* @__PURE__ */ _jsxs(SelectPrimitive.Content, {
|
|
247
|
+
"data-slot": "select-content",
|
|
248
|
+
"data-align-trigger": t3,
|
|
249
|
+
className: t5,
|
|
250
|
+
position,
|
|
251
|
+
align,
|
|
252
|
+
...props,
|
|
253
|
+
children: [
|
|
254
|
+
t6,
|
|
255
|
+
t9,
|
|
256
|
+
t10
|
|
257
|
+
]
|
|
258
|
+
})
|
|
249
259
|
})
|
|
250
260
|
}) });
|
|
251
261
|
$[17] = align;
|
|
@@ -334,7 +344,10 @@ function SelectItem(t0) {
|
|
|
334
344
|
if ($[6] === Symbol.for("react.memo_cache_sentinel")) {
|
|
335
345
|
t2 = /* @__PURE__ */ _jsx("span", {
|
|
336
346
|
className: "pointer-events-none absolute right-2 flex size-4 items-center justify-center",
|
|
337
|
-
children: /* @__PURE__ */ _jsx(
|
|
347
|
+
children: /* @__PURE__ */ _jsx(MotionSlot, {
|
|
348
|
+
kind: "item-check",
|
|
349
|
+
children: /* @__PURE__ */ _jsx(SelectPrimitive.ItemIndicator, { children: /* @__PURE__ */ _jsx(CheckIcon, { className: "pointer-events-none" }) })
|
|
350
|
+
})
|
|
338
351
|
});
|
|
339
352
|
$[6] = t2;
|
|
340
353
|
} else {
|
|
@@ -351,7 +364,7 @@ function SelectItem(t0) {
|
|
|
351
364
|
let t4;
|
|
352
365
|
if ($[9] !== props || $[10] !== t1 || $[11] !== t3) {
|
|
353
366
|
t4 = /* @__PURE__ */ _jsx(MotionSlot, {
|
|
354
|
-
kind: "
|
|
367
|
+
kind: "list-item",
|
|
355
368
|
children: /* @__PURE__ */ _jsxs(SelectPrimitive.Item, {
|
|
356
369
|
"data-slot": "select-item",
|
|
357
370
|
className: t1,
|
|
@@ -5,6 +5,7 @@ import { cva } from "class-variance-authority";
|
|
|
5
5
|
import { Slot } from "radix-ui";
|
|
6
6
|
import { useIsMobile } from "../hooks/use-mobile.js";
|
|
7
7
|
import { useSidebarPersistence, saveSidebarPreference } from "../hooks/use-sidebar-persistence.js";
|
|
8
|
+
import { GlideSlot } from "../motion/glide-slot.js";
|
|
8
9
|
import { MotionSlot } from "../motion/motion-slot.js";
|
|
9
10
|
import { cn } from "../lib/utils.js";
|
|
10
11
|
import { SIDEBAR_COOKIE_MAX_AGE, SIDEBAR_COOKIE_NAME } from "../constants/sidebar.js";
|
|
@@ -937,11 +938,14 @@ function SidebarMenu(t0) {
|
|
|
937
938
|
}
|
|
938
939
|
let t2;
|
|
939
940
|
if ($[5] !== props || $[6] !== t1) {
|
|
940
|
-
t2 = /* @__PURE__ */ _jsx(
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
941
|
+
t2 = /* @__PURE__ */ _jsx(GlideSlot, {
|
|
942
|
+
preset: "sidebar",
|
|
943
|
+
children: /* @__PURE__ */ _jsx("ul", {
|
|
944
|
+
"data-slot": "sidebar-menu",
|
|
945
|
+
"data-sidebar": "menu",
|
|
946
|
+
className: t1,
|
|
947
|
+
...props
|
|
948
|
+
})
|
|
945
949
|
});
|
|
946
950
|
$[5] = props;
|
|
947
951
|
$[6] = t1;
|
|
@@ -1056,7 +1060,7 @@ function SidebarMenuButton(t0) {
|
|
|
1056
1060
|
let t6;
|
|
1057
1061
|
if ($[12] !== Comp || $[13] !== isActive || $[14] !== props || $[15] !== size || $[16] !== t5) {
|
|
1058
1062
|
t6 = /* @__PURE__ */ _jsx(MotionSlot, {
|
|
1059
|
-
kind: "press",
|
|
1063
|
+
kind: "subtle-press",
|
|
1060
1064
|
children: /* @__PURE__ */ _jsx(Comp, {
|
|
1061
1065
|
"data-slot": "sidebar-menu-button",
|
|
1062
1066
|
"data-sidebar": "menu-button",
|
|
@@ -1396,7 +1400,7 @@ function SidebarMenuSubButton(t0) {
|
|
|
1396
1400
|
let t5;
|
|
1397
1401
|
if ($[8] !== Comp || $[9] !== isActive || $[10] !== props || $[11] !== size || $[12] !== t4) {
|
|
1398
1402
|
t5 = /* @__PURE__ */ _jsx(MotionSlot, {
|
|
1399
|
-
kind: "press",
|
|
1403
|
+
kind: "subtle-press",
|
|
1400
1404
|
children: /* @__PURE__ */ _jsx(Comp, {
|
|
1401
1405
|
"data-slot": "sidebar-menu-sub-button",
|
|
1402
1406
|
"data-sidebar": "menu-sub-button",
|
package/dist/components/tabs.js
CHANGED
|
@@ -3,6 +3,7 @@ import { c as _c } from "react/compiler-runtime";
|
|
|
3
3
|
import * as React from "react";
|
|
4
4
|
import { cva } from "class-variance-authority";
|
|
5
5
|
import { Tabs as TabsPrimitive } from "radix-ui";
|
|
6
|
+
import { GlideSlot } from "../motion/glide-slot.js";
|
|
6
7
|
import { MotionSlot } from "../motion/motion-slot.js";
|
|
7
8
|
import { cn } from "../lib/utils.js";
|
|
8
9
|
import { jsx as _jsx } from "react/jsx-runtime";
|
|
@@ -83,11 +84,14 @@ function TabsList(t0) {
|
|
|
83
84
|
}
|
|
84
85
|
let t3;
|
|
85
86
|
if ($[7] !== props || $[8] !== t2 || $[9] !== variant) {
|
|
86
|
-
t3 = /* @__PURE__ */ _jsx(
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
87
|
+
t3 = /* @__PURE__ */ _jsx(GlideSlot, {
|
|
88
|
+
preset: "tabs",
|
|
89
|
+
children: /* @__PURE__ */ _jsx(TabsPrimitive.List, {
|
|
90
|
+
"data-slot": "tabs-list",
|
|
91
|
+
"data-variant": variant,
|
|
92
|
+
className: t2,
|
|
93
|
+
...props
|
|
94
|
+
})
|
|
91
95
|
});
|
|
92
96
|
$[7] = props;
|
|
93
97
|
$[8] = t2;
|
|
@@ -113,7 +117,7 @@ function TabsTrigger(t0) {
|
|
|
113
117
|
}
|
|
114
118
|
let t1;
|
|
115
119
|
if ($[3] !== className) {
|
|
116
|
-
t1 = cn("relative inline-flex h-[calc(100%-1px)] flex-1 items-center justify-center gap-1.5 rounded-md border border-transparent px-1.5 py-0.5 text-sm font-medium whitespace-nowrap text-foreground/60 transition-all group-data-vertical/tabs:w-full group-data-vertical/tabs:justify-start hover:text-foreground focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 focus-visible:outline-1 focus-visible:outline-ring disabled:pointer-events-none disabled:opacity-50 has-data-[icon=inline-end]:pr-1.5 has-data-[icon=inline-start]:pl-1.5 dark:text-muted-foreground dark:hover:text-foreground group-data-[variant=default]/tabs-list:data-active:shadow-sm group-data-[variant=line]/tabs-list:data-active:shadow-none [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4", "group-data-[variant=line]/tabs-list:bg-transparent group-data-[variant=line]/tabs-list:data-active:bg-transparent dark:group-data-[variant=line]/tabs-list:data-active:border-transparent dark:group-data-[variant=line]/tabs-list:data-active:bg-transparent", "data-active:bg-background data-active:text-foreground dark:data-active:border-input dark:data-active:bg-input/30 dark:data-active:text-foreground", "after:absolute after:bg-foreground after:opacity-0 after:transition-opacity group-data-horizontal/tabs:after:inset-x-0 group-data-horizontal/tabs:after:bottom-[-5px] group-data-horizontal/tabs:after:h-0.5 group-data-vertical/tabs:after:inset-y-0 group-data-vertical/tabs:after:-right-1 group-data-vertical/tabs:after:w-0.5 group-data-[variant=line]/tabs-list:data-active:after:opacity-100", className);
|
|
120
|
+
t1 = cn("relative inline-flex h-[calc(100%-1px)] flex-1 items-center justify-center gap-1.5 rounded-md border border-transparent px-1.5 py-0.5 text-sm font-medium whitespace-nowrap text-foreground/60 transition-all group-data-vertical/tabs:w-full group-data-vertical/tabs:justify-start hover:text-foreground focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 focus-visible:outline-1 focus-visible:outline-ring disabled:pointer-events-none disabled:opacity-50 has-data-[icon=inline-end]:pr-1.5 has-data-[icon=inline-start]:pl-1.5 dark:text-muted-foreground dark:hover:text-foreground group-data-[variant=default]/tabs-list:data-active:shadow-sm group-data-[variant=line]/tabs-list:data-active:shadow-none [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4", "group-data-[variant=line]/tabs-list:bg-transparent group-data-[variant=line]/tabs-list:data-active:bg-transparent dark:group-data-[variant=line]/tabs-list:data-active:border-transparent dark:group-data-[variant=line]/tabs-list:data-active:bg-transparent", "data-active:bg-background data-active:text-foreground dark:data-active:border-input dark:data-active:bg-input/30 dark:data-active:text-foreground", "after:absolute after:bg-foreground after:opacity-0 after:transition-opacity group-data-horizontal/tabs:after:inset-x-0 group-data-horizontal/tabs:after:bottom-[-5px] group-data-horizontal/tabs:after:h-0.5 group-data-vertical/tabs:after:inset-y-0 group-data-vertical/tabs:after:-right-1 group-data-vertical/tabs:after:w-0.5 group-data-[variant=line]/tabs-list:data-active:after:opacity-100 data-glide-target:after:opacity-0! data-glide-freeze:after:transition-none", className);
|
|
117
121
|
$[3] = className;
|
|
118
122
|
$[4] = t1;
|
|
119
123
|
} else {
|
|
@@ -122,7 +126,7 @@ function TabsTrigger(t0) {
|
|
|
122
126
|
let t2;
|
|
123
127
|
if ($[5] !== props || $[6] !== t1) {
|
|
124
128
|
t2 = /* @__PURE__ */ _jsx(MotionSlot, {
|
|
125
|
-
kind: "press",
|
|
129
|
+
kind: "subtle-press",
|
|
126
130
|
children: /* @__PURE__ */ _jsx(TabsPrimitive.Trigger, {
|
|
127
131
|
"data-slot": "tabs-trigger",
|
|
128
132
|
className: t1,
|
|
@@ -161,7 +165,7 @@ function TabsContent(t0) {
|
|
|
161
165
|
let t2;
|
|
162
166
|
if ($[5] !== props || $[6] !== t1) {
|
|
163
167
|
t2 = /* @__PURE__ */ _jsx(MotionSlot, {
|
|
164
|
-
kind: "
|
|
168
|
+
kind: "content",
|
|
165
169
|
children: /* @__PURE__ */ _jsx(TabsPrimitive.Content, {
|
|
166
170
|
"data-slot": "tabs-content",
|
|
167
171
|
className: t1,
|
|
@@ -138,7 +138,7 @@ function TooltipContent(t0) {
|
|
|
138
138
|
let t5;
|
|
139
139
|
if ($[8] !== children || $[9] !== props || $[10] !== sideOffset || $[11] !== t3) {
|
|
140
140
|
t5 = /* @__PURE__ */ _jsx(MotionSlot, {
|
|
141
|
-
kind: "
|
|
141
|
+
kind: "tooltip",
|
|
142
142
|
children: /* @__PURE__ */ _jsxs(TooltipPrimitive.Content, {
|
|
143
143
|
forceMount: true,
|
|
144
144
|
"data-slot": "tooltip-content",
|
|
@@ -1,2 +1,6 @@
|
|
|
1
1
|
/** Shares the operating-system preference across CSS-adjacent and JavaScript animations. */
|
|
2
2
|
export declare const REDUCED_MOTION_QUERY = "(prefers-reduced-motion: reduce)";
|
|
3
|
+
/** Only true hover pointers get hover lifts; touch devices keep phantom hovers after taps. */
|
|
4
|
+
export declare const HOVER_CAPABLE_QUERY = "(hover: hover) and (pointer: fine)";
|
|
5
|
+
/** Converts Motion's second-based timings to the millisecond timings of Web Animations. */
|
|
6
|
+
export declare const MILLISECONDS_PER_SECOND = 1000;
|
package/dist/constants/motion.js
CHANGED
|
@@ -1,2 +1,6 @@
|
|
|
1
1
|
/** Shares the operating-system preference across CSS-adjacent and JavaScript animations. */
|
|
2
2
|
export const REDUCED_MOTION_QUERY = "(prefers-reduced-motion: reduce)";
|
|
3
|
+
/** Only true hover pointers get hover lifts; touch devices keep phantom hovers after taps. */
|
|
4
|
+
export const HOVER_CAPABLE_QUERY = "(hover: hover) and (pointer: fine)";
|
|
5
|
+
/** Converts Motion's second-based timings to the millisecond timings of Web Animations. */
|
|
6
|
+
export const MILLISECONDS_PER_SECOND = 1e3;
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import { SPRING_LAYOUT, SPRING_TABS } from "./tokens.js";
|
|
2
|
+
/** Which part of the active item carries the highlight that must travel. */
|
|
3
|
+
type GlideSurface = "background" | "after";
|
|
4
|
+
/** Describes the items of one container and how their active state is exposed. */
|
|
5
|
+
export interface GlidePreset {
|
|
6
|
+
itemSelector: string;
|
|
7
|
+
/**
|
|
8
|
+
* Matches the container and any nested container of the same kind, so items that belong to a
|
|
9
|
+
* nested one (a submenu rendered inside its parent menu) are left to that container's glide.
|
|
10
|
+
*/
|
|
11
|
+
containerSelector: string;
|
|
12
|
+
/** Attributes whose mutations can change which item is active. */
|
|
13
|
+
attributes: string[];
|
|
14
|
+
isActive: (item: Element) => boolean;
|
|
15
|
+
surface: (container: HTMLElement) => GlideSurface;
|
|
16
|
+
spring: typeof SPRING_LAYOUT | typeof SPRING_TABS;
|
|
17
|
+
}
|
|
18
|
+
export declare const GLIDE_PRESETS: {
|
|
19
|
+
tabs: {
|
|
20
|
+
itemSelector: string;
|
|
21
|
+
containerSelector: string;
|
|
22
|
+
attributes: string[];
|
|
23
|
+
isActive: (item: Element) => boolean;
|
|
24
|
+
surface: (container: HTMLElement) => "after" | "background";
|
|
25
|
+
spring: {
|
|
26
|
+
readonly type: "spring";
|
|
27
|
+
readonly stiffness: 245;
|
|
28
|
+
readonly damping: 36;
|
|
29
|
+
readonly mass: 1.2;
|
|
30
|
+
};
|
|
31
|
+
};
|
|
32
|
+
sidebar: {
|
|
33
|
+
itemSelector: string;
|
|
34
|
+
containerSelector: string;
|
|
35
|
+
attributes: string[];
|
|
36
|
+
isActive: (item: Element) => boolean;
|
|
37
|
+
surface: () => "background";
|
|
38
|
+
spring: {
|
|
39
|
+
readonly type: "spring";
|
|
40
|
+
readonly stiffness: 360;
|
|
41
|
+
readonly damping: 32;
|
|
42
|
+
readonly mass: 0.6;
|
|
43
|
+
};
|
|
44
|
+
};
|
|
45
|
+
menu: {
|
|
46
|
+
itemSelector: string;
|
|
47
|
+
containerSelector: string;
|
|
48
|
+
attributes: string[];
|
|
49
|
+
isActive: (item: Element) => boolean;
|
|
50
|
+
surface: () => "background";
|
|
51
|
+
spring: {
|
|
52
|
+
readonly type: "spring";
|
|
53
|
+
readonly stiffness: 360;
|
|
54
|
+
readonly damping: 32;
|
|
55
|
+
readonly mass: 0.6;
|
|
56
|
+
};
|
|
57
|
+
};
|
|
58
|
+
};
|
|
59
|
+
export type GlidePresetName = keyof typeof GLIDE_PRESETS;
|
|
60
|
+
/** Marks the item whose own highlight is hidden while the travelling copy stands in for it. */
|
|
61
|
+
export declare const GLIDE_TARGET_ATTRIBUTE = "data-glide-target";
|
|
62
|
+
/** Marks items whose transitions, including pseudo-element ones, are suspended during a glide. */
|
|
63
|
+
export declare const GLIDE_FREEZE_ATTRIBUTE = "data-glide-freeze";
|
|
64
|
+
/** Observes the container and glides the highlight whenever the active item changes. */
|
|
65
|
+
export declare function attachGlideIndicator(container: HTMLElement, preset: GlidePreset): () => void;
|
|
66
|
+
export {};
|
|
@@ -0,0 +1,252 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Glides an active-item indicator between items of a container, like a shared `layoutId`,
|
|
3
|
+
* without adding persistent markup: a transient copy of the target's highlight travels from the
|
|
4
|
+
* previous item and is removed once it lands, leaving the item's own styles in charge again.
|
|
5
|
+
*/
|
|
6
|
+
import { animate } from "motion/react";
|
|
7
|
+
import { GLIDE_CONTINUITY_MS, SPRING_LAYOUT, SPRING_TABS } from "./tokens.js";
|
|
8
|
+
export const GLIDE_PRESETS = {
|
|
9
|
+
tabs: {
|
|
10
|
+
itemSelector: "[data-slot=\"tabs-trigger\"]",
|
|
11
|
+
containerSelector: "[role=\"tablist\"]",
|
|
12
|
+
attributes: ["data-state"],
|
|
13
|
+
isActive: (item) => item.getAttribute("data-state") === "active",
|
|
14
|
+
surface: (container) => container.getAttribute("data-variant") === "line" ? "after" : "background",
|
|
15
|
+
spring: SPRING_TABS
|
|
16
|
+
},
|
|
17
|
+
sidebar: {
|
|
18
|
+
itemSelector: "[data-slot=\"sidebar-menu-button\"]",
|
|
19
|
+
containerSelector: "[data-slot=\"sidebar-menu\"]",
|
|
20
|
+
attributes: ["data-active"],
|
|
21
|
+
isActive: (item) => item.getAttribute("data-active") === "true",
|
|
22
|
+
surface: () => "background",
|
|
23
|
+
spring: SPRING_LAYOUT
|
|
24
|
+
},
|
|
25
|
+
menu: {
|
|
26
|
+
itemSelector: "[role=\"menuitem\"],[role=\"menuitemcheckbox\"],[role=\"menuitemradio\"],[role=\"option\"]",
|
|
27
|
+
containerSelector: "[role=\"menu\"],[role=\"listbox\"]",
|
|
28
|
+
attributes: ["data-highlighted"],
|
|
29
|
+
isActive: (item) => item.hasAttribute("data-highlighted"),
|
|
30
|
+
surface: () => "background",
|
|
31
|
+
spring: SPRING_LAYOUT
|
|
32
|
+
}
|
|
33
|
+
};
|
|
34
|
+
/** Marks the item whose own highlight is hidden while the travelling copy stands in for it. */
|
|
35
|
+
export const GLIDE_TARGET_ATTRIBUTE = "data-glide-target";
|
|
36
|
+
/** Marks items whose transitions, including pseudo-element ones, are suspended during a glide. */
|
|
37
|
+
export const GLIDE_FREEZE_ATTRIBUTE = "data-glide-freeze";
|
|
38
|
+
/** Converts a viewport rectangle into the container's scrolling coordinate space. */
|
|
39
|
+
function toContainerBox(container, rect) {
|
|
40
|
+
const origin = container.getBoundingClientRect();
|
|
41
|
+
return {
|
|
42
|
+
left: rect.left - origin.left - container.clientLeft + container.scrollLeft,
|
|
43
|
+
top: rect.top - origin.top - container.clientTop + container.scrollTop,
|
|
44
|
+
width: rect.width,
|
|
45
|
+
height: rect.height
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
/** Translation an item's own running entrance adds, read from its computed `matrix()`. */
|
|
49
|
+
function inFlightTranslation(item) {
|
|
50
|
+
const values = /^matrix\(([^)]+)\)$/.exec(getComputedStyle(item).transform);
|
|
51
|
+
if (!values) return {
|
|
52
|
+
x: 0,
|
|
53
|
+
y: 0
|
|
54
|
+
};
|
|
55
|
+
const parts = values[1].split(",").map(Number.parseFloat);
|
|
56
|
+
return {
|
|
57
|
+
x: parts[4] ?? 0,
|
|
58
|
+
y: parts[5] ?? 0
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* Measures the highlight: the item box, or its positioned `::after` bar for line variants.
|
|
63
|
+
* An item still sliding into place (staggered Select options) is measured where it will
|
|
64
|
+
* settle, so the highlight does not land offset and jump when the entrance ends.
|
|
65
|
+
*/
|
|
66
|
+
function highlightBox(item, surface) {
|
|
67
|
+
const box = item.getBoundingClientRect();
|
|
68
|
+
const offset = inFlightTranslation(item);
|
|
69
|
+
const rect = {
|
|
70
|
+
left: box.left - offset.x,
|
|
71
|
+
top: box.top - offset.y,
|
|
72
|
+
width: box.width,
|
|
73
|
+
height: box.height
|
|
74
|
+
};
|
|
75
|
+
if (surface === "background") return rect;
|
|
76
|
+
const pseudo = getComputedStyle(item, "::after");
|
|
77
|
+
return {
|
|
78
|
+
left: rect.left + item.clientLeft + (parseFloat(pseudo.left) || 0),
|
|
79
|
+
top: rect.top + item.clientTop + (parseFloat(pseudo.top) || 0),
|
|
80
|
+
width: parseFloat(pseudo.width) || rect.width,
|
|
81
|
+
height: parseFloat(pseudo.height) || 0
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
/** Copies only the paint that identifies the highlight, never layout or content. */
|
|
85
|
+
function highlightPaint(item, surface) {
|
|
86
|
+
const style = getComputedStyle(item, surface === "after" ? "::after" : null);
|
|
87
|
+
return {
|
|
88
|
+
backgroundColor: style.backgroundColor,
|
|
89
|
+
borderRadius: style.borderRadius,
|
|
90
|
+
boxShadow: style.boxShadow,
|
|
91
|
+
borderStyle: style.borderStyle,
|
|
92
|
+
borderWidth: style.borderWidth,
|
|
93
|
+
borderColor: style.borderColor,
|
|
94
|
+
boxSizing: "border-box"
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
/** Tracks overlapping freezes so the caller-owned inline transition is restored exactly once. */
|
|
98
|
+
const frozenItems = new WeakMap();
|
|
99
|
+
/**
|
|
100
|
+
* Suppresses CSS transitions for one item, so state changes paint instantly; the returned
|
|
101
|
+
* release restores them after two painted frames. Overlapping freezes are reference-counted.
|
|
102
|
+
*/
|
|
103
|
+
function freezeTransitions(item) {
|
|
104
|
+
const frozen = frozenItems.get(item) ?? {
|
|
105
|
+
transition: item.style.transition,
|
|
106
|
+
holds: 0
|
|
107
|
+
};
|
|
108
|
+
frozen.holds += 1;
|
|
109
|
+
frozenItems.set(item, frozen);
|
|
110
|
+
item.style.transition = "none";
|
|
111
|
+
item.setAttribute(GLIDE_FREEZE_ATTRIBUTE, "");
|
|
112
|
+
let released = false;
|
|
113
|
+
return () => {
|
|
114
|
+
if (released) return;
|
|
115
|
+
released = true;
|
|
116
|
+
requestAnimationFrame(() => {
|
|
117
|
+
requestAnimationFrame(() => {
|
|
118
|
+
frozen.holds -= 1;
|
|
119
|
+
if (frozen.holds > 0) return;
|
|
120
|
+
frozenItems.delete(item);
|
|
121
|
+
item.style.transition = frozen.transition;
|
|
122
|
+
item.removeAttribute(GLIDE_FREEZE_ATTRIBUTE);
|
|
123
|
+
});
|
|
124
|
+
});
|
|
125
|
+
};
|
|
126
|
+
}
|
|
127
|
+
/** Observes the container and glides the highlight whenever the active item changes. */
|
|
128
|
+
export function attachGlideIndicator(container, preset) {
|
|
129
|
+
const items = () => Array.from(container.querySelectorAll(preset.itemSelector)).filter((item) => item.parentElement?.closest(preset.containerSelector) === container);
|
|
130
|
+
let lastActive = items().find(preset.isActive) ?? null;
|
|
131
|
+
let lastActiveSeenAt = Number.NEGATIVE_INFINITY;
|
|
132
|
+
let overlay = null;
|
|
133
|
+
let animation = null;
|
|
134
|
+
let releaseTarget = null;
|
|
135
|
+
const containerStyle = {
|
|
136
|
+
position: container.style.position,
|
|
137
|
+
isolation: container.style.isolation
|
|
138
|
+
};
|
|
139
|
+
/** Puts the item's own highlight back and removes the travelling copy. */
|
|
140
|
+
function settle() {
|
|
141
|
+
animation?.cancel();
|
|
142
|
+
animation = null;
|
|
143
|
+
overlay?.remove();
|
|
144
|
+
overlay = null;
|
|
145
|
+
releaseTarget?.();
|
|
146
|
+
releaseTarget = null;
|
|
147
|
+
container.style.position = containerStyle.position;
|
|
148
|
+
container.style.isolation = containerStyle.isolation;
|
|
149
|
+
}
|
|
150
|
+
/** Hides the frozen target's highlight so the copy is the only one visible. */
|
|
151
|
+
function hideTarget(item, surface, thaw) {
|
|
152
|
+
item.setAttribute(GLIDE_TARGET_ATTRIBUTE, "");
|
|
153
|
+
const inline = {
|
|
154
|
+
backgroundColor: item.style.getPropertyValue("background-color"),
|
|
155
|
+
backgroundPriority: item.style.getPropertyPriority("background-color"),
|
|
156
|
+
boxShadow: item.style.getPropertyValue("box-shadow"),
|
|
157
|
+
boxShadowPriority: item.style.getPropertyPriority("box-shadow"),
|
|
158
|
+
borderColor: item.style.getPropertyValue("border-color"),
|
|
159
|
+
borderPriority: item.style.getPropertyPriority("border-color")
|
|
160
|
+
};
|
|
161
|
+
if (surface === "background") {
|
|
162
|
+
item.style.setProperty("background-color", "transparent", "important");
|
|
163
|
+
item.style.setProperty("box-shadow", "none", "important");
|
|
164
|
+
item.style.setProperty("border-color", "transparent", "important");
|
|
165
|
+
}
|
|
166
|
+
return () => {
|
|
167
|
+
item.style.setProperty("background-color", inline.backgroundColor, inline.backgroundPriority);
|
|
168
|
+
item.style.setProperty("box-shadow", inline.boxShadow, inline.boxShadowPriority);
|
|
169
|
+
item.style.setProperty("border-color", inline.borderColor, inline.borderPriority);
|
|
170
|
+
// Transitions stay frozen until the restored highlight has painted in place.
|
|
171
|
+
item.removeAttribute(GLIDE_TARGET_ATTRIBUTE);
|
|
172
|
+
thaw();
|
|
173
|
+
};
|
|
174
|
+
}
|
|
175
|
+
/** Starts, or redirects, the glide from the previous highlight to the new active item. */
|
|
176
|
+
function glide(from, to) {
|
|
177
|
+
const surface = preset.surface(container);
|
|
178
|
+
const origin = overlay ? overlay.getBoundingClientRect() : highlightBox(from, surface);
|
|
179
|
+
settle();
|
|
180
|
+
// The previous item drops its highlight instantly: the copy is what leaves it.
|
|
181
|
+
freezeTransitions(from)();
|
|
182
|
+
// Read the target's settled paint before any transition has a chance to start.
|
|
183
|
+
const thawTarget = freezeTransitions(to);
|
|
184
|
+
const destination = toContainerBox(container, highlightBox(to, surface));
|
|
185
|
+
const paint = highlightPaint(to, surface);
|
|
186
|
+
if (destination.width === 0 || destination.height === 0) {
|
|
187
|
+
thawTarget();
|
|
188
|
+
return;
|
|
189
|
+
}
|
|
190
|
+
if (getComputedStyle(container).position === "static") {
|
|
191
|
+
container.style.position = "relative";
|
|
192
|
+
}
|
|
193
|
+
// Negative z-index keeps the copy above the container background and below item content.
|
|
194
|
+
container.style.isolation = "isolate";
|
|
195
|
+
overlay = document.createElement("span");
|
|
196
|
+
overlay.setAttribute("aria-hidden", "true");
|
|
197
|
+
overlay.setAttribute("data-glide-indicator", "");
|
|
198
|
+
Object.assign(overlay.style, paint, {
|
|
199
|
+
position: "absolute",
|
|
200
|
+
pointerEvents: "none",
|
|
201
|
+
zIndex: "-1",
|
|
202
|
+
margin: "0"
|
|
203
|
+
});
|
|
204
|
+
const start = toContainerBox(container, origin);
|
|
205
|
+
container.prepend(overlay);
|
|
206
|
+
releaseTarget = hideTarget(to, surface, thawTarget);
|
|
207
|
+
animation = animate(overlay, {
|
|
208
|
+
left: [`${start.left}px`, `${destination.left}px`],
|
|
209
|
+
top: [`${start.top}px`, `${destination.top}px`],
|
|
210
|
+
width: [`${start.width}px`, `${destination.width}px`],
|
|
211
|
+
height: [`${start.height}px`, `${destination.height}px`]
|
|
212
|
+
}, preset.spring);
|
|
213
|
+
const current = animation;
|
|
214
|
+
void current.then(() => {
|
|
215
|
+
if (animation === current) settle();
|
|
216
|
+
});
|
|
217
|
+
}
|
|
218
|
+
const observer = new MutationObserver(() => {
|
|
219
|
+
// Items can be remounted (a sidebar button wrapped in a tooltip after hydration); the
|
|
220
|
+
// highlight then belongs to the item that replaced the detached one.
|
|
221
|
+
if (lastActive && !lastActive.isConnected) {
|
|
222
|
+
lastActive = items().find(preset.isActive) ?? null;
|
|
223
|
+
lastActiveSeenAt = Number.NEGATIVE_INFINITY;
|
|
224
|
+
}
|
|
225
|
+
const active = items().find(preset.isActive) ?? null;
|
|
226
|
+
const now = performance.now();
|
|
227
|
+
if (active && active === lastActive) return;
|
|
228
|
+
if (!active) {
|
|
229
|
+
if (lastActive && lastActiveSeenAt === Number.NEGATIVE_INFINITY) {
|
|
230
|
+
lastActiveSeenAt = now;
|
|
231
|
+
}
|
|
232
|
+
settle();
|
|
233
|
+
return;
|
|
234
|
+
}
|
|
235
|
+
const previous = lastActive;
|
|
236
|
+
const isContinuous = previous !== null && previous !== active && previous.isConnected && !preset.isActive(previous) && (lastActiveSeenAt === Number.NEGATIVE_INFINITY || now - lastActiveSeenAt <= GLIDE_CONTINUITY_MS);
|
|
237
|
+
lastActive = active;
|
|
238
|
+
lastActiveSeenAt = Number.NEGATIVE_INFINITY;
|
|
239
|
+
if (isContinuous) glide(previous, active);
|
|
240
|
+
else settle();
|
|
241
|
+
});
|
|
242
|
+
observer.observe(container, {
|
|
243
|
+
subtree: true,
|
|
244
|
+
childList: true,
|
|
245
|
+
attributes: true,
|
|
246
|
+
attributeFilter: preset.attributes
|
|
247
|
+
});
|
|
248
|
+
return () => {
|
|
249
|
+
observer.disconnect();
|
|
250
|
+
settle();
|
|
251
|
+
};
|
|
252
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/** Adds a gliding active-item indicator to the original container, without DOM wrappers. */
|
|
2
|
+
import { type ComponentProps, type ReactElement } from "react";
|
|
3
|
+
import { Slot } from "radix-ui";
|
|
4
|
+
import { type GlidePresetName } from "./glide-indicator.js";
|
|
5
|
+
/** Composes refs through Radix Slot and glides the preset's highlight between its items. */
|
|
6
|
+
export declare function GlideSlot({ children, preset, ref, ...props }: Omit<ComponentProps<typeof Slot.Root>, "children"> & {
|
|
7
|
+
children: ReactElement;
|
|
8
|
+
preset: GlidePresetName;
|
|
9
|
+
}): import("react").JSX.Element;
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { c as _c } from "react/compiler-runtime";
|
|
3
|
+
/** Adds a gliding active-item indicator to the original container, without DOM wrappers. */
|
|
4
|
+
import { useLayoutEffect } from "react";
|
|
5
|
+
import { Slot } from "radix-ui";
|
|
6
|
+
import { usePrefersReducedMotion } from "../hooks/use-prefers-reduced-motion.js";
|
|
7
|
+
import { attachGlideIndicator, GLIDE_PRESETS } from "./glide-indicator.js";
|
|
8
|
+
import { useAttachedElement } from "./use-attached-element.js";
|
|
9
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
10
|
+
/** Composes refs through Radix Slot and glides the preset's highlight between its items. */
|
|
11
|
+
export function GlideSlot(t0) {
|
|
12
|
+
const $ = _c(14);
|
|
13
|
+
let children;
|
|
14
|
+
let preset;
|
|
15
|
+
let props;
|
|
16
|
+
let ref;
|
|
17
|
+
if ($[0] !== t0) {
|
|
18
|
+
({children, preset, ref, ...props} = t0);
|
|
19
|
+
$[0] = t0;
|
|
20
|
+
$[1] = children;
|
|
21
|
+
$[2] = preset;
|
|
22
|
+
$[3] = props;
|
|
23
|
+
$[4] = ref;
|
|
24
|
+
} else {
|
|
25
|
+
children = $[1];
|
|
26
|
+
preset = $[2];
|
|
27
|
+
props = $[3];
|
|
28
|
+
ref = $[4];
|
|
29
|
+
}
|
|
30
|
+
const [element, attach] = useAttachedElement(ref);
|
|
31
|
+
const reduceMotion = usePrefersReducedMotion();
|
|
32
|
+
let t1;
|
|
33
|
+
let t2;
|
|
34
|
+
if ($[5] !== element || $[6] !== preset || $[7] !== reduceMotion) {
|
|
35
|
+
t1 = () => {
|
|
36
|
+
if (!element || reduceMotion) {
|
|
37
|
+
return;
|
|
38
|
+
}
|
|
39
|
+
return attachGlideIndicator(element, GLIDE_PRESETS[preset]);
|
|
40
|
+
};
|
|
41
|
+
t2 = [
|
|
42
|
+
element,
|
|
43
|
+
preset,
|
|
44
|
+
reduceMotion
|
|
45
|
+
];
|
|
46
|
+
$[5] = element;
|
|
47
|
+
$[6] = preset;
|
|
48
|
+
$[7] = reduceMotion;
|
|
49
|
+
$[8] = t1;
|
|
50
|
+
$[9] = t2;
|
|
51
|
+
} else {
|
|
52
|
+
t1 = $[8];
|
|
53
|
+
t2 = $[9];
|
|
54
|
+
}
|
|
55
|
+
useLayoutEffect(t1, t2);
|
|
56
|
+
let t3;
|
|
57
|
+
if ($[10] !== attach || $[11] !== children || $[12] !== props) {
|
|
58
|
+
t3 = /* @__PURE__ */ _jsx(Slot.Root, {
|
|
59
|
+
...props,
|
|
60
|
+
ref: attach,
|
|
61
|
+
children
|
|
62
|
+
});
|
|
63
|
+
$[10] = attach;
|
|
64
|
+
$[11] = children;
|
|
65
|
+
$[12] = props;
|
|
66
|
+
$[13] = t3;
|
|
67
|
+
} else {
|
|
68
|
+
t3 = $[13];
|
|
69
|
+
}
|
|
70
|
+
return t3;
|
|
71
|
+
}
|