@waveso/ui 0.15.1 → 0.17.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.
Files changed (87) hide show
  1. package/README.md +13 -12
  2. package/dist/accordion.js +2 -2
  3. package/dist/action-bar.js +4 -4
  4. package/dist/alert-dialog.js +5 -5
  5. package/dist/alert.d.ts +8 -1
  6. package/dist/alert.js +13 -4
  7. package/dist/animate.d.ts +1 -1
  8. package/dist/animate.js +2 -9
  9. package/dist/autocomplete.js +3 -3
  10. package/dist/avatar.d.ts +1 -1
  11. package/dist/avatar.js +4 -4
  12. package/dist/badge.d.ts +1 -1
  13. package/dist/badge.js +6 -6
  14. package/dist/button-group.d.ts +4 -2
  15. package/dist/button-group.js +5 -4
  16. package/dist/button.d.ts +1 -1
  17. package/dist/button.js +6 -6
  18. package/dist/card.d.ts +11 -4
  19. package/dist/card.js +27 -14
  20. package/dist/checkbox.js +2 -2
  21. package/dist/combobox.js +4 -4
  22. package/dist/context-menu.d.ts +2 -1
  23. package/dist/context-menu.js +14 -14
  24. package/dist/dialog.js +5 -5
  25. package/dist/drawer.js +1 -1
  26. package/dist/encrypted-text.d.ts +15 -1
  27. package/dist/encrypted-text.js +205 -67
  28. package/dist/field.d.ts +21 -3
  29. package/dist/field.js +36 -8
  30. package/dist/fieldset.d.ts +14 -0
  31. package/dist/fieldset.js +26 -0
  32. package/dist/form.d.ts +14 -14
  33. package/dist/form.js +7 -4
  34. package/dist/glyph-matrix.d.ts +25 -0
  35. package/dist/glyph-matrix.js +266 -0
  36. package/dist/gradient-reveal-text.d.ts +3 -3
  37. package/dist/gradient-reveal-text.js +8 -8
  38. package/dist/input-group.js +1 -1
  39. package/dist/input-otp.d.ts +3 -3
  40. package/dist/input-otp.js +2 -2
  41. package/dist/input.js +1 -1
  42. package/dist/item.d.ts +1 -1
  43. package/dist/item.js +17 -11
  44. package/dist/kbd.d.ts +1 -1
  45. package/dist/kbd.js +2 -2
  46. package/dist/label.js +1 -1
  47. package/dist/lib/merge-refs.d.ts +9 -0
  48. package/dist/lib/merge-refs.js +16 -0
  49. package/dist/lib/utils.js +7 -0
  50. package/dist/masonry.d.ts +1 -1
  51. package/dist/masonry.js +4 -2
  52. package/dist/menu.d.ts +2 -1
  53. package/dist/menu.js +7 -7
  54. package/dist/menubar.js +1 -1
  55. package/dist/pagination.js +1 -1
  56. package/dist/popover.js +1 -1
  57. package/dist/preview-card.js +1 -1
  58. package/dist/progress.js +2 -2
  59. package/dist/radio-group.js +1 -1
  60. package/dist/radio.js +2 -2
  61. package/dist/resizable.d.ts +43 -0
  62. package/dist/resizable.js +53 -0
  63. package/dist/scroll-area.d.ts +13 -2
  64. package/dist/scroll-area.js +26 -7
  65. package/dist/select.js +4 -4
  66. package/dist/separator.js +1 -1
  67. package/dist/sidebar.js +12 -12
  68. package/dist/slider.js +3 -3
  69. package/dist/spinner.d.ts +7 -0
  70. package/dist/spinner.js +24 -6
  71. package/dist/styles.css +581 -194
  72. package/dist/swipe-deck.d.ts +98 -0
  73. package/dist/swipe-deck.js +931 -0
  74. package/dist/switch.js +6 -6
  75. package/dist/table.d.ts +7 -1
  76. package/dist/table.js +40 -9
  77. package/dist/tabs.js +3 -3
  78. package/dist/textarea.js +1 -1
  79. package/dist/toast.js +4 -4
  80. package/dist/toggle-group.js +3 -3
  81. package/dist/toggle.d.ts +1 -1
  82. package/dist/toggle.js +11 -6
  83. package/dist/tooltip.d.ts +3 -1
  84. package/dist/tooltip.js +10 -6
  85. package/dist/video-text.d.ts +25 -0
  86. package/dist/video-text.js +147 -0
  87. 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-edge/30 *:data-[slot=input-group]:border-edge/30 group/combobox-content relative max-h-(--available-height) max-w-(--available-width) min-w-(--anchor-width) origin-(--transform-origin) 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),
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-primary data-highlighted:text-white not-data-[variant=destructive]:data-highlighted:**:text-white 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-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4", className),
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-line -mx-1 my-1 h-px", className),
153
+ className: cn("bg-contour -mx-1 my-1 h-px", className),
154
154
  ...props
155
155
  });
156
156
  }
157
- const comboboxChipsVariants = cva("bg-edge hover:border-focus focus-within:border-focus focus-within:ring-focus/50 has-aria-invalid:ring-destructive/30 has-aria-invalid:border-destructive 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", {
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",
@@ -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
- variant?: "default" | "destructive";
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> & {
@@ -24,7 +24,7 @@ function ContextMenuTrigger({ className, ...props }) {
24
24
  ...props
25
25
  });
26
26
  }
27
- function ContextMenuContent({ className, align = "start", alignOffset = 4, side = "right", sideOffset = 0, restoreFocusOnClose, finalFocus, ...props }) {
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 ring-contrast/10 bg-elevated text-contrast z-50 max-h-(--available-height) min-w-36 origin-(--transform-origin) overflow-x-hidden overflow-y-auto rounded-md p-1 shadow-md ring-1 outline-hidden", className),
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-1.5 py-1 text-xs font-medium data-inset:pl-7", className),
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-primary focus:text-white data-[variant=destructive]:text-destructive data-[variant=destructive]:focus:bg-destructive/10 dark:data-[variant=destructive]:focus:bg-destructive/20 data-[variant=destructive]:focus:text-destructive data-[variant=destructive]:*:[svg]:text-destructive focus:*:[svg]:text-white data-[variant=destructive]:focus:*:[svg]:text-destructive group/context-menu-item relative flex cursor-clickable items-center gap-1.5 rounded-sm px-1.5 py-1 text-sm outline-hidden select-none data-disabled:pointer-events-none data-disabled:opacity-50 data-inset:pl-7 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4", className),
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-primary focus:text-white data-popup-open:bg-primary data-popup-open:text-white flex cursor-clickable items-center gap-1.5 rounded-sm px-1.5 py-1 text-sm outline-hidden select-none data-inset:pl-7 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4", className),
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: "ml-auto" })]
78
+ children: [children, /* @__PURE__ */ jsx(ChevronRightIcon, { className: "ms-auto rtl:rotate-180" })]
79
79
  });
80
80
  }
81
- function ContextMenuSubContent({ align = "start", alignOffset = -4, side = "right", sideOffset = 0, className, ...props }) {
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 shadow-md", className),
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-primary focus:text-white focus:*:[svg]:text-white relative flex cursor-clickable items-center gap-1.5 rounded-sm py-1 pr-8 pl-1.5 text-sm outline-hidden select-none data-disabled:pointer-events-none data-disabled:opacity-50 data-inset:pl-7 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4", className),
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 right-2 pointer-events-none",
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-primary focus:text-white focus:*:[svg]:text-white relative flex cursor-clickable items-center gap-1.5 rounded-sm py-1 pr-8 pl-1.5 text-sm outline-hidden select-none data-disabled:pointer-events-none data-disabled:opacity-50 data-inset:pl-7 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4", className),
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 right-2 pointer-events-none",
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-line -mx-1 my-1 h-px", className),
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 group-focus/context-menu-item:text-white ml-auto text-xs tracking-widest", className),
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 left-1/2 z-50 grid w-full max-w-[calc(100%-2rem)] -translate-x-1/2 -translate-y-1/2 gap-4 rounded-lg p-4 text-sm ring-1 outline-hidden sm:max-w-sm", className),
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-2 right-2",
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-2", className),
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("bg-secondary/50 -mx-4 -mb-4 flex flex-col-reverse gap-2 rounded-b-lg border-t border-line p-4 sm:flex-row sm:justify-end", className),
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-base leading-none font-medium pr-8", className),
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-edge 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),
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", {
@@ -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
- declare function EncryptedText({ text, className, revealDelayMs, charset, flipDelayMs, encryptedClassName, revealedClassName, scrambleOnly, scrambleOneChar, ...props }: EncryptedTextProps): React.JSX.Element | null;
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 };
@@ -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
- function randomChar(charset) {
9
- return charset.charAt(Math.floor(Math.random() * charset.length));
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 scramblePreservingSpaces(original, charset) {
12
- if (!original) return "";
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
- function EncryptedText({ text, className, revealDelayMs = 50, charset = DEFAULT_CHARSET, flipDelayMs = 50, encryptedClassName, revealedClassName, scrambleOnly = false, scrambleOneChar = false, ...props }) {
18
- const prefersReducedMotion = useReducedMotion();
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 [revealCount, setRevealCount] = React.useState(0);
22
- const [, setFlipTick] = React.useState(0);
23
- const animationFrameRef = React.useRef(null);
24
- const startTimeRef = React.useRef(0);
25
- const lastFlipTimeRef = React.useRef(0);
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
- if (prefersReducedMotion) return;
42
- if (!isInView) return;
43
- const initial = text ? scramblePreservingSpaces(text, charset) : "";
44
- scrambleCharsRef.current = initial.split("");
45
- startTimeRef.current = performance.now();
46
- lastFlipTimeRef.current = startTimeRef.current;
47
- setRevealCount(0);
48
- let isCancelled = false;
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
- if (isCancelled) return;
51
- const totalLength = text.length;
52
- if (scrambleOnly) {
53
- if (now - lastFlipTimeRef.current >= Math.max(0, flipDelayMs)) {
54
- if (scrambleOneChar) {
55
- const indices = [];
56
- for (let i = 0; i < totalLength; i++) if (text[i] !== " ") indices.push(i);
57
- if (indices.length > 0) {
58
- const idx = indices[Math.floor(Math.random() * indices.length)];
59
- scrambleCharsRef.current[idx] = randomChar(charset);
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
- const elapsedMs = now - startTimeRef.current;
69
- const currentRevealCount = Math.min(totalLength, Math.floor(elapsedMs / Math.max(1, revealDelayMs)));
70
- setRevealCount(currentRevealCount);
71
- if (currentRevealCount >= totalLength) return;
72
- if (now - lastFlipTimeRef.current >= Math.max(0, flipDelayMs)) {
73
- for (let index = currentRevealCount; index < totalLength; index += 1) scrambleCharsRef.current[index] = text[index] === " " ? " " : randomChar(charset);
74
- lastFlipTimeRef.current = now;
75
- setFlipTick((t) => t + 1 & 65535);
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
- animationFrameRef.current = requestAnimationFrame(update);
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
- animationFrameRef.current = requestAnimationFrame(update);
179
+ restartRef.current = run;
180
+ run();
80
181
  return () => {
81
- isCancelled = true;
82
- if (animationFrameRef.current !== null) cancelAnimationFrame(animationFrameRef.current);
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
- text,
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
- }), text.split("").map((char, index) => {
104
- const isRevealed = prefersReducedMotion || !isInView || !scrambleOnly && index < revealCount;
105
- const displayChar = isRevealed ? char : char === " " ? " " : scrambleCharsRef.current[index] ?? char;
106
- return /* @__PURE__ */ jsx("span", {
107
- "aria-hidden": "true",
108
- "data-slot": "encrypted-text-char",
109
- "data-revealed": isRevealed || void 0,
110
- className: cn(isRevealed ? revealedClassName : encryptedClassName),
111
- children: displayChar
112
- }, index);
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
- type FieldProps = React.ComponentProps<typeof FieldPrimitive.Root>;
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
- declare function Field({ className, ...props }: FieldProps): React.JSX.Element;
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 };