@boyernick/standard-ui-react 0.1.1-canary.2 → 0.1.1-canary.22

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 (56) hide show
  1. package/package.json +5 -3
  2. package/src/accordion.tsx +1 -1
  3. package/src/alert-dialog.tsx +1 -1
  4. package/src/autocomplete.tsx +12 -11
  5. package/src/badge.tsx +73 -9
  6. package/src/brand.tsx +2 -2
  7. package/src/breadcrumb.tsx +38 -20
  8. package/src/button.tsx +2 -2
  9. package/src/calendar.tsx +22 -11
  10. package/src/card.tsx +57 -14
  11. package/src/carousel.tsx +163 -6
  12. package/src/checkbox.tsx +35 -13
  13. package/src/collapsible.tsx +4 -2
  14. package/src/combobox.tsx +13 -12
  15. package/src/command.tsx +38 -14
  16. package/src/context-menu.tsx +6 -3
  17. package/src/date-picker.tsx +174 -0
  18. package/src/dialog.tsx +33 -10
  19. package/src/drawer.tsx +1 -1
  20. package/src/empty.tsx +4 -4
  21. package/src/field.tsx +1 -1
  22. package/src/filter-group.tsx +139 -0
  23. package/src/icons.tsx +39 -0
  24. package/src/illustrations.tsx +219 -141
  25. package/src/image-modal.tsx +440 -49
  26. package/src/index.ts +188 -0
  27. package/src/input.tsx +1 -1
  28. package/src/kbd.tsx +49 -0
  29. package/src/lib/motion.ts +128 -18
  30. package/src/lib/popup.ts +45 -0
  31. package/src/menu.tsx +7 -4
  32. package/src/menubar.tsx +5 -0
  33. package/src/meter.tsx +1 -1
  34. package/src/minimap.tsx +267 -0
  35. package/src/navigation-menu.tsx +336 -67
  36. package/src/number-field.tsx +327 -61
  37. package/src/pagination.tsx +258 -42
  38. package/src/popover.tsx +3 -1
  39. package/src/preview-card.tsx +3 -1
  40. package/src/progress.tsx +6 -1
  41. package/src/questionnaire.tsx +279 -0
  42. package/src/scroll-area.tsx +1 -1
  43. package/src/select.tsx +8 -6
  44. package/src/sidebar.tsx +109 -28
  45. package/src/slider.tsx +64 -6
  46. package/src/sounds.tsx +6 -10
  47. package/src/spinner.tsx +105 -32
  48. package/src/switch.tsx +2 -2
  49. package/src/table.tsx +82 -15
  50. package/src/tabs.tsx +184 -38
  51. package/src/text-animate.tsx +71 -28
  52. package/src/textarea.tsx +1 -1
  53. package/src/timeline.tsx +321 -0
  54. package/src/toast.tsx +156 -23
  55. package/src/tooltip.tsx +43 -3
  56. package/src/video-player.tsx +296 -120
package/src/table.tsx CHANGED
@@ -1,8 +1,60 @@
1
+ import { cva, type VariantProps } from "class-variance-authority";
1
2
  import type { ComponentProps } from "react";
2
3
  import { cn } from "./lib/cn";
3
4
 
4
- export type TableProps = ComponentProps<"table">;
5
- export type TableHeaderProps = ComponentProps<"thead">;
5
+ const tableVariants = cva(
6
+ "group/table w-full text-sm text-fg-primary",
7
+ {
8
+ variants: {
9
+ variant: {
10
+ default: "border-collapse",
11
+ grid: [
12
+ "border-separate border-spacing-0",
13
+ "[&_tr]:border-b-0",
14
+ "[&_tr>*]:border-r [&_tr>*]:border-b [&_tr>*]:border-border-primary",
15
+ "[&_tr>*:last-child]:border-r-0",
16
+ "[&_tbody_tr:last-child>*]:border-b-0 [&_tfoot_tr:last-child>*]:border-b-0",
17
+ ],
18
+ },
19
+ density: {
20
+ default: "",
21
+ compact: "[&_th]:h-8 [&_th]:px-3 [&_td]:px-3 [&_td]:py-2",
22
+ },
23
+ striped: {
24
+ true: "[&_tbody_tr:nth-child(even)]:bg-background-secondary",
25
+ false: "",
26
+ },
27
+ },
28
+ defaultVariants: {
29
+ variant: "default",
30
+ density: "default",
31
+ striped: false,
32
+ },
33
+ },
34
+ );
35
+
36
+ const tableHeaderVariants = cva(
37
+ "bg-background-secondary [&_tr]:border-b [&_tr]:border-border-primary",
38
+ {
39
+ variants: {
40
+ sticky: {
41
+ true: "[&_th]:sticky [&_th]:top-0 [&_th]:z-10 [&_th]:bg-background-secondary [&_th]:py-2.5",
42
+ false: "",
43
+ },
44
+ },
45
+ defaultVariants: {
46
+ sticky: false,
47
+ },
48
+ },
49
+ );
50
+
51
+ export type TableProps = ComponentProps<"table"> &
52
+ VariantProps<typeof tableVariants> & {
53
+ /** Classes for the bordered horizontal-scroll container. */
54
+ containerClassName?: string;
55
+ };
56
+ export type TableHeaderProps = ComponentProps<"thead"> &
57
+ VariantProps<typeof tableHeaderVariants>;
6
58
  export type TableBodyProps = ComponentProps<"tbody">;
7
59
  export type TableFooterProps = ComponentProps<"tfoot">;
8
60
  export type TableRowProps = ComponentProps<"tr">;
@@ -10,24 +62,37 @@ export type TableHeadProps = ComponentProps<"th">;
10
62
  export type TableCellProps = ComponentProps<"td">;
11
63
  export type TableCaptionProps = ComponentProps<"caption">;
12
64
 
13
- export const Table = ({ className, ...props }: TableProps) => (
14
- <div className="w-full overflow-x-auto rounded-lg border border-border-primary bg-surface">
65
+ export const Table = ({
66
+ className,
67
+ containerClassName,
68
+ variant,
69
+ density,
70
+ striped,
71
+ ...props
72
+ }: TableProps) => (
73
+ <div
74
+ className={cn(
75
+ "w-full overflow-auto rounded-lg border border-border-primary bg-surface",
76
+ containerClassName,
77
+ )}
78
+ >
15
79
  <table
16
- className={cn(
17
- "w-full border-collapse text-sm text-fg-primary",
18
- className,
19
- )}
80
+ data-variant={variant ?? "default"}
81
+ data-density={density ?? "default"}
82
+ data-striped={striped || undefined}
83
+ className={cn(tableVariants({ variant, density, striped }), className)}
20
84
  {...props}
21
85
  />
22
86
  </div>
23
87
  );
24
88
 
25
- export const TableHeader = ({ className, ...props }: TableHeaderProps) => (
89
+ export const TableHeader = ({
90
+ className,
91
+ sticky,
92
+ ...props
93
+ }: TableHeaderProps) => (
26
94
  <thead
27
- className={cn(
28
- "bg-background-secondary [&_tr]:border-b [&_tr]:border-border-primary",
29
- className,
30
- )}
95
+ className={cn(tableHeaderVariants({ sticky }), className)}
31
96
  {...props}
32
97
  />
33
98
  );
@@ -39,7 +104,7 @@ export const TableBody = ({ className, ...props }: TableBodyProps) => (
39
104
  export const TableFooter = ({ className, ...props }: TableFooterProps) => (
40
105
  <tfoot
41
106
  className={cn(
42
- "border-t border-border-primary bg-background-secondary font-medium",
107
+ "border-t border-border-primary bg-background-secondary font-medium [&_tr]:hover:bg-transparent",
43
108
  className,
44
109
  )}
45
110
  {...props}
@@ -49,7 +114,7 @@ export const TableFooter = ({ className, ...props }: TableFooterProps) => (
49
114
  export const TableRow = ({ className, ...props }: TableRowProps) => (
50
115
  <tr
51
116
  className={cn(
52
- "border-b border-border-primary hover:bg-background-secondary",
117
+ "border-b border-border-primary transition-colors duration-[var(--duration-sm)] ease-enter hover:bg-background-secondary aria-selected:bg-background-tertiary data-[selected]:bg-background-tertiary data-[state=selected]:bg-background-tertiary",
53
118
  className,
54
119
  )}
55
120
  {...props}
@@ -79,3 +144,5 @@ export const TableCaption = ({ className, ...props }: TableCaptionProps) => (
79
144
  {...props}
80
145
  />
81
146
  );
147
+
148
+ export { tableHeaderVariants, tableVariants };
package/src/tabs.tsx CHANGED
@@ -1,62 +1,208 @@
1
1
  "use client"
2
2
 
3
3
  import { Tabs as BaseTabs } from "@base-ui/react/tabs"
4
- import type { ComponentProps } from "react"
4
+ import { cva, type VariantProps } from "class-variance-authority"
5
+ import { createContext, type ComponentProps, useContext } from "react"
5
6
  import { cn } from "./lib/cn"
6
7
  import { motion } from "./lib/motion"
7
8
 
8
- export type TabsProps = ComponentProps<typeof BaseTabs.Root>
9
- export type TabsListProps = ComponentProps<typeof BaseTabs.List>
10
- export type TabsTabProps = ComponentProps<typeof BaseTabs.Tab>
11
- export type TabsIndicatorProps = ComponentProps<typeof BaseTabs.Indicator>
12
- export type TabsPanelProps = ComponentProps<typeof BaseTabs.Panel>
9
+ const tabsListVariants = cva("relative flex items-center", {
10
+ variants: {
11
+ variant: {
12
+ underline:
13
+ "w-full gap-0.5 border-b border-border-primary data-[orientation=vertical]:w-40 data-[orientation=vertical]:shrink-0 data-[orientation=vertical]:flex-col data-[orientation=vertical]:items-stretch data-[orientation=vertical]:gap-0 data-[orientation=vertical]:border-r data-[orientation=vertical]:border-b-0",
14
+ segmented:
15
+ "w-fit rounded-lg bg-background-tertiary p-0.5 data-[orientation=vertical]:flex-col data-[orientation=vertical]:items-stretch",
16
+ pill: "w-fit gap-1 data-[orientation=vertical]:flex-col data-[orientation=vertical]:items-stretch",
17
+ },
18
+ },
19
+ defaultVariants: {
20
+ variant: "underline",
21
+ },
22
+ })
13
23
 
14
- export const Tabs = ({ className, ...props }: TabsProps) => (
15
- <BaseTabs.Root className={cn("flex w-full flex-col", className)} {...props} />
24
+ const tabsTabVariants = cva(
25
+ "relative z-10 inline-flex shrink-0 cursor-pointer items-center justify-center gap-1.5 whitespace-nowrap text-fg-tertiary outline-none select-none hover:text-fg-primary focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-offset-1 focus-visible:ring-offset-background-primary focus-visible:ring-ring/20 data-active:text-fg-primary data-disabled:cursor-not-allowed data-disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0",
26
+ {
27
+ variants: {
28
+ variant: {
29
+ underline:
30
+ "data-[orientation=horizontal]:pb-1 data-[orientation=vertical]:w-full data-[orientation=vertical]:justify-start",
31
+ segmented: "rounded-md",
32
+ pill: "rounded-full bg-background-tertiary data-active:bg-transparent",
33
+ },
34
+ size: {
35
+ sm: "text-xs [&_svg]:size-3.5",
36
+ md: "text-sm [&_svg]:size-4",
37
+ lg: "text-md [&_svg]:size-4",
38
+ },
39
+ },
40
+ compoundVariants: [
41
+ {
42
+ variant: "underline",
43
+ size: "sm",
44
+ class: "h-8 px-2.5 data-[orientation=horizontal]:h-9",
45
+ },
46
+ {
47
+ variant: "underline",
48
+ size: "md",
49
+ class: "h-10 px-3 data-[orientation=horizontal]:h-11",
50
+ },
51
+ {
52
+ variant: "underline",
53
+ size: "lg",
54
+ class: "h-12 px-3.5 data-[orientation=horizontal]:h-13",
55
+ },
56
+ { variant: "segmented", size: "sm", class: "h-7 px-2.5" },
57
+ { variant: "segmented", size: "md", class: "h-8 px-3" },
58
+ { variant: "segmented", size: "lg", class: "h-9 px-3.5" },
59
+ { variant: "pill", size: "sm", class: "h-7 px-2.5" },
60
+ { variant: "pill", size: "md", class: "h-8 px-3" },
61
+ { variant: "pill", size: "lg", class: "h-9 px-3.5" },
62
+ ],
63
+ defaultVariants: {
64
+ variant: "underline",
65
+ size: "md",
66
+ },
67
+ },
16
68
  )
17
69
 
18
- export const TabsList = ({ className, ...props }: TabsListProps) => (
19
- <BaseTabs.List
20
- className={cn(
21
- "relative flex gap-0.5 border-b border-border-primary",
22
- className,
23
- )}
24
- {...props}
25
- />
26
- )
70
+ const tabsIndicatorVariants = cva("absolute z-0", {
71
+ variants: {
72
+ variant: {
73
+ underline:
74
+ "bottom-0 left-0 h-0.5 w-(--active-tab-width) translate-x-(--active-tab-left) rounded-full bg-brand-primary data-[orientation=vertical]:top-0 data-[orientation=vertical]:right-0 data-[orientation=vertical]:bottom-auto data-[orientation=vertical]:left-auto data-[orientation=vertical]:h-(--active-tab-height) data-[orientation=vertical]:w-0.5 data-[orientation=vertical]:translate-x-0 data-[orientation=vertical]:translate-y-(--active-tab-top)",
75
+ segmented:
76
+ "inset-y-0.5 left-0 w-(--active-tab-width) translate-x-(--active-tab-left) rounded-md bg-surface-raised shadow-hairline data-[orientation=vertical]:inset-x-0.5 data-[orientation=vertical]:top-0 data-[orientation=vertical]:bottom-auto data-[orientation=vertical]:h-(--active-tab-height) data-[orientation=vertical]:w-auto data-[orientation=vertical]:translate-x-0 data-[orientation=vertical]:translate-y-(--active-tab-top)",
77
+ pill: "inset-y-0 left-0 w-(--active-tab-width) translate-x-(--active-tab-left) rounded-full bg-surface-raised shadow-hairline data-[orientation=vertical]:inset-x-0 data-[orientation=vertical]:top-0 data-[orientation=vertical]:bottom-auto data-[orientation=vertical]:h-(--active-tab-height) data-[orientation=vertical]:w-auto data-[orientation=vertical]:translate-x-0 data-[orientation=vertical]:translate-y-(--active-tab-top)",
78
+ },
79
+ },
80
+ defaultVariants: {
81
+ variant: "underline",
82
+ },
83
+ })
27
84
 
28
- export const TabsTab = ({ className, ...props }: TabsTabProps) => (
29
- <BaseTabs.Tab
30
- className={cn(
31
- "relative cursor-pointer px-3 py-2.5 text-sm text-fg-tertiary outline-none select-none",
32
- motion.colors,
33
- "hover:text-fg-primary outline-none focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-offset-1 focus-visible:ring-offset-background-primary focus-visible:ring-ring/20 data-active:text-fg-primary data-disabled:cursor-not-allowed data-disabled:opacity-50",
34
- className,
35
- )}
36
- {...props}
37
- />
85
+ export type TabsVariant = NonNullable<
86
+ VariantProps<typeof tabsListVariants>["variant"]
87
+ >
88
+ export type TabsSize = NonNullable<
89
+ VariantProps<typeof tabsTabVariants>["size"]
90
+ >
91
+
92
+ type TabsStyleContextValue = {
93
+ variant: TabsVariant
94
+ size: TabsSize
95
+ }
96
+
97
+ const TabsStyleContext = createContext<TabsStyleContextValue>({
98
+ variant: "underline",
99
+ size: "md",
100
+ })
101
+
102
+ export type TabsProps = ComponentProps<typeof BaseTabs.Root> & {
103
+ /** Visual treatment shared by the list, tabs, and indicator. */
104
+ variant?: TabsVariant
105
+ /** Height and typography shared by every tab. */
106
+ size?: TabsSize
107
+ }
108
+ export type TabsListProps = ComponentProps<typeof BaseTabs.List> &
109
+ VariantProps<typeof tabsListVariants>
110
+ export type TabsTabProps = ComponentProps<typeof BaseTabs.Tab> &
111
+ VariantProps<typeof tabsTabVariants>
112
+ export type TabsIndicatorProps = ComponentProps<typeof BaseTabs.Indicator> &
113
+ VariantProps<typeof tabsIndicatorVariants>
114
+ export type TabsPanelProps = ComponentProps<typeof BaseTabs.Panel>
115
+
116
+ export const Tabs = ({
117
+ className,
118
+ variant = "underline",
119
+ size = "md",
120
+ ...props
121
+ }: TabsProps) => (
122
+ <TabsStyleContext.Provider value={{ variant, size }}>
123
+ <BaseTabs.Root
124
+ data-slot="tabs"
125
+ className={cn(
126
+ "flex w-full flex-col data-[orientation=vertical]:flex-row data-[orientation=vertical]:gap-6",
127
+ className,
128
+ )}
129
+ {...props}
130
+ />
131
+ </TabsStyleContext.Provider>
38
132
  )
39
133
 
134
+ export const TabsList = ({
135
+ className,
136
+ variant,
137
+ ...props
138
+ }: TabsListProps) => {
139
+ const styles = useContext(TabsStyleContext)
140
+
141
+ return (
142
+ <BaseTabs.List
143
+ data-slot="tabs-list"
144
+ className={cn(
145
+ tabsListVariants({ variant: variant ?? styles.variant }),
146
+ className,
147
+ )}
148
+ {...props}
149
+ />
150
+ )
151
+ }
152
+
153
+ export const TabsTab = ({
154
+ className,
155
+ variant,
156
+ size,
157
+ ...props
158
+ }: TabsTabProps) => {
159
+ const styles = useContext(TabsStyleContext)
160
+
161
+ return (
162
+ <BaseTabs.Tab
163
+ data-slot="tabs-tab"
164
+ className={cn(
165
+ tabsTabVariants({
166
+ variant: variant ?? styles.variant,
167
+ size: size ?? styles.size,
168
+ }),
169
+ motion.colors,
170
+ className,
171
+ )}
172
+ {...props}
173
+ />
174
+ )
175
+ }
176
+
40
177
  export const TabsIndicator = ({
41
178
  className,
179
+ variant,
42
180
  ...props
43
- }: TabsIndicatorProps) => (
44
- <BaseTabs.Indicator
45
- className={cn(
46
- "absolute bottom-0 left-0 z-10 h-0.5 w-(--active-tab-width) translate-x-(--active-tab-left) rounded-full bg-brand-primary",
47
- motion.tabsIndicator,
48
- className,
49
- )}
50
- {...props}
51
- />
52
- )
181
+ }: TabsIndicatorProps) => {
182
+ const styles = useContext(TabsStyleContext)
183
+
184
+ return (
185
+ <BaseTabs.Indicator
186
+ data-slot="tabs-indicator"
187
+ className={cn(
188
+ tabsIndicatorVariants({ variant: variant ?? styles.variant }),
189
+ motion.tabsIndicator,
190
+ className,
191
+ )}
192
+ {...props}
193
+ />
194
+ )
195
+ }
53
196
 
54
197
  export const TabsPanel = ({ className, ...props }: TabsPanelProps) => (
55
198
  <BaseTabs.Panel
199
+ data-slot="tabs-panel"
56
200
  className={cn(
57
- "pt-4 text-sm leading-relaxed text-fg-secondary outline-none focus-visible:outline-none",
201
+ "pt-4 text-sm leading-relaxed text-fg-secondary outline-none focus-visible:outline-none data-[orientation=vertical]:pt-0",
58
202
  className,
59
203
  )}
60
204
  {...props}
61
205
  />
62
206
  )
207
+
208
+ export { tabsIndicatorVariants, tabsListVariants, tabsTabVariants }
@@ -8,12 +8,18 @@ import {
8
8
  } from "react"
9
9
  import { cn } from "./lib/cn"
10
10
 
11
+ /** How many glyphs are scrambling at once. The window travels the line; behind
12
+ * it the text is settled, ahead of it nothing is drawn yet. 8 is the reference
13
+ * default from baffle.js, which this effect follows. */
14
+ const DECODE_WINDOW = 8
15
+
11
16
  const DECODE_GLYPHS =
12
17
  "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789#@$%&*"
13
18
 
14
19
  export type TextAnimateEffect =
15
20
  | "typewriter"
16
21
  | "decode"
22
+ | "shimmer"
17
23
  | "fade"
18
24
  | "blur"
19
25
 
@@ -32,20 +38,25 @@ export type TextAnimateProps = HTMLAttributes<HTMLSpanElement> & {
32
38
  export const TextAnimate = ({
33
39
  text,
34
40
  effect = "typewriter",
35
- speed = 40,
41
+ speed = 60,
36
42
  delay = 0,
37
43
  replay = true,
38
44
  as: Tag = "span",
39
45
  className,
40
46
  ...props
41
47
  }: TextAnimateProps) => {
42
- const [output, setOutput] = useState(effect === "typewriter" || effect === "decode" ? "" : text)
43
- const [done, setDone] = useState(effect === "fade" || effect === "blur")
48
+ const [output, setOutput] = useState(
49
+ effect === "typewriter" || effect === "decode" ? "" : text,
50
+ )
51
+ const [done, setDone] = useState(
52
+ effect === "fade" || effect === "blur" || effect === "shimmer",
53
+ )
44
54
  const chars = useMemo(() => [...text], [text])
45
55
 
46
56
  useEffect(() => {
47
57
  let cancelled = false
48
58
  let timeoutId: ReturnType<typeof setTimeout> | undefined
59
+ let frameId: number | undefined
49
60
 
50
61
  const start = () => {
51
62
  if (
@@ -57,7 +68,7 @@ export const TextAnimate = ({
57
68
  return
58
69
  }
59
70
 
60
- if (effect === "fade" || effect === "blur") {
71
+ if (effect === "fade" || effect === "blur" || effect === "shimmer") {
61
72
  setOutput(text)
62
73
  setDone(true)
63
74
  return
@@ -82,30 +93,59 @@ export const TextAnimate = ({
82
93
  return
83
94
  }
84
95
 
85
- // decode
86
- setOutput(
87
- chars.map(() => DECODE_GLYPHS[Math.floor(Math.random() * DECODE_GLYPHS.length)]).join(""),
88
- )
89
- let revealed = 0
90
- const tick = () => {
96
+ // Decode follows baffle.js's `reveal`, which is the reference for this
97
+ // effect: a fixed-width window of scrambled glyphs travels left to
98
+ // right, the real text sits behind it, and nothing at all is drawn ahead
99
+ // of it — so the line grows in with a churning leading edge rather than
100
+ // resolving out of a full-width block of noise.
101
+ //
102
+ // It runs on rAF with a time accumulator rather than `setTimeout(speed)`.
103
+ // A 28ms timer does not divide into a ~16.7ms frame, so steps land on
104
+ // uneven frames and the text visibly stutters; accumulating elapsed time
105
+ // keeps the reference's cadence while landing each step on a real frame.
106
+ const slots = chars.reduce<number[]>((acc, char, index) => {
107
+ if (char !== " ") acc.push(index)
108
+ return acc
109
+ }, [])
110
+
111
+ // The window starts off the front of the line so the first glyph is
112
+ // already scrambling as it arrives.
113
+ let position = -DECODE_WINDOW
114
+ let previousStep = -Infinity
115
+ const startedAt = performance.now()
116
+
117
+ const frame = (now: number) => {
91
118
  if (cancelled) return
92
- revealed += 1
93
- const next = chars
94
- .map((char, index) => {
95
- if (char === " ") return " "
96
- if (index < revealed) return char
97
- return DECODE_GLYPHS[Math.floor(Math.random() * DECODE_GLYPHS.length)]
98
- })
99
- .join("")
100
- setOutput(next)
101
- if (revealed < chars.length) {
102
- timeoutId = setTimeout(tick, speed)
103
- } else {
104
- setOutput(text)
105
- setDone(true)
119
+
120
+ const step = Math.floor((now - startedAt) / speed) - DECODE_WINDOW
121
+ if (step !== previousStep) {
122
+ previousStep = step
123
+ position = step
124
+
125
+ if (position > slots.length) {
126
+ setOutput(text)
127
+ setDone(true)
128
+ return
129
+ }
130
+
131
+ const next = [...chars]
132
+ for (let p = Math.max(position, 0); p < slots.length; p += 1) {
133
+ next[slots[p]] =
134
+ p < position + DECODE_WINDOW
135
+ ? DECODE_GLYPHS[
136
+ Math.floor(Math.random() * DECODE_GLYPHS.length)
137
+ ]
138
+ : ""
139
+ }
140
+ setOutput(next.join(""))
106
141
  }
142
+
143
+ frameId = requestAnimationFrame(frame)
107
144
  }
108
- timeoutId = setTimeout(tick, speed)
145
+
146
+ setOutput("")
147
+ frameId = requestAnimationFrame(frame)
148
+ return
109
149
  }
110
150
 
111
151
  timeoutId = setTimeout(start, delay)
@@ -113,18 +153,21 @@ export const TextAnimate = ({
113
153
  return () => {
114
154
  cancelled = true
115
155
  if (timeoutId) clearTimeout(timeoutId)
156
+ if (frameId) cancelAnimationFrame(frameId)
116
157
  }
117
158
  }, [chars, delay, effect, replay, speed, text])
118
159
 
119
- if (effect === "fade" || effect === "blur") {
160
+ if (effect === "fade" || effect === "blur" || effect === "shimmer") {
120
161
  return (
121
162
  <Tag
122
163
  className={cn(
123
164
  "inline-block text-fg-primary",
124
165
  effect === "fade" &&
125
- "animate-[text-fade-in_0.6s_ease-out_both] motion-reduce:animate-none",
166
+ "animate-[text-fade-in_0.9s_var(--ease-enter)_both] motion-reduce:animate-none",
126
167
  effect === "blur" &&
127
- "animate-[text-blur-in_0.7s_ease-out_both] motion-reduce:animate-none",
168
+ "animate-[text-blur-in_1s_var(--ease-enter)_both] motion-reduce:animate-none",
169
+ effect === "shimmer" &&
170
+ "bg-[linear-gradient(110deg,var(--color-fg-tertiary)_35%,var(--color-fg-primary)_50%,var(--color-fg-tertiary)_65%)] bg-[length:250%_100%] bg-clip-text text-transparent animate-[text-shimmer_3s_linear_infinite] motion-reduce:animate-none motion-reduce:bg-none motion-reduce:text-fg-primary",
128
171
  className,
129
172
  )}
130
173
  style={{ animationDelay: `${delay}ms` }}
package/src/textarea.tsx CHANGED
@@ -3,7 +3,7 @@ import type { TextareaHTMLAttributes } from "react"
3
3
  import { cn } from "./lib/cn"
4
4
 
5
5
  const textareaVariants = cva(
6
- "flex min-h-20 w-full cursor-text resize-y rounded-md px-3 py-2 text-sm text-fg-primary transition-[color,box-shadow] duration-150 ease-out placeholder:text-fg-quaternary outline-none focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-offset-1 focus-visible:ring-offset-background-primary focus-visible:ring-ring/20 aria-invalid:border-destructive aria-invalid:focus-visible:border-destructive aria-invalid:focus-visible:ring-destructive/20 disabled:cursor-not-allowed disabled:opacity-50",
6
+ "flex min-h-20 w-full cursor-text resize-y rounded-md px-3 py-2 text-sm text-fg-primary transition-[color,box-shadow] duration-[var(--duration-sm)] ease-enter placeholder:text-fg-quaternary outline-none focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-offset-1 focus-visible:ring-offset-background-primary focus-visible:ring-ring/20 aria-invalid:border-destructive aria-invalid:focus-visible:border-destructive aria-invalid:focus-visible:ring-destructive/20 disabled:cursor-not-allowed disabled:opacity-50",
7
7
  {
8
8
  variants: {
9
9
  variant: {