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

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,118 @@
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
+ * These carry separate enter and exit behaviour. Base UI flags the exit
19
+ * with `data-ending-style`, and the `data-ending-style:` variants below
20
+ * re-declare duration and easing for that direction. Exits always run at
21
+ * `--duration-exit`, so dismissing anything costs the same no matter how
22
+ * long the entrance took.
23
+ *
24
+ * State tokens — tabsIndicator, colors, transform, all.
25
+ * These animate between states on a mounted element, so there is no enter
26
+ * or exit to distinguish; they take a single curve.
27
+ *
28
+ * Fades are linear on purpose. Perceived brightness is already non-linear, so
29
+ * easing an opacity change makes it look like it sticks at the end.
5
30
  */
31
+
32
+ const DURATION = {
33
+ xs: "duration-[var(--duration-xs)]",
34
+ sm: "duration-[var(--duration-sm)]",
35
+ md: "duration-[var(--duration-md)]",
36
+ lg: "duration-[var(--duration-lg)]",
37
+ } as const
38
+
39
+ /** Every exit runs at the same length — see `--duration-exit`. */
40
+ const EXIT_DURATION = "data-ending-style:duration-[var(--duration-exit)]"
41
+
42
+ const REDUCED = "motion-reduce:transition-none"
43
+
6
44
  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",
45
+ /** Overlay scrims. Linear both ways — see the note on fades above. */
46
+ backdrop: [
47
+ "transition-opacity",
48
+ DURATION.sm,
49
+ "ease-linear",
50
+ REDUCED,
51
+ "data-starting-style:opacity-0",
52
+ "data-ending-style:opacity-0",
53
+ EXIT_DURATION,
54
+ "data-ending-style:ease-linear",
55
+ ].join(" "),
56
+
57
+ /** Centred dialog / alert — animates `scale`, so translate centring holds. */
58
+ popupCenter: [
59
+ "transition-[scale,opacity]",
60
+ DURATION.md,
61
+ "ease-enter",
62
+ REDUCED,
63
+ "data-starting-style:scale-[0.96] data-starting-style:opacity-0",
64
+ "data-ending-style:scale-[0.96] data-ending-style:opacity-0",
65
+ EXIT_DURATION,
66
+ "data-ending-style:ease-move",
67
+ ].join(" "),
68
+
69
+ /** Select, tooltip, menus — transform-origin comes from Base UI. */
70
+ popupAnchor: [
71
+ "origin-[var(--transform-origin)]",
72
+ "transition-[transform,scale,opacity]",
73
+ DURATION.sm,
74
+ "ease-enter",
75
+ REDUCED,
76
+ "data-starting-style:scale-[0.96] data-starting-style:opacity-0",
77
+ "data-ending-style:scale-[0.96] data-ending-style:opacity-0",
78
+ EXIT_DURATION,
79
+ "data-ending-style:ease-move",
80
+ ].join(" "),
81
+
82
+ /** Height expand and collapse. */
83
+ accordionPanel: [
84
+ "transition-[height]",
85
+ DURATION.md,
86
+ "ease-move",
87
+ REDUCED,
88
+ "data-starting-style:h-0",
89
+ "data-ending-style:h-0",
90
+ EXIT_DURATION,
91
+ "data-ending-style:ease-move",
92
+ ].join(" "),
93
+
94
+ /** Sliding tab underline or pill — follows a pointer, so it snaps. */
95
+ tabsIndicator: [
96
+ "transition-[translate,width]",
97
+ DURATION.md,
98
+ "ease-snap",
99
+ REDUCED,
100
+ ].join(" "),
101
+
102
+ /** Hover and pressed colour changes. */
103
+ colors: ["transition-colors", DURATION.sm, "ease-enter", REDUCED].join(" "),
104
+
105
+ /** Icon rotates and simple transforms. */
106
+ transform: [
107
+ "transition-transform",
108
+ DURATION.sm,
109
+ "ease-move",
110
+ REDUCED,
111
+ ].join(" "),
112
+
113
+ /** Every property. Reach for a narrower token first. */
114
+ all: ["transition-all", DURATION.sm, "ease-enter", REDUCED].join(" "),
115
+
116
+ /** Long, ambient movement — progress fills, meters, background shifts. */
117
+ passive: ["transition-all", DURATION.lg, "ease-passive", REDUCED].join(" "),
23
118
  } as const
package/src/menu.tsx CHANGED
@@ -34,7 +34,7 @@ export type MenuSubmenuTriggerProps = ComponentProps<
34
34
  export type MenuViewportProps = ComponentProps<typeof BaseMenu.Viewport>
35
35
 
36
36
  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",
37
+ "flex h-8 cursor-default items-center gap-2 rounded-sm px-2.5 py-1.5 text-sm text-fg-primary outline-none select-none",
38
38
  motion.colors,
39
39
  "data-disabled:cursor-not-allowed data-disabled:opacity-50 data-highlighted:bg-background-tertiary",
40
40
  )
@@ -59,7 +59,7 @@ export const MenuBackdrop = ({ className, ...props }: MenuBackdropProps) => (
59
59
  )
60
60
 
61
61
  export const MenuPositioner = ({
62
- sideOffset = 4,
62
+ sideOffset = 8,
63
63
  className,
64
64
  ...props
65
65
  }: MenuPositionerProps) => (
@@ -73,7 +73,7 @@ export const MenuPositioner = ({
73
73
  export const MenuPopup = ({ className, ...props }: MenuPopupProps) => (
74
74
  <BaseMenu.Popup
75
75
  className={cn(
76
- "z-50 min-w-40 overflow-hidden rounded-md border border-border-primary bg-surface p-1 shadow-md outline-none",
76
+ "z-50 min-w-40 overflow-hidden rounded-xl border border-[var(--border-faint)] bg-surface p-1.5 shadow-lg outline-none",
77
77
  motion.popupAnchor,
78
78
  className,
79
79
  )}
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}
@@ -101,7 +101,7 @@ export const NavigationMenuIcon = ({
101
101
  }: NavigationMenuIconProps) => (
102
102
  <BaseNavigationMenu.Icon
103
103
  className={cn(
104
- "inline-flex text-fg-tertiary transition-transform duration-150 ease-out motion-reduce:transition-none data-popup-open:rotate-180",
104
+ "inline-flex text-fg-tertiary transition-transform duration-[var(--duration-sm)] ease-enter motion-reduce:transition-none data-popup-open:rotate-180",
105
105
  className,
106
106
  )}
107
107
  {...props}
@@ -178,7 +178,7 @@ export const NavigationMenuPopup = ({
178
178
  <BaseNavigationMenu.Popup
179
179
  className={cn(
180
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
+ "h-[var(--popup-height)] transition-[width,height,opacity] duration-[var(--duration-md)] ease-enter motion-reduce:transition-none",
182
182
  "data-starting-style:opacity-0 data-ending-style:opacity-0",
183
183
  className,
184
184
  )}
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/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
  )
@@ -122,9 +122,9 @@ export const TextAnimate = ({
122
122
  className={cn(
123
123
  "inline-block text-fg-primary",
124
124
  effect === "fade" &&
125
- "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",
126
126
  effect === "blur" &&
127
- "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",
128
128
  className,
129
129
  )}
130
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
  )}