@waveso/ui 0.15.1 → 0.16.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 +13 -12
- package/dist/accordion.js +2 -2
- package/dist/action-bar.js +4 -4
- package/dist/alert-dialog.js +5 -5
- package/dist/alert.d.ts +8 -1
- package/dist/alert.js +13 -4
- package/dist/animate.d.ts +1 -1
- package/dist/animate.js +2 -9
- package/dist/autocomplete.js +3 -3
- package/dist/avatar.d.ts +1 -1
- package/dist/avatar.js +4 -4
- package/dist/badge.d.ts +1 -1
- package/dist/badge.js +6 -6
- package/dist/button-group.d.ts +4 -2
- package/dist/button-group.js +5 -4
- package/dist/button.d.ts +1 -1
- package/dist/button.js +6 -6
- package/dist/card.d.ts +11 -4
- package/dist/card.js +27 -14
- package/dist/checkbox.js +2 -2
- package/dist/combobox.js +4 -4
- package/dist/context-menu.d.ts +2 -1
- package/dist/context-menu.js +14 -14
- package/dist/dialog.js +5 -5
- package/dist/drawer.js +1 -1
- package/dist/encrypted-text.d.ts +15 -1
- package/dist/encrypted-text.js +205 -67
- package/dist/field.d.ts +21 -3
- package/dist/field.js +36 -8
- package/dist/fieldset.d.ts +14 -0
- package/dist/fieldset.js +26 -0
- package/dist/form.d.ts +14 -14
- package/dist/form.js +7 -4
- package/dist/glyph-matrix.d.ts +25 -0
- package/dist/glyph-matrix.js +266 -0
- package/dist/gradient-reveal-text.d.ts +3 -3
- package/dist/gradient-reveal-text.js +8 -8
- package/dist/input-group.js +1 -1
- package/dist/input-otp.d.ts +3 -3
- package/dist/input-otp.js +2 -2
- package/dist/input.js +1 -1
- package/dist/item.d.ts +1 -1
- package/dist/item.js +17 -11
- package/dist/kbd.d.ts +1 -1
- package/dist/kbd.js +2 -2
- package/dist/label.js +1 -1
- package/dist/lib/merge-refs.d.ts +9 -0
- package/dist/lib/merge-refs.js +16 -0
- package/dist/lib/utils.js +7 -0
- package/dist/masonry.d.ts +1 -1
- package/dist/masonry.js +4 -2
- package/dist/menu.d.ts +2 -1
- package/dist/menu.js +7 -7
- package/dist/menubar.js +1 -1
- package/dist/pagination.js +1 -1
- package/dist/popover.js +1 -1
- package/dist/preview-card.js +1 -1
- package/dist/progress.js +2 -2
- package/dist/radio-group.js +1 -1
- package/dist/radio.js +2 -2
- package/dist/resizable.d.ts +43 -0
- package/dist/resizable.js +53 -0
- package/dist/scroll-area.d.ts +13 -2
- package/dist/scroll-area.js +26 -7
- package/dist/select.js +4 -4
- package/dist/separator.js +1 -1
- package/dist/sidebar.js +12 -12
- package/dist/slider.js +3 -3
- package/dist/spinner.d.ts +7 -0
- package/dist/spinner.js +24 -6
- package/dist/styles.css +575 -182
- package/dist/switch.js +6 -6
- package/dist/table.d.ts +7 -1
- package/dist/table.js +40 -9
- package/dist/tabs.js +3 -3
- package/dist/textarea.js +1 -1
- package/dist/toast.js +4 -4
- package/dist/toggle-group.js +3 -3
- package/dist/toggle.d.ts +1 -1
- package/dist/toggle.js +11 -6
- package/dist/tooltip.d.ts +3 -1
- package/dist/tooltip.js +10 -6
- package/dist/video-text.d.ts +25 -0
- package/dist/video-text.js +147 -0
- package/package.json +6 -1
package/dist/combobox.js
CHANGED
|
@@ -94,7 +94,7 @@ function ComboboxContent({ className, side = "bottom", sideOffset = 6, align = "
|
|
|
94
94
|
className: "isolate z-50",
|
|
95
95
|
children: /* @__PURE__ */ jsx(Combobox$1.Popup, {
|
|
96
96
|
"data-slot": "combobox-content",
|
|
97
|
-
className: cn("motion-pop-md bg-elevated text-contrast ring-contrast/10 *:data-[slot=input-group]:bg-
|
|
97
|
+
className: cn("motion-pop-md bg-elevated text-contrast ring-contrast/10 *:data-[slot=input-group]:bg-shade/30 *:data-[slot=input-group]:border-shade/30 group/combobox-content relative max-h-(--available-height) max-w-(--available-width) min-w-(--anchor-width) overflow-hidden rounded-md shadow-md ring-1 *:data-[slot=input-group]:m-1 *:data-[slot=input-group]:mb-0 *:data-[slot=input-group]:h-8 *:data-[slot=input-group]:shadow-none", className),
|
|
98
98
|
...props
|
|
99
99
|
})
|
|
100
100
|
}) });
|
|
@@ -109,7 +109,7 @@ function ComboboxList({ className, ...props }) {
|
|
|
109
109
|
function ComboboxItem({ className, children, ...props }) {
|
|
110
110
|
return /* @__PURE__ */ jsxs(Combobox$1.Item, {
|
|
111
111
|
"data-slot": "combobox-item",
|
|
112
|
-
className: cn("data-highlighted:bg-
|
|
112
|
+
className: cn("data-highlighted:bg-highlight relative flex w-full cursor-clickable items-center gap-2 rounded-sm py-1 pr-8 pl-1.5 text-sm outline-hidden select-none data-disabled:pointer-events-none data-disabled:opacity-30 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4", className),
|
|
113
113
|
...props,
|
|
114
114
|
children: [children, /* @__PURE__ */ jsx(Combobox$1.ItemIndicator, {
|
|
115
115
|
render: /* @__PURE__ */ jsx("span", {
|
|
@@ -150,11 +150,11 @@ function ComboboxEmpty({ className, ...props }) {
|
|
|
150
150
|
function ComboboxSeparator({ className, ...props }) {
|
|
151
151
|
return /* @__PURE__ */ jsx(Combobox$1.Separator, {
|
|
152
152
|
"data-slot": "combobox-separator",
|
|
153
|
-
className: cn("bg-
|
|
153
|
+
className: cn("bg-contour -mx-1 my-1 h-px", className),
|
|
154
154
|
...props
|
|
155
155
|
});
|
|
156
156
|
}
|
|
157
|
-
const comboboxChipsVariants = cva("bg-
|
|
157
|
+
const comboboxChipsVariants = cva("bg-shade hover:border-focus focus-within:border-focus focus-within:ring-focus/50 has-aria-invalid:ring-danger/30 has-aria-invalid:border-danger flex flex-wrap items-center gap-1 rounded-md border border-transparent motion-color focus-within:ring-3 has-aria-invalid:ring-3 has-data-[slot=combobox-chip]:px-1", {
|
|
158
158
|
variants: { size: {
|
|
159
159
|
xs: "min-h-6 rounded-sm px-2 py-0.5 text-xs",
|
|
160
160
|
sm: "min-h-8 px-3 py-1 text-sm",
|
package/dist/context-menu.d.ts
CHANGED
|
@@ -20,7 +20,8 @@ type ContextMenuLabelProps = React.ComponentProps<typeof ContextMenu$1.GroupLabe
|
|
|
20
20
|
};
|
|
21
21
|
type ContextMenuItemProps = React.ComponentProps<typeof ContextMenu$1.Item> & {
|
|
22
22
|
inset?: boolean;
|
|
23
|
-
|
|
23
|
+
/** `danger` turns the item's text and leading icon red. */
|
|
24
|
+
variant?: "default" | "danger";
|
|
24
25
|
};
|
|
25
26
|
type ContextMenuSubProps = React.ComponentProps<typeof ContextMenu$1.SubmenuRoot>;
|
|
26
27
|
type ContextMenuSubTriggerProps = React.ComponentProps<typeof ContextMenu$1.SubmenuTrigger> & {
|
package/dist/context-menu.js
CHANGED
|
@@ -24,7 +24,7 @@ function ContextMenuTrigger({ className, ...props }) {
|
|
|
24
24
|
...props
|
|
25
25
|
});
|
|
26
26
|
}
|
|
27
|
-
function ContextMenuContent({ className, align = "start", alignOffset = 4, side = "
|
|
27
|
+
function ContextMenuContent({ className, align = "start", alignOffset = 4, side = "inline-end", sideOffset = 0, restoreFocusOnClose, finalFocus, ...props }) {
|
|
28
28
|
return /* @__PURE__ */ jsx(ContextMenu$1.Portal, { children: /* @__PURE__ */ jsx(ContextMenu$1.Positioner, {
|
|
29
29
|
"data-slot": "context-menu-positioner",
|
|
30
30
|
className: "isolate z-50 outline-hidden",
|
|
@@ -34,7 +34,7 @@ function ContextMenuContent({ className, align = "start", alignOffset = 4, side
|
|
|
34
34
|
sideOffset,
|
|
35
35
|
children: /* @__PURE__ */ jsx(ContextMenu$1.Popup, {
|
|
36
36
|
"data-slot": "context-menu-content",
|
|
37
|
-
className: cn("motion-pop-md
|
|
37
|
+
className: cn("motion-pop-md bg-elevated/80 not-supports-backdrop-filter:bg-elevated/90 text-contrast ring-contrast/10 z-50 max-h-(--available-height) min-w-48 overflow-x-hidden overflow-y-auto rounded-lg p-1.5 shadow-md ring-1 backdrop-blur-xl backdrop-saturate-150 outline-hidden", className),
|
|
38
38
|
finalFocus: resolveFinalFocus(restoreFocusOnClose, finalFocus),
|
|
39
39
|
...props
|
|
40
40
|
})
|
|
@@ -50,7 +50,7 @@ function ContextMenuLabel({ className, inset, ...props }) {
|
|
|
50
50
|
return /* @__PURE__ */ jsx(ContextMenu$1.GroupLabel, {
|
|
51
51
|
"data-slot": "context-menu-label",
|
|
52
52
|
"data-inset": inset,
|
|
53
|
-
className: cn("text-muted px-
|
|
53
|
+
className: cn("text-muted px-3 py-2.5 text-xs font-medium data-inset:ps-9.5", className),
|
|
54
54
|
...props
|
|
55
55
|
});
|
|
56
56
|
}
|
|
@@ -59,7 +59,7 @@ function ContextMenuItem({ className, inset, variant = "default", ...props }) {
|
|
|
59
59
|
"data-slot": "context-menu-item",
|
|
60
60
|
"data-inset": inset,
|
|
61
61
|
"data-variant": variant,
|
|
62
|
-
className: cn("focus:bg-
|
|
62
|
+
className: cn("focus:bg-highlight data-[variant=danger]:text-danger data-[variant=danger]:*:[svg]:text-danger data-[variant=danger]:focus:bg-danger data-[variant=danger]:focus:text-white data-[variant=danger]:focus:**:text-white group/context-menu-item relative flex cursor-clickable items-center gap-2.5 rounded-[calc(var(--radius)+var(--spacing)-var(--spacing)*1.5)] px-3 py-2 text-sm font-medium outline-hidden select-none data-disabled:pointer-events-none data-disabled:opacity-30 data-inset:ps-9.5 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4", className),
|
|
63
63
|
...props
|
|
64
64
|
});
|
|
65
65
|
}
|
|
@@ -73,15 +73,15 @@ function ContextMenuSubTrigger({ className, inset, children, ...props }) {
|
|
|
73
73
|
return /* @__PURE__ */ jsxs(ContextMenu$1.SubmenuTrigger, {
|
|
74
74
|
"data-slot": "context-menu-sub-trigger",
|
|
75
75
|
"data-inset": inset,
|
|
76
|
-
className: cn("focus:bg-
|
|
76
|
+
className: cn("focus:bg-highlight data-popup-open:bg-highlight flex cursor-clickable items-center gap-2.5 rounded-[calc(var(--radius)+var(--spacing)-var(--spacing)*1.5)] px-3 py-2 text-sm font-medium outline-hidden select-none data-inset:ps-9.5 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4", className),
|
|
77
77
|
...props,
|
|
78
|
-
children: [children, /* @__PURE__ */ jsx(ChevronRightIcon, { className: "
|
|
78
|
+
children: [children, /* @__PURE__ */ jsx(ChevronRightIcon, { className: "ms-auto rtl:rotate-180" })]
|
|
79
79
|
});
|
|
80
80
|
}
|
|
81
|
-
function ContextMenuSubContent({ align = "start", alignOffset = -
|
|
81
|
+
function ContextMenuSubContent({ align = "start", alignOffset = -6, side = "inline-end", sideOffset = 0, className, ...props }) {
|
|
82
82
|
return /* @__PURE__ */ jsx(ContextMenuContent, {
|
|
83
83
|
"data-slot": "context-menu-sub-content",
|
|
84
|
-
className: cn("w-auto min-w-24
|
|
84
|
+
className: cn("w-auto min-w-24", className),
|
|
85
85
|
align,
|
|
86
86
|
alignOffset,
|
|
87
87
|
side,
|
|
@@ -93,12 +93,12 @@ function ContextMenuCheckboxItem({ className, children, checked, inset, ...props
|
|
|
93
93
|
return /* @__PURE__ */ jsxs(ContextMenu$1.CheckboxItem, {
|
|
94
94
|
"data-slot": "context-menu-checkbox-item",
|
|
95
95
|
"data-inset": inset,
|
|
96
|
-
className: cn("focus:bg-
|
|
96
|
+
className: cn("focus:bg-highlight relative flex cursor-clickable items-center gap-2.5 rounded-[calc(var(--radius)+var(--spacing)-var(--spacing)*1.5)] py-2 pe-8 ps-3 text-sm font-medium outline-hidden select-none data-disabled:pointer-events-none data-disabled:opacity-30 data-inset:ps-9.5 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4", className),
|
|
97
97
|
checked,
|
|
98
98
|
...props,
|
|
99
99
|
children: [/* @__PURE__ */ jsx("span", {
|
|
100
100
|
"data-slot": "context-menu-checkbox-item-indicator",
|
|
101
|
-
className: "absolute
|
|
101
|
+
className: "absolute end-2 pointer-events-none",
|
|
102
102
|
children: /* @__PURE__ */ jsx(ContextMenu$1.CheckboxItemIndicator, { children: /* @__PURE__ */ jsx(CheckIcon, {}) })
|
|
103
103
|
}), children]
|
|
104
104
|
});
|
|
@@ -113,11 +113,11 @@ function ContextMenuRadioItem({ className, children, inset, ...props }) {
|
|
|
113
113
|
return /* @__PURE__ */ jsxs(ContextMenu$1.RadioItem, {
|
|
114
114
|
"data-slot": "context-menu-radio-item",
|
|
115
115
|
"data-inset": inset,
|
|
116
|
-
className: cn("focus:bg-
|
|
116
|
+
className: cn("focus:bg-highlight relative flex cursor-clickable items-center gap-2.5 rounded-[calc(var(--radius)+var(--spacing)-var(--spacing)*1.5)] py-2 pe-8 ps-3 text-sm font-medium outline-hidden select-none data-disabled:pointer-events-none data-disabled:opacity-30 data-inset:ps-9.5 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4", className),
|
|
117
117
|
...props,
|
|
118
118
|
children: [/* @__PURE__ */ jsx("span", {
|
|
119
119
|
"data-slot": "context-menu-radio-item-indicator",
|
|
120
|
-
className: "absolute
|
|
120
|
+
className: "absolute end-2 pointer-events-none",
|
|
121
121
|
children: /* @__PURE__ */ jsx(ContextMenu$1.RadioItemIndicator, { children: /* @__PURE__ */ jsx(CheckIcon, {}) })
|
|
122
122
|
}), children]
|
|
123
123
|
});
|
|
@@ -125,14 +125,14 @@ function ContextMenuRadioItem({ className, children, inset, ...props }) {
|
|
|
125
125
|
function ContextMenuSeparator({ className, ...props }) {
|
|
126
126
|
return /* @__PURE__ */ jsx(ContextMenu$1.Separator, {
|
|
127
127
|
"data-slot": "context-menu-separator",
|
|
128
|
-
className: cn("bg-
|
|
128
|
+
className: cn("bg-contour -mx-1.5 my-1.5 h-px", className),
|
|
129
129
|
...props
|
|
130
130
|
});
|
|
131
131
|
}
|
|
132
132
|
function ContextMenuShortcut({ className, ...props }) {
|
|
133
133
|
return /* @__PURE__ */ jsx("span", {
|
|
134
134
|
"data-slot": "context-menu-shortcut",
|
|
135
|
-
className: cn("text-muted
|
|
135
|
+
className: cn("text-muted ms-auto text-xs tracking-widest", className),
|
|
136
136
|
...props
|
|
137
137
|
});
|
|
138
138
|
}
|
package/dist/dialog.js
CHANGED
|
@@ -40,13 +40,13 @@ function DialogOverlay({ className, ...props }) {
|
|
|
40
40
|
function DialogContent({ className, children, showCloseButton = true, ...props }) {
|
|
41
41
|
return /* @__PURE__ */ jsxs(DialogPortal, { children: [/* @__PURE__ */ jsx(DialogOverlay, {}), /* @__PURE__ */ jsxs(Dialog$1.Popup, {
|
|
42
42
|
"data-slot": "dialog-content",
|
|
43
|
-
className: cn("motion-scale-lg bg-foundation ring-contrast/10 fixed top-1/2
|
|
43
|
+
className: cn("motion-scale-lg bg-foundation ring-contrast/10 fixed top-1/2 start-1/2 z-50 grid w-full max-w-[calc(100%-2rem)] -translate-x-1/2 rtl:translate-x-1/2 -translate-y-1/2 gap-6 rounded-lg p-6 text-sm ring-1 outline-hidden sm:max-w-sm shadow-lg", className),
|
|
44
44
|
...props,
|
|
45
45
|
children: [children, showCloseButton && /* @__PURE__ */ jsxs(Dialog$1.Close, {
|
|
46
46
|
"data-slot": "dialog-close",
|
|
47
47
|
render: /* @__PURE__ */ jsx(Button, {
|
|
48
48
|
variant: "ghost",
|
|
49
|
-
className: "absolute top-
|
|
49
|
+
className: "absolute top-3 end-3",
|
|
50
50
|
size: "icon-sm"
|
|
51
51
|
}),
|
|
52
52
|
children: [/* @__PURE__ */ jsx(CloseIcon, {}), /* @__PURE__ */ jsx("span", {
|
|
@@ -59,21 +59,21 @@ function DialogContent({ className, children, showCloseButton = true, ...props }
|
|
|
59
59
|
function DialogHeader({ className, ...props }) {
|
|
60
60
|
return /* @__PURE__ */ jsx("div", {
|
|
61
61
|
"data-slot": "dialog-header",
|
|
62
|
-
className: cn("flex flex-col gap-
|
|
62
|
+
className: cn("flex flex-col gap-3", className),
|
|
63
63
|
...props
|
|
64
64
|
});
|
|
65
65
|
}
|
|
66
66
|
function DialogFooter({ className, ...props }) {
|
|
67
67
|
return /* @__PURE__ */ jsx("div", {
|
|
68
68
|
"data-slot": "dialog-footer",
|
|
69
|
-
className: cn("
|
|
69
|
+
className: cn("flex flex-col-reverse gap-2 sm:flex-row sm:justify-end", className),
|
|
70
70
|
...props
|
|
71
71
|
});
|
|
72
72
|
}
|
|
73
73
|
function DialogTitle({ className, ...props }) {
|
|
74
74
|
return /* @__PURE__ */ jsx(Dialog$1.Title, {
|
|
75
75
|
"data-slot": "dialog-title",
|
|
76
|
-
className: cn("text-
|
|
76
|
+
className: cn("text-lg font-medium leading-none pe-8", className),
|
|
77
77
|
...props
|
|
78
78
|
});
|
|
79
79
|
}
|
package/dist/drawer.js
CHANGED
|
@@ -46,7 +46,7 @@ function DrawerViewport({ className, ...props }) {
|
|
|
46
46
|
function DrawerContent({ className, children, showCloseButton = false, ...props }) {
|
|
47
47
|
return /* @__PURE__ */ jsxs(DrawerPortal, { children: [/* @__PURE__ */ jsx(DrawerOverlay, {}), /* @__PURE__ */ jsx(DrawerViewport, { children: /* @__PURE__ */ jsxs(Drawer$1.Popup, {
|
|
48
48
|
"data-slot": "drawer-content",
|
|
49
|
-
className: cn("group/drawer-content bg-foundation fixed z-50 flex flex-col overflow-y-auto border-
|
|
49
|
+
className: cn("group/drawer-content bg-foundation fixed z-50 flex flex-col overflow-y-auto border-shade text-sm shadow-lg outline-hidden", "transition-[translate] duration-(--duration-lg) ease-(--ease) data-[swiping]:duration-0", "data-[swipe-direction=down]:inset-x-0 data-[swipe-direction=down]:bottom-0 data-[swipe-direction=down]:mt-24 data-[swipe-direction=down]:max-h-[80vh] data-[swipe-direction=down]:rounded-t-lg data-[swipe-direction=down]:border-t", "data-[swipe-direction=up]:inset-x-0 data-[swipe-direction=up]:top-0 data-[swipe-direction=up]:mb-24 data-[swipe-direction=up]:max-h-[80vh] data-[swipe-direction=up]:rounded-b-lg data-[swipe-direction=up]:border-b", "data-[swipe-direction=left]:inset-y-0 data-[swipe-direction=left]:left-0 data-[swipe-direction=left]:w-3/4 data-[swipe-direction=left]:rounded-r-lg data-[swipe-direction=left]:border-r data-[swipe-direction=left]:sm:max-w-sm", "data-[swipe-direction=right]:inset-y-0 data-[swipe-direction=right]:right-0 data-[swipe-direction=right]:w-3/4 data-[swipe-direction=right]:rounded-l-lg data-[swipe-direction=right]:border-l data-[swipe-direction=right]:sm:max-w-sm", "data-[swipe-direction=down]:data-[starting-style]:translate-y-full data-[swipe-direction=down]:data-[ending-style]:translate-y-full", "data-[swipe-direction=up]:data-[starting-style]:-translate-y-full data-[swipe-direction=up]:data-[ending-style]:-translate-y-full", "data-[swipe-direction=left]:data-[starting-style]:-translate-x-full data-[swipe-direction=left]:data-[ending-style]:-translate-x-full", "data-[swipe-direction=right]:data-[starting-style]:translate-x-full data-[swipe-direction=right]:data-[ending-style]:translate-x-full", className),
|
|
50
50
|
...props,
|
|
51
51
|
children: [
|
|
52
52
|
/* @__PURE__ */ jsx("div", {
|
package/dist/encrypted-text.d.ts
CHANGED
|
@@ -9,7 +9,21 @@ type EncryptedTextProps = React.ComponentProps<"span"> & {
|
|
|
9
9
|
revealedClassName?: string;
|
|
10
10
|
scrambleOnly?: boolean;
|
|
11
11
|
scrambleOneChar?: boolean;
|
|
12
|
+
/** Where the reveal begins. Default: "start" */
|
|
13
|
+
revealDirection?: "start" | "end" | "center" | "random";
|
|
14
|
+
/** Scramble and reveal again when hovered. Default: false */
|
|
15
|
+
scrambleOnHover?: boolean;
|
|
16
|
+
/** Scramble and reveal again after every reveal. Default: false */
|
|
17
|
+
loop?: boolean;
|
|
18
|
+
/** Pause between a reveal and the next loop, in ms. Default: 2000 */
|
|
19
|
+
loopDelayMs?: number;
|
|
12
20
|
};
|
|
13
|
-
|
|
21
|
+
/**
|
|
22
|
+
* Text that starts scrambled and reveals itself, character by character, once
|
|
23
|
+
* it scrolls into view. Each character keeps its real width while scrambled
|
|
24
|
+
* (the stand-in glyph sits over an invisible copy and narrows to fit), so the
|
|
25
|
+
* text never shifts what's around it. Screen readers get the real text.
|
|
26
|
+
*/
|
|
27
|
+
declare function EncryptedText({ text, className, revealDelayMs, charset, flipDelayMs, encryptedClassName, revealedClassName, scrambleOnly, scrambleOneChar, revealDirection, scrambleOnHover, loop, loopDelayMs, onPointerEnter, ...props }: EncryptedTextProps): React.JSX.Element | null;
|
|
14
28
|
//#endregion
|
|
15
29
|
export { EncryptedText, type EncryptedTextProps };
|
package/dist/encrypted-text.js
CHANGED
|
@@ -1,29 +1,95 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
import { cn } from "./lib/utils.js";
|
|
3
|
-
import { jsx, jsxs } from "react/jsx-runtime";
|
|
3
|
+
import { Fragment, jsx, jsxs } from "react/jsx-runtime";
|
|
4
4
|
import * as React from "react";
|
|
5
|
-
import { useReducedMotion } from "motion/react";
|
|
6
5
|
//#region src/encrypted-text.tsx
|
|
7
6
|
const DEFAULT_CHARSET = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789!@#$%^&*()_+-={}[];:,.<>/?";
|
|
8
|
-
|
|
9
|
-
|
|
7
|
+
const REDUCED_MOTION = "(prefers-reduced-motion: reduce)";
|
|
8
|
+
/** Opacity of a scrambled character far from its turn. */
|
|
9
|
+
const ENCRYPTED_OPACITY = .6;
|
|
10
|
+
/** Characters ahead of the reveal that brighten toward full opacity. */
|
|
11
|
+
const LEAD = 4;
|
|
12
|
+
function isSpace(char) {
|
|
13
|
+
return /\s/.test(char);
|
|
10
14
|
}
|
|
11
|
-
function
|
|
12
|
-
|
|
13
|
-
let result = "";
|
|
14
|
-
for (let i = 0; i < original.length; i += 1) result += original[i] === " " ? " " : randomChar(charset);
|
|
15
|
-
return result;
|
|
15
|
+
function randomGlyph(pool) {
|
|
16
|
+
return pool[Math.floor(Math.random() * pool.length)] ?? "";
|
|
16
17
|
}
|
|
17
|
-
|
|
18
|
-
|
|
18
|
+
/**
|
|
19
|
+
* A scramble that's the same on the server and in the browser, so the text
|
|
20
|
+
* can start encrypted without a hydration mismatch.
|
|
21
|
+
*/
|
|
22
|
+
function initialFrame(chars, pool) {
|
|
23
|
+
let rank = 0;
|
|
24
|
+
return {
|
|
25
|
+
glyphs: chars.map((char, index) => isSpace(char) ? char : pool[((char.codePointAt(0) ?? 0) + index * 7) % pool.length] ?? char),
|
|
26
|
+
scales: chars.map(() => 1),
|
|
27
|
+
ranks: chars.map((char) => isSpace(char) ? -1 : rank++),
|
|
28
|
+
revealed: 0
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
/** Each character's place in the reveal order for a direction; -1 for spaces. */
|
|
32
|
+
function rankLetters(chars, direction) {
|
|
33
|
+
const order = chars.flatMap((char, index) => isSpace(char) ? [] : [index]);
|
|
34
|
+
if (direction === "end") order.reverse();
|
|
35
|
+
if (direction === "center") {
|
|
36
|
+
const middle = (chars.length - 1) / 2;
|
|
37
|
+
order.sort((a, b) => Math.abs(a - middle) - Math.abs(b - middle));
|
|
38
|
+
}
|
|
39
|
+
if (direction === "random") for (let i = order.length - 1; i > 0; i--) {
|
|
40
|
+
const j = Math.floor(Math.random() * (i + 1));
|
|
41
|
+
[order[i], order[j]] = [order[j] ?? 0, order[i] ?? 0];
|
|
42
|
+
}
|
|
43
|
+
const ranks = chars.map(() => -1);
|
|
44
|
+
order.forEach((index, rank) => {
|
|
45
|
+
ranks[index] = rank;
|
|
46
|
+
});
|
|
47
|
+
return ranks;
|
|
48
|
+
}
|
|
49
|
+
const glyphWidths = /* @__PURE__ */ new Map();
|
|
50
|
+
let measurer;
|
|
51
|
+
/** Measures a glyph in a font, caching it once the font has loaded. */
|
|
52
|
+
function measureGlyph(font, glyph) {
|
|
53
|
+
const key = `${font}|${glyph}`;
|
|
54
|
+
const cached = glyphWidths.get(key);
|
|
55
|
+
if (cached !== void 0) return cached;
|
|
56
|
+
measurer ??= document.createElement("canvas").getContext("2d");
|
|
57
|
+
if (!measurer) return 0;
|
|
58
|
+
measurer.font = font;
|
|
59
|
+
const width = measurer.measureText(glyph).width;
|
|
60
|
+
if (document.fonts.check(font)) glyphWidths.set(key, width);
|
|
61
|
+
return width;
|
|
62
|
+
}
|
|
63
|
+
/** How much to narrow a stand-in glyph so it fits the real one's width. */
|
|
64
|
+
function squeeze(font, char, glyph) {
|
|
65
|
+
const room = measureGlyph(font, char);
|
|
66
|
+
const need = measureGlyph(font, glyph);
|
|
67
|
+
return need > room && need > 0 ? Math.max(.05, room / need) : 1;
|
|
68
|
+
}
|
|
69
|
+
function subscribeReducedMotion(onChange) {
|
|
70
|
+
const query = window.matchMedia(REDUCED_MOTION);
|
|
71
|
+
query.addEventListener("change", onChange);
|
|
72
|
+
return () => query.removeEventListener("change", onChange);
|
|
73
|
+
}
|
|
74
|
+
const getReducedMotion = () => window.matchMedia(REDUCED_MOTION).matches;
|
|
75
|
+
const getServerReducedMotion = () => false;
|
|
76
|
+
/**
|
|
77
|
+
* Text that starts scrambled and reveals itself, character by character, once
|
|
78
|
+
* it scrolls into view. Each character keeps its real width while scrambled
|
|
79
|
+
* (the stand-in glyph sits over an invisible copy and narrows to fit), so the
|
|
80
|
+
* text never shifts what's around it. Screen readers get the real text.
|
|
81
|
+
*/
|
|
82
|
+
function EncryptedText({ text, className, revealDelayMs = 50, charset = DEFAULT_CHARSET, flipDelayMs = 50, encryptedClassName, revealedClassName, scrambleOnly = false, scrambleOneChar = false, revealDirection = "start", scrambleOnHover = false, loop = false, loopDelayMs = 2e3, onPointerEnter, ...props }) {
|
|
83
|
+
const prefersReducedMotion = React.useSyncExternalStore(subscribeReducedMotion, getReducedMotion, getServerReducedMotion);
|
|
19
84
|
const ref = React.useRef(null);
|
|
85
|
+
const restartRef = React.useRef(null);
|
|
86
|
+
const isRunningRef = React.useRef(false);
|
|
20
87
|
const [isInView, setIsInView] = React.useState(false);
|
|
21
|
-
const
|
|
22
|
-
const
|
|
23
|
-
const
|
|
24
|
-
const
|
|
25
|
-
const
|
|
26
|
-
const scrambleCharsRef = React.useRef([]);
|
|
88
|
+
const chars = React.useMemo(() => Array.from(text), [text]);
|
|
89
|
+
const pool = React.useMemo(() => Array.from(charset.length > 0 ? charset : DEFAULT_CHARSET), [charset]);
|
|
90
|
+
const initial = React.useMemo(() => initialFrame(chars, pool), [chars, pool]);
|
|
91
|
+
const [live, setLive] = React.useState();
|
|
92
|
+
const frame = live?.chars === chars ? live.frame : initial;
|
|
27
93
|
React.useEffect(() => {
|
|
28
94
|
if (prefersReducedMotion) return;
|
|
29
95
|
const el = ref.current;
|
|
@@ -38,79 +104,151 @@ function EncryptedText({ text, className, revealDelayMs = 50, charset = DEFAULT_
|
|
|
38
104
|
return () => observer.disconnect();
|
|
39
105
|
}, [prefersReducedMotion]);
|
|
40
106
|
React.useEffect(() => {
|
|
41
|
-
|
|
42
|
-
if (!isInView) return;
|
|
43
|
-
const
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
let
|
|
107
|
+
const root = ref.current;
|
|
108
|
+
if (prefersReducedMotion || !isInView || !root) return;
|
|
109
|
+
const computed = getComputedStyle(root);
|
|
110
|
+
const font = `${computed.fontStyle} ${computed.fontWeight} ${computed.fontSize} ${computed.fontFamily}`;
|
|
111
|
+
const letters = chars.filter((char) => !isSpace(char)).length;
|
|
112
|
+
let glyphs = [];
|
|
113
|
+
let scales = [];
|
|
114
|
+
let ranks = [];
|
|
115
|
+
let revealed = 0;
|
|
116
|
+
let start = 0;
|
|
117
|
+
let lastFlip = 0;
|
|
118
|
+
let frameId = 0;
|
|
119
|
+
let timer;
|
|
120
|
+
const scramble = (index) => {
|
|
121
|
+
const glyph = randomGlyph(pool);
|
|
122
|
+
glyphs[index] = glyph;
|
|
123
|
+
scales[index] = squeeze(font, chars[index] ?? glyph, glyph);
|
|
124
|
+
};
|
|
125
|
+
const publish = () => {
|
|
126
|
+
setLive({
|
|
127
|
+
chars,
|
|
128
|
+
frame: {
|
|
129
|
+
glyphs: [...glyphs],
|
|
130
|
+
scales: [...scales],
|
|
131
|
+
ranks,
|
|
132
|
+
revealed
|
|
133
|
+
}
|
|
134
|
+
});
|
|
135
|
+
};
|
|
49
136
|
const update = (now) => {
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
}
|
|
61
|
-
} else for (let index = 0; index < totalLength; index += 1) scrambleCharsRef.current[index] = text[index] === " " ? " " : randomChar(charset);
|
|
62
|
-
lastFlipTimeRef.current = now;
|
|
63
|
-
setFlipTick((t) => t + 1 & 65535);
|
|
137
|
+
let isChanged = false;
|
|
138
|
+
if (!scrambleOnly) {
|
|
139
|
+
const next = Math.min(letters, Math.floor((now - start) / Math.max(1, revealDelayMs)));
|
|
140
|
+
isChanged = next !== revealed;
|
|
141
|
+
revealed = next;
|
|
142
|
+
if (revealed >= letters) {
|
|
143
|
+
publish();
|
|
144
|
+
isRunningRef.current = false;
|
|
145
|
+
if (loop) timer = setTimeout(run, Math.max(0, loopDelayMs));
|
|
146
|
+
return;
|
|
64
147
|
}
|
|
65
|
-
animationFrameRef.current = requestAnimationFrame(update);
|
|
66
|
-
return;
|
|
67
148
|
}
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
149
|
+
if (now - lastFlip >= Math.max(0, flipDelayMs)) {
|
|
150
|
+
lastFlip = now;
|
|
151
|
+
isChanged = true;
|
|
152
|
+
if (scrambleOnly && scrambleOneChar) {
|
|
153
|
+
const scrambled = chars.flatMap((char, index) => isSpace(char) ? [] : [index]);
|
|
154
|
+
const index = scrambled[Math.floor(Math.random() * scrambled.length)];
|
|
155
|
+
if (index !== void 0) scramble(index);
|
|
156
|
+
} else chars.forEach((char, index) => {
|
|
157
|
+
if (!isSpace(char) && (ranks[index] ?? 0) >= revealed) scramble(index);
|
|
158
|
+
});
|
|
76
159
|
}
|
|
77
|
-
|
|
160
|
+
if (isChanged) publish();
|
|
161
|
+
frameId = requestAnimationFrame(update);
|
|
162
|
+
};
|
|
163
|
+
const run = () => {
|
|
164
|
+
cancelAnimationFrame(frameId);
|
|
165
|
+
clearTimeout(timer);
|
|
166
|
+
isRunningRef.current = true;
|
|
167
|
+
ranks = rankLetters(chars, revealDirection);
|
|
168
|
+
glyphs = [...chars];
|
|
169
|
+
scales = chars.map(() => 1);
|
|
170
|
+
chars.forEach((char, index) => {
|
|
171
|
+
if (!isSpace(char)) scramble(index);
|
|
172
|
+
});
|
|
173
|
+
revealed = 0;
|
|
174
|
+
start = performance.now();
|
|
175
|
+
lastFlip = start;
|
|
176
|
+
publish();
|
|
177
|
+
frameId = requestAnimationFrame(update);
|
|
78
178
|
};
|
|
79
|
-
|
|
179
|
+
restartRef.current = run;
|
|
180
|
+
run();
|
|
80
181
|
return () => {
|
|
81
|
-
|
|
82
|
-
|
|
182
|
+
cancelAnimationFrame(frameId);
|
|
183
|
+
clearTimeout(timer);
|
|
184
|
+
isRunningRef.current = false;
|
|
185
|
+
restartRef.current = null;
|
|
83
186
|
};
|
|
84
187
|
}, [
|
|
85
188
|
prefersReducedMotion,
|
|
86
189
|
isInView,
|
|
87
|
-
|
|
190
|
+
chars,
|
|
191
|
+
pool,
|
|
88
192
|
revealDelayMs,
|
|
89
|
-
charset,
|
|
90
193
|
flipDelayMs,
|
|
91
194
|
scrambleOnly,
|
|
92
|
-
scrambleOneChar
|
|
195
|
+
scrambleOneChar,
|
|
196
|
+
revealDirection,
|
|
197
|
+
loop,
|
|
198
|
+
loopDelayMs
|
|
93
199
|
]);
|
|
200
|
+
const handlePointerEnter = (event) => {
|
|
201
|
+
onPointerEnter?.(event);
|
|
202
|
+
if (scrambleOnHover && !scrambleOnly && !isRunningRef.current) restartRef.current?.();
|
|
203
|
+
};
|
|
94
204
|
if (!text) return null;
|
|
205
|
+
const words = [];
|
|
206
|
+
chars.forEach((char, index) => {
|
|
207
|
+
const word = words.at(-1);
|
|
208
|
+
if (isSpace(char) || !word || isSpace(chars[index - 1] ?? " ")) words.push({
|
|
209
|
+
start: index,
|
|
210
|
+
letters: [char]
|
|
211
|
+
});
|
|
212
|
+
else word.letters.push(char);
|
|
213
|
+
});
|
|
95
214
|
return /* @__PURE__ */ jsxs("span", {
|
|
96
215
|
ref,
|
|
97
216
|
"data-slot": "encrypted-text",
|
|
98
217
|
className,
|
|
218
|
+
onPointerEnter: handlePointerEnter,
|
|
99
219
|
...props,
|
|
100
220
|
children: [/* @__PURE__ */ jsx("span", {
|
|
101
221
|
className: "sr-only",
|
|
102
222
|
children: text
|
|
103
|
-
}),
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
223
|
+
}), words.map((word) => isSpace(word.letters[0] ?? "") ? /* @__PURE__ */ jsx(React.Fragment, { children: word.letters[0] }, word.start) : /* @__PURE__ */ jsx("span", {
|
|
224
|
+
"aria-hidden": "true",
|
|
225
|
+
"data-slot": "encrypted-text-word",
|
|
226
|
+
className: "whitespace-nowrap",
|
|
227
|
+
children: word.letters.map((char, offset) => {
|
|
228
|
+
const index = word.start + offset;
|
|
229
|
+
const rank = frame.ranks[index] ?? 0;
|
|
230
|
+
const isRevealed = prefersReducedMotion || !scrambleOnly && rank < frame.revealed;
|
|
231
|
+
const distance = rank - frame.revealed;
|
|
232
|
+
const lift = Math.max(0, 1 - distance / LEAD);
|
|
233
|
+
const scale = frame.scales[index] ?? 1;
|
|
234
|
+
return /* @__PURE__ */ jsx("span", {
|
|
235
|
+
"data-slot": "encrypted-text-char",
|
|
236
|
+
"data-revealed": isRevealed || void 0,
|
|
237
|
+
className: cn("motion-color relative inline-block", isRevealed ? revealedClassName : encryptedClassName),
|
|
238
|
+
style: isRevealed || scrambleOnly ? void 0 : { opacity: ENCRYPTED_OPACITY + .4 * lift },
|
|
239
|
+
children: isRevealed ? char : /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx("span", {
|
|
240
|
+
"data-slot": "encrypted-text-ghost",
|
|
241
|
+
className: "invisible",
|
|
242
|
+
children: char
|
|
243
|
+
}), /* @__PURE__ */ jsx("span", {
|
|
244
|
+
"data-slot": "encrypted-text-glyph",
|
|
245
|
+
className: "absolute top-0 left-1/2 -translate-x-1/2",
|
|
246
|
+
style: scale < 1 ? { scale: `${scale} 1` } : void 0,
|
|
247
|
+
children: frame.glyphs[index] ?? char
|
|
248
|
+
})] })
|
|
249
|
+
}, index);
|
|
250
|
+
})
|
|
251
|
+
}, word.start))]
|
|
114
252
|
});
|
|
115
253
|
}
|
|
116
254
|
//#endregion
|
package/dist/field.d.ts
CHANGED
|
@@ -1,19 +1,37 @@
|
|
|
1
1
|
import * as React from "react";
|
|
2
|
+
import { VariantProps } from "class-variance-authority";
|
|
2
3
|
import { Field as FieldPrimitive } from "@base-ui/react/field";
|
|
3
4
|
//#region src/field.d.ts
|
|
4
|
-
|
|
5
|
+
declare const fieldVariants: (props?: ({
|
|
6
|
+
orientation?: "horizontal" | "vertical" | null | undefined;
|
|
7
|
+
} & import("class-variance-authority/types").ClassProp) | undefined) => string;
|
|
8
|
+
type FieldProps = React.ComponentProps<typeof FieldPrimitive.Root> & VariantProps<typeof fieldVariants>;
|
|
5
9
|
type FieldLabelProps = React.ComponentProps<typeof FieldPrimitive.Label>;
|
|
10
|
+
type FieldContentProps = React.ComponentProps<"div">;
|
|
6
11
|
type FieldDescriptionProps = React.ComponentProps<typeof FieldPrimitive.Description>;
|
|
7
12
|
type FieldItemProps = React.ComponentProps<typeof FieldPrimitive.Item>;
|
|
8
13
|
type FieldErrorProps = React.ComponentProps<typeof FieldPrimitive.Error>;
|
|
9
14
|
type FieldValidityProps = React.ComponentProps<typeof FieldPrimitive.Validity>;
|
|
10
15
|
type FieldControlProps = React.ComponentProps<typeof FieldPrimitive.Control>;
|
|
11
|
-
|
|
16
|
+
/**
|
|
17
|
+
* A labelled control with its messages. Below the control, `FieldError`
|
|
18
|
+
* comes first and `FieldDescription` under it, so the hint stays visible
|
|
19
|
+
* while the error is showing. An invalid field doesn't tint its text: the
|
|
20
|
+
* control's danger ring and the error carry the state, and the label
|
|
21
|
+
* stays neutral.
|
|
22
|
+
*/
|
|
23
|
+
declare function Field({ className, orientation, ...props }: FieldProps): React.JSX.Element;
|
|
12
24
|
declare function FieldLabel({ className, ...props }: FieldLabelProps): React.JSX.Element;
|
|
13
25
|
declare function FieldControl({ className, ...props }: FieldControlProps): React.JSX.Element;
|
|
26
|
+
/**
|
|
27
|
+
* A label and its description stacked together, beside a control. The label
|
|
28
|
+
* row is as tall as the default checkbox, radio and switch (24px), so its
|
|
29
|
+
* text centers on the control.
|
|
30
|
+
*/
|
|
31
|
+
declare function FieldContent({ className, ...props }: FieldContentProps): React.JSX.Element;
|
|
14
32
|
declare function FieldDescription({ className, ...props }: FieldDescriptionProps): React.JSX.Element;
|
|
15
33
|
declare function FieldItem({ className, ...props }: FieldItemProps): React.JSX.Element;
|
|
16
34
|
declare function FieldError({ className, ...props }: FieldErrorProps): React.JSX.Element;
|
|
17
35
|
declare const FieldValidity: React.FC<import("@base-ui/react").FieldValidityProps>;
|
|
18
36
|
//#endregion
|
|
19
|
-
export { Field, FieldControl, type FieldControlProps, FieldDescription, type FieldDescriptionProps, FieldError, type FieldErrorProps, FieldItem, type FieldItemProps, FieldLabel, type FieldLabelProps, FieldPrimitive, type FieldProps, FieldValidity, type FieldValidityProps };
|
|
37
|
+
export { Field, FieldContent, type FieldContentProps, FieldControl, type FieldControlProps, FieldDescription, type FieldDescriptionProps, FieldError, type FieldErrorProps, FieldItem, type FieldItemProps, FieldLabel, type FieldLabelProps, FieldPrimitive, type FieldProps, FieldValidity, type FieldValidityProps, fieldVariants };
|