@boyernick/standard-ui-react 0.1.0 → 0.1.1-canary.10

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/src/lib/motion.ts CHANGED
@@ -1,23 +1,133 @@
1
1
  /**
2
2
  * Shared motion classes for StandardUI.
3
- * Prefer CSS `scale` on centered modals so `translate` centering stays intact.
4
- * Anchored popups use `transform-origin` from Base UI.
3
+ *
4
+ * Every value resolves to a token in `@boyernick/standard-ui-tokens` the
5
+ * `--ease-*` ladder and the `--duration-*` scale. Nothing here hardcodes a
6
+ * curve or a millisecond count.
7
+ *
8
+ * The easing ladder is ordered by *onset* — how quickly a transition commits:
9
+ *
10
+ * passive ambient, system-initiated (slowest onset)
11
+ * enter surfaces arriving
12
+ * move things travelling across a surface
13
+ * snap direct response to a pointer (sharpest onset)
14
+ *
15
+ * Two kinds of token live here, and they behave differently:
16
+ *
17
+ * Mount tokens — backdrop, popupCenter, popupAnchor, accordionPanel,
18
+ * checkIndicator.
19
+ * These carry separate enter and exit behaviour. Base UI flags the exit
20
+ * with `data-ending-style`, and the `data-ending-style:` variants below
21
+ * re-declare duration and easing for that direction. Exits always run at
22
+ * `--duration-exit`, so dismissing anything costs the same no matter how
23
+ * long the entrance took.
24
+ *
25
+ * State tokens — tabsIndicator, colors, transform, all.
26
+ * These animate between states on a mounted element, so there is no enter
27
+ * or exit to distinguish; they take a single curve.
28
+ *
29
+ * Fades are linear on purpose. Perceived brightness is already non-linear, so
30
+ * easing an opacity change makes it look like it sticks at the end.
5
31
  */
32
+
33
+ const DURATION = {
34
+ xs: "duration-[var(--duration-xs)]",
35
+ sm: "duration-[var(--duration-sm)]",
36
+ md: "duration-[var(--duration-md)]",
37
+ lg: "duration-[var(--duration-lg)]",
38
+ } as const
39
+
40
+ /** Every exit runs at the same length — see `--duration-exit`. */
41
+ const EXIT_DURATION = "data-ending-style:duration-[var(--duration-exit)]"
42
+
43
+ const REDUCED = "motion-reduce:transition-none"
44
+
6
45
  export const motion = {
7
- backdrop:
8
- "transition-opacity duration-150 ease-out motion-reduce:transition-none data-starting-style:opacity-0 data-ending-style:opacity-0",
9
- /** Centered dialog / alert — scale property, not transform */
10
- popupCenter:
11
- "transition-[scale,opacity] duration-150 ease-out motion-reduce:transition-none data-starting-style:scale-[0.96] data-starting-style:opacity-0 data-ending-style:scale-[0.96] data-ending-style:opacity-0",
12
- /** Select, tooltip, menus — transform-origin from Base UI */
13
- popupAnchor:
14
- "origin-[var(--transform-origin)] transition-[transform,scale,opacity] duration-150 ease-out motion-reduce:transition-none data-starting-style:scale-[0.96] data-starting-style:opacity-0 data-ending-style:scale-[0.96] data-ending-style:opacity-0",
15
- accordionPanel:
16
- "transition-[height] duration-200 ease-[cubic-bezier(0.22,1,0.36,1)] motion-reduce:transition-none data-starting-style:h-0 data-ending-style:h-0",
17
- tabsIndicator:
18
- "transition-[translate,width] duration-200 ease-[cubic-bezier(0.22,1,0.36,1)] motion-reduce:transition-none",
19
- colors: "transition-colors duration-150 ease-out motion-reduce:transition-none",
20
- transform:
21
- "transition-transform duration-150 ease-out motion-reduce:transition-none",
22
- all: "transition-all duration-150 ease-out motion-reduce:transition-none",
46
+ /** Overlay scrims. Linear both ways — see the note on fades above. */
47
+ backdrop: [
48
+ "transition-opacity",
49
+ DURATION.sm,
50
+ "ease-linear",
51
+ REDUCED,
52
+ "data-starting-style:opacity-0",
53
+ "data-ending-style:opacity-0",
54
+ EXIT_DURATION,
55
+ "data-ending-style:ease-linear",
56
+ ].join(" "),
57
+
58
+ /** Centred dialog / alert — animates `scale`, so translate centring holds. */
59
+ popupCenter: [
60
+ "transition-[scale,opacity]",
61
+ DURATION.md,
62
+ "ease-enter",
63
+ REDUCED,
64
+ "data-starting-style:scale-[0.96] data-starting-style:opacity-0",
65
+ "data-ending-style:scale-[0.96] data-ending-style:opacity-0",
66
+ EXIT_DURATION,
67
+ "data-ending-style:ease-move",
68
+ ].join(" "),
69
+
70
+ /** Select, tooltip, menus — transform-origin comes from Base UI. */
71
+ popupAnchor: [
72
+ "origin-[var(--transform-origin)]",
73
+ "transition-[transform,scale,opacity]",
74
+ DURATION.sm,
75
+ "ease-enter",
76
+ REDUCED,
77
+ "data-starting-style:scale-[0.96] data-starting-style:opacity-0",
78
+ "data-ending-style:scale-[0.96] data-ending-style:opacity-0",
79
+ EXIT_DURATION,
80
+ "data-ending-style:ease-move",
81
+ ].join(" "),
82
+
83
+ /** Height expand and collapse. */
84
+ accordionPanel: [
85
+ "transition-[height]",
86
+ DURATION.md,
87
+ "ease-move",
88
+ REDUCED,
89
+ "data-starting-style:h-0",
90
+ "data-ending-style:h-0",
91
+ EXIT_DURATION,
92
+ "data-ending-style:ease-move",
93
+ ].join(" "),
94
+
95
+ /** Check and minus glyphs mounting inside a small control. Snaps, because
96
+ * it answers a click directly, and scales from the centre so the mark
97
+ * reads as landing in the box rather than fading onto it. */
98
+ checkIndicator: [
99
+ "transition-[scale,opacity]",
100
+ DURATION.sm,
101
+ "ease-snap",
102
+ REDUCED,
103
+ "data-starting-style:scale-50 data-starting-style:opacity-0",
104
+ "data-ending-style:scale-50 data-ending-style:opacity-0",
105
+ EXIT_DURATION,
106
+ "data-ending-style:ease-move",
107
+ ].join(" "),
108
+
109
+ /** Sliding tab underline or pill — follows a pointer, so it snaps. */
110
+ tabsIndicator: [
111
+ "transition-[translate,width]",
112
+ DURATION.md,
113
+ "ease-snap",
114
+ REDUCED,
115
+ ].join(" "),
116
+
117
+ /** Hover and pressed colour changes. */
118
+ colors: ["transition-colors", DURATION.sm, "ease-enter", REDUCED].join(" "),
119
+
120
+ /** Icon rotates and simple transforms. */
121
+ transform: [
122
+ "transition-transform",
123
+ DURATION.sm,
124
+ "ease-move",
125
+ REDUCED,
126
+ ].join(" "),
127
+
128
+ /** Every property. Reach for a narrower token first. */
129
+ all: ["transition-all", DURATION.sm, "ease-enter", REDUCED].join(" "),
130
+
131
+ /** Long, ambient movement — progress fills, meters, background shifts. */
132
+ passive: ["transition-all", DURATION.lg, "ease-passive", REDUCED].join(" "),
23
133
  } as const
@@ -0,0 +1,45 @@
1
+ /**
2
+ * Shared surface classes for anchored popups — autocomplete, combobox, select,
3
+ * menu, context menu, and anything else that floats a panel next to a trigger.
4
+ *
5
+ * These live in one place because they have to agree. A popup that picks its
6
+ * own radius or hairline reads as a different component to the one beside it,
7
+ * and the drift is invisible until two of them sit on the same page.
8
+ *
9
+ * The radii are related, not independent. `item` is nested inside `inset`, so
10
+ * it follows the nested-radius rule:
11
+ *
12
+ * inner = outer − padding 6px = 12px − 6px
13
+ *
14
+ * Change `surface`'s radius or `inset`'s padding and `item` has to move with
15
+ * it, or the corners stop being concentric. See `.cursor/rules/nested-radius`.
16
+ */
17
+
18
+ /** Radius, hairline and elevation. Add `overflow-hidden` where items can reach
19
+ * the corners; leave it off panels that nest their own popups.
20
+ *
21
+ * No `border` here on purpose. Every shadow token composes
22
+ * `--elevation-hairline`, so `shadow-lg` already draws the edge — and that
23
+ * hairline flips colour with the theme (black on light, white on dark) and
24
+ * thins to 0.5px on hi-dpi displays. A border on top stacks a second edge
25
+ * just outside the first, which reads as one muddy line darker than either,
26
+ * and none of that tracking comes with it. Leaving it off also keeps the
27
+ * edge out of the box model, so `popupInset` is the whole inset. */
28
+ export const popupSurface = "rounded-xl bg-surface shadow-lg outline-none"
29
+
30
+ /** Padding around a list of items. Collapses when there is nothing to pad, so
31
+ * a sibling empty state is not pushed off-centre by a list of zero items.
32
+ *
33
+ * 6px is the whole inset — `popupSurface` carries no border to add to it —
34
+ * which is what keeps 12 − 6 = 6 landing exactly on `popupItem`'s radius. */
35
+ export const popupInset = "p-1.5 empty:p-0"
36
+
37
+ /** One selectable row. */
38
+ export const popupItem =
39
+ "flex min-h-9 cursor-default items-center gap-2 rounded-sm px-3 py-2 text-sm text-fg-primary outline-none select-none"
40
+
41
+ /** A group heading. Shares the item's left edge. */
42
+ export const popupLabel = "px-3 py-1.5 text-xs text-fg-tertiary"
43
+
44
+ /** Empty and status text. Shares the item's left edge. */
45
+ export const popupMessage = "px-3 py-2 text-sm text-fg-tertiary"
@@ -108,7 +108,7 @@ type Format = {
108
108
  }
109
109
 
110
110
  const formats: Format[] = [
111
- { label: "Bold", marker: "**", className: "font-semibold" },
111
+ { label: "Bold", marker: "**", className: "font-medium" },
112
112
  { label: "Italic", marker: "*", className: "italic" },
113
113
  { label: "Code", marker: "`", className: "font-mono" },
114
114
  ]
package/src/menu.tsx CHANGED
@@ -5,6 +5,7 @@ import type { ComponentProps } from "react"
5
5
  import { IconCheckmark1, IconChevronRightSmall } from "./icons"
6
6
  import { cn } from "./lib/cn"
7
7
  import { motion } from "./lib/motion"
8
+ import { popupInset, popupItem, popupLabel, popupSurface } from "./lib/popup"
8
9
 
9
10
  export type MenuProps = ComponentProps<typeof BaseMenu.Root>
10
11
  export type MenuTriggerProps = ComponentProps<typeof BaseMenu.Trigger>
@@ -34,7 +35,7 @@ export type MenuSubmenuTriggerProps = ComponentProps<
34
35
  export type MenuViewportProps = ComponentProps<typeof BaseMenu.Viewport>
35
36
 
36
37
  const menuItemClassName = cn(
37
- "flex min-h-8 cursor-default items-center gap-2 rounded-xs px-2.5 py-1.5 text-sm text-fg-primary outline-none select-none",
38
+ popupItem,
38
39
  motion.colors,
39
40
  "data-disabled:cursor-not-allowed data-disabled:opacity-50 data-highlighted:bg-background-tertiary",
40
41
  )
@@ -59,7 +60,7 @@ export const MenuBackdrop = ({ className, ...props }: MenuBackdropProps) => (
59
60
  )
60
61
 
61
62
  export const MenuPositioner = ({
62
- sideOffset = 4,
63
+ sideOffset = 8,
63
64
  className,
64
65
  ...props
65
66
  }: MenuPositionerProps) => (
@@ -73,7 +74,9 @@ export const MenuPositioner = ({
73
74
  export const MenuPopup = ({ className, ...props }: MenuPopupProps) => (
74
75
  <BaseMenu.Popup
75
76
  className={cn(
76
- "z-50 min-w-40 overflow-hidden rounded-md border border-border-primary bg-surface p-1 shadow-md outline-none",
77
+ "z-50 min-w-40 overflow-hidden",
78
+ popupSurface,
79
+ popupInset,
77
80
  motion.popupAnchor,
78
81
  className,
79
82
  )}
@@ -123,7 +126,7 @@ export const MenuGroupLabel = ({
123
126
  ...props
124
127
  }: MenuGroupLabelProps) => (
125
128
  <BaseMenu.GroupLabel
126
- className={cn("px-2.5 py-1.5 text-xs text-fg-tertiary", className)}
129
+ className={cn(popupLabel, className)}
127
130
  {...props}
128
131
  />
129
132
  )
package/src/meter.tsx CHANGED
@@ -53,7 +53,7 @@ export const MeterIndicator = ({
53
53
  }: MeterIndicatorProps) => (
54
54
  <BaseMeter.Indicator
55
55
  className={cn(
56
- "h-full rounded-full bg-brand-primary transition-[width] duration-300 ease-out motion-reduce:transition-none",
56
+ "h-full rounded-full bg-brand-primary transition-[width] duration-[var(--duration-lg)] ease-enter motion-reduce:transition-none",
57
57
  className,
58
58
  )}
59
59
  {...props}
@@ -5,6 +5,7 @@ import type { ComponentProps } from "react"
5
5
  import { IconChevronDownSmall } from "./icons"
6
6
  import { cn } from "./lib/cn"
7
7
  import { motion } from "./lib/motion"
8
+ import { popupSurface } from "./lib/popup"
8
9
 
9
10
  export type NavigationMenuProps = ComponentProps<typeof BaseNavigationMenu.Root>
10
11
  export type NavigationMenuListProps = ComponentProps<
@@ -101,7 +102,7 @@ export const NavigationMenuIcon = ({
101
102
  }: NavigationMenuIconProps) => (
102
103
  <BaseNavigationMenu.Icon
103
104
  className={cn(
104
- "inline-flex text-fg-tertiary transition-transform duration-150 ease-out motion-reduce:transition-none data-popup-open:rotate-180",
105
+ "inline-flex text-fg-tertiary transition-transform duration-[var(--duration-sm)] ease-enter motion-reduce:transition-none data-popup-open:rotate-180",
105
106
  className,
106
107
  )}
107
108
  {...props}
@@ -177,8 +178,9 @@ export const NavigationMenuPopup = ({
177
178
  }: NavigationMenuPopupProps) => (
178
179
  <BaseNavigationMenu.Popup
179
180
  className={cn(
180
- "relative z-50 w-[var(--popup-width)] overflow-hidden rounded-xl border border-border-primary bg-surface shadow-md outline-none",
181
- "h-[var(--popup-height)] transition-[width,height,opacity] duration-200 ease-out motion-reduce:transition-none",
181
+ "relative z-50 w-[var(--popup-width)] overflow-hidden",
182
+ popupSurface,
183
+ "h-[var(--popup-height)] transition-[width,height,opacity] duration-[var(--duration-md)] ease-enter motion-reduce:transition-none",
182
184
  "data-starting-style:opacity-0 data-ending-style:opacity-0",
183
185
  className,
184
186
  )}
package/src/popover.tsx CHANGED
@@ -4,6 +4,7 @@ import { Popover as BasePopover } from "@base-ui/react/popover"
4
4
  import type { ComponentProps } from "react"
5
5
  import { cn } from "./lib/cn"
6
6
  import { motion } from "./lib/motion"
7
+ import { popupSurface } from "./lib/popup"
7
8
 
8
9
  export type PopoverProps = ComponentProps<typeof BasePopover.Root>
9
10
  export type PopoverTriggerProps = ComponentProps<typeof BasePopover.Trigger>
@@ -47,7 +48,8 @@ export const PopoverPositioner = ({
47
48
  export const PopoverPopup = ({ className, ...props }: PopoverPopupProps) => (
48
49
  <BasePopover.Popup
49
50
  className={cn(
50
- "z-50 flex w-72 flex-col gap-1 rounded-xl border border-border-primary bg-surface p-4 shadow-md outline-none",
51
+ "z-50 flex w-72 flex-col gap-1 p-4",
52
+ popupSurface,
51
53
  motion.popupAnchor,
52
54
  className,
53
55
  )}
@@ -4,6 +4,7 @@ import { PreviewCard as BasePreviewCard } from "@base-ui/react/preview-card"
4
4
  import type { ComponentProps } from "react"
5
5
  import { cn } from "./lib/cn"
6
6
  import { motion } from "./lib/motion"
7
+ import { popupSurface } from "./lib/popup"
7
8
 
8
9
  export type PreviewCardProps = ComponentProps<typeof BasePreviewCard.Root>
9
10
  export type PreviewCardTriggerProps = ComponentProps<
@@ -67,7 +68,8 @@ export const PreviewCardPopup = ({
67
68
  }: PreviewCardPopupProps) => (
68
69
  <BasePreviewCard.Popup
69
70
  className={cn(
70
- "z-50 w-72 overflow-hidden rounded-xl border border-border-primary bg-surface p-4 shadow-md outline-none",
71
+ "z-50 w-72 overflow-hidden p-4",
72
+ popupSurface,
71
73
  motion.popupAnchor,
72
74
  className,
73
75
  )}
package/src/progress.tsx CHANGED
@@ -52,7 +52,7 @@ export const ProgressIndicator = ({
52
52
  }: ProgressIndicatorProps) => (
53
53
  <BaseProgress.Indicator
54
54
  className={cn(
55
- "h-full rounded-full bg-brand-primary transition-[width] duration-300 ease-out motion-reduce:transition-none",
55
+ "h-full rounded-full bg-brand-primary transition-[width] duration-[var(--duration-lg)] ease-enter motion-reduce:transition-none",
56
56
  className,
57
57
  )}
58
58
  {...props}
@@ -49,7 +49,7 @@ export const ScrollAreaScrollbar = ({
49
49
  }: ScrollAreaScrollbarProps) => (
50
50
  <BaseScrollArea.Scrollbar
51
51
  className={cn(
52
- "flex touch-none p-0.5 opacity-100 transition-opacity duration-150 ease-out motion-reduce:transition-none data-[orientation=horizontal]:h-2 data-[orientation=vertical]:w-2",
52
+ "flex touch-none p-0.5 opacity-100 transition-opacity duration-[var(--duration-sm)] ease-enter motion-reduce:transition-none data-[orientation=horizontal]:h-2 data-[orientation=vertical]:w-2",
53
53
  className,
54
54
  )}
55
55
  {...props}
package/src/select.tsx CHANGED
@@ -5,6 +5,7 @@ import type { ComponentProps } from "react"
5
5
  import { IconChevronDownSmall } from "./icons"
6
6
  import { cn } from "./lib/cn"
7
7
  import { motion } from "./lib/motion"
8
+ import { popupInset, popupItem, popupLabel, popupSurface } from "./lib/popup"
8
9
 
9
10
  export type SelectProps = ComponentProps<typeof BaseSelect.Root>
10
11
  export type SelectTriggerProps = ComponentProps<typeof BaseSelect.Trigger>
@@ -33,7 +34,7 @@ export const SelectTrigger = ({
33
34
  className={cn(
34
35
  "flex h-9 w-full cursor-pointer items-center justify-between gap-2 rounded-md border border-border-secondary bg-surface px-3 text-sm text-fg-primary inset-shadow-outline-top outline-none",
35
36
  motion.all,
36
- "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 data-disabled:cursor-not-allowed data-disabled:opacity-50 data-placeholder:text-fg-quaternary data-popup-open:border-border-secondary data-popup-open:bg-background-tertiary/60",
37
+ "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 data-disabled:cursor-not-allowed data-disabled:opacity-50 data-placeholder:text-fg-quaternary data-popup-open:border-border-secondary",
37
38
  className,
38
39
  )}
39
40
  {...props}
@@ -71,7 +72,7 @@ export const SelectPortal = (props: SelectPortalProps) => (
71
72
 
72
73
  export const SelectPositioner = ({
73
74
  alignItemWithTrigger = false,
74
- sideOffset = 4,
75
+ sideOffset = 6,
75
76
  className,
76
77
  ...props
77
78
  }: SelectPositionerProps) => (
@@ -86,7 +87,8 @@ export const SelectPositioner = ({
86
87
  export const SelectPopup = ({ className, ...props }: SelectPopupProps) => (
87
88
  <BaseSelect.Popup
88
89
  className={cn(
89
- "z-50 max-h-[min(24rem,var(--available-height))] w-[var(--anchor-width)] overflow-hidden rounded-md border border-border-primary bg-surface shadow-md outline-none",
90
+ "z-50 max-h-[min(24rem,var(--available-height))] w-[var(--anchor-width)] overflow-hidden",
91
+ popupSurface,
90
92
  motion.popupAnchor,
91
93
  className,
92
94
  )}
@@ -96,7 +98,7 @@ export const SelectPopup = ({ className, ...props }: SelectPopupProps) => (
96
98
 
97
99
  export const SelectList = ({ className, ...props }: SelectListProps) => (
98
100
  <BaseSelect.List
99
- className={cn("max-h-[inherit] overflow-y-auto p-1 outline-none", className)}
101
+ className={cn("max-h-[inherit] overflow-y-auto outline-none", popupInset, className)}
100
102
  {...props}
101
103
  />
102
104
  )
@@ -104,7 +106,7 @@ export const SelectList = ({ className, ...props }: SelectListProps) => (
104
106
  export const SelectItem = ({ className, ...props }: SelectItemProps) => (
105
107
  <BaseSelect.Item
106
108
  className={cn(
107
- "flex min-h-8 cursor-default items-center gap-2 rounded-xs px-2.5 py-1.5 text-sm text-fg-primary outline-none select-none",
109
+ popupItem,
108
110
  motion.colors,
109
111
  "data-disabled:cursor-not-allowed data-disabled:opacity-50 data-highlighted:bg-background-tertiary data-selected:text-fg-primary",
110
112
  className,
@@ -145,7 +147,7 @@ export const SelectGroupLabel = ({
145
147
  ...props
146
148
  }: SelectGroupLabelProps) => (
147
149
  <BaseSelect.GroupLabel
148
- className={cn("px-2.5 py-1.5 text-xs text-fg-tertiary", className)}
150
+ className={cn(popupLabel, className)}
149
151
  {...props}
150
152
  />
151
153
  )
package/src/slider.tsx CHANGED
@@ -38,7 +38,7 @@ export const SliderIndicator = ({
38
38
  }: SliderIndicatorProps) => (
39
39
  <BaseSlider.Indicator
40
40
  className={cn(
41
- "absolute h-full rounded-full bg-brand-primary transition-[width] duration-150 ease-out motion-reduce:transition-none",
41
+ "absolute h-full rounded-full bg-brand-primary transition-[width] duration-[var(--duration-sm)] ease-enter motion-reduce:transition-none",
42
42
  className,
43
43
  )}
44
44
  {...props}
package/src/switch.tsx CHANGED
@@ -9,11 +9,11 @@ export type SwitchProps = ComponentProps<typeof BaseSwitch.Root>
9
9
  export const Switch = ({ className, ...props }: SwitchProps) => (
10
10
  <BaseSwitch.Root
11
11
  className={cn(
12
- "relative inline-flex h-6 w-10 shrink-0 items-center rounded-full bg-background-quaternary transition-colors duration-150 ease-out motion-reduce:transition-none 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-checked:bg-brand-primary data-disabled:cursor-not-allowed data-disabled:opacity-50",
12
+ "relative inline-flex h-6 w-10 shrink-0 items-center rounded-full bg-background-quaternary transition-colors duration-[var(--duration-sm)] ease-enter motion-reduce:transition-none 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-checked:bg-brand-primary data-disabled:cursor-not-allowed data-disabled:opacity-50",
13
13
  className,
14
14
  )}
15
15
  {...props}
16
16
  >
17
- <BaseSwitch.Thumb className="pointer-events-none block size-5 translate-x-0.5 rounded-full bg-surface shadow-sm transition-transform duration-150 ease-out motion-reduce:transition-none data-checked:translate-x-[1.125rem]" />
17
+ <BaseSwitch.Thumb className="pointer-events-none block size-5 translate-x-0.5 rounded-full bg-surface shadow-sm transition-transform duration-[var(--duration-sm)] ease-enter motion-reduce:transition-none data-checked:translate-x-[1.125rem]" />
18
18
  </BaseSwitch.Root>
19
19
  )
@@ -45,7 +45,6 @@ export const TextAnimate = ({
45
45
 
46
46
  useEffect(() => {
47
47
  let cancelled = false
48
- let frame = 0
49
48
  let timeoutId: ReturnType<typeof setTimeout> | undefined
50
49
 
51
50
  const start = () => {
@@ -114,7 +113,6 @@ export const TextAnimate = ({
114
113
  return () => {
115
114
  cancelled = true
116
115
  if (timeoutId) clearTimeout(timeoutId)
117
- cancelAnimationFrame(frame)
118
116
  }
119
117
  }, [chars, delay, effect, replay, speed, text])
120
118
 
@@ -124,9 +122,9 @@ export const TextAnimate = ({
124
122
  className={cn(
125
123
  "inline-block text-fg-primary",
126
124
  effect === "fade" &&
127
- "animate-[text-fade-in_0.6s_ease-out_both] motion-reduce:animate-none",
125
+ "animate-[text-fade-in_0.6s_var(--ease-enter)_both] motion-reduce:animate-none",
128
126
  effect === "blur" &&
129
- "animate-[text-blur-in_0.7s_ease-out_both] motion-reduce:animate-none",
127
+ "animate-[text-blur-in_0.7s_var(--ease-enter)_both] motion-reduce:animate-none",
130
128
  className,
131
129
  )}
132
130
  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: {
package/src/toast.tsx CHANGED
@@ -41,7 +41,7 @@ export const ToastRoot = ({ className, ...props }: ToastRootProps) => (
41
41
  className={cn(
42
42
  "relative z-[calc(1000-var(--toast-index))] mb-2 box-border w-full rounded-xl border border-border-primary bg-surface p-4 shadow-lg outline-none",
43
43
  "h-[var(--toast-frontmost-height,var(--toast-height))]",
44
- "transition-[transform,opacity] duration-200 ease-out motion-reduce:transition-none",
44
+ "transition-[transform,opacity] duration-[var(--duration-md)] ease-enter motion-reduce:transition-none",
45
45
  "data-starting-style:opacity-0 data-starting-style:translate-y-2",
46
46
  "data-ending-style:opacity-0 data-ending-style:translate-y-2",
47
47
  "data-expanded:h-[var(--toast-height)] data-expanded:translate-y-[var(--toast-offset-y)]",
@@ -56,7 +56,7 @@ export const ToastRoot = ({ className, ...props }: ToastRootProps) => (
56
56
  export const ToastContent = ({ className, ...props }: ToastContentProps) => (
57
57
  <BaseToast.Content
58
58
  className={cn(
59
- "flex flex-col gap-1 overflow-hidden transition-opacity duration-150",
59
+ "flex flex-col gap-1 overflow-hidden transition-opacity duration-[var(--duration-sm)]",
60
60
  "data-behind:pointer-events-none data-behind:opacity-0 data-expanded:data-behind:opacity-100",
61
61
  className,
62
62
  )}