@boyernick/standard-ui-react 0.1.1-canary.1 → 0.1.1-canary.11

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.
@@ -1,167 +1,245 @@
1
- import type { SVGProps } from "react"
1
+ import type { ComponentProps, ReactNode } from "react"
2
2
  import { cn } from "./lib/cn"
3
3
 
4
- export type IllustrationProps = SVGProps<SVGSVGElement>
4
+ /**
5
+ * Small, composition-based illustrations for empty states.
6
+ *
7
+ * Built from divs and theme tokens rather than SVG paths, so they follow the
8
+ * palette into dark mode instead of carrying baked-in colours. They are
9
+ * deliberately abstract and monochrome — a miniature of the thing that is
10
+ * missing, not a picture about it. Nothing here uses a brand colour: an empty
11
+ * state is a quiet moment, and an accent turns it into an announcement.
12
+ */
5
13
 
6
- const illustrationProps = {
7
- viewBox: "0 0 240 160",
8
- fill: "none",
9
- xmlns: "http://www.w3.org/2000/svg",
10
- } as const
14
+ export type IllustrationProps = ComponentProps<"div">
11
15
 
12
- export const IllustrationEmpty = ({
13
- className,
14
- ...props
15
- }: IllustrationProps) => (
16
- <svg
17
- {...illustrationProps}
18
- className={cn("text-fg-tertiary", className)}
16
+ /** Fixed 64×48 stage, so a row of these lines up without ad-hoc sizing. */
17
+ const Frame = ({ className, children, ...props }: IllustrationProps) => (
18
+ <div
19
+ className={cn(
20
+ "inline-flex h-12 w-16 shrink-0 items-center justify-center",
21
+ className,
22
+ )}
19
23
  aria-hidden
20
24
  {...props}
21
25
  >
22
- <path
23
- d="M52 65 79 40h82l27 25v55H52V65Z"
24
- className="fill-background-secondary"
25
- stroke="currentColor"
26
- strokeWidth="2"
27
- />
28
- <path
29
- d="M52 65h45l8 13h30l8-13h45"
30
- stroke="currentColor"
31
- strokeWidth="2"
32
- strokeLinejoin="round"
33
- />
34
- <path
35
- d="M97 31h46"
36
- className="text-brand-primary"
37
- stroke="currentColor"
38
- strokeWidth="4"
39
- strokeLinecap="round"
40
- />
41
- <circle cx="120" cy="104" r="4" className="fill-brand-primary" />
42
- </svg>
26
+ {children}
27
+ </div>
43
28
  )
44
29
 
45
- export const IllustrationError = ({
30
+ /** A sheet. The hairline and radius are sub-pixel on purpose — at this size a
31
+ * 1px border reads as a heavy outline rather than an edge. */
32
+ const Card = ({
33
+ className,
34
+ children,
35
+ }: {
36
+ className?: string
37
+ children?: ReactNode
38
+ }) => (
39
+ <div
40
+ className={cn(
41
+ "relative flex shrink-0 flex-col overflow-hidden rounded-[3px] border-[0.5px] border-border-primary bg-surface p-1 shadow-hairline",
42
+ className,
43
+ )}
44
+ >
45
+ {children}
46
+ </div>
47
+ )
48
+
49
+ /** A line of content. */
50
+ const Bar = ({ width }: { width: string }) => (
51
+ <div
52
+ className="h-[3px] shrink-0 rounded-[1px] bg-background-tertiary"
53
+ style={{ width }}
54
+ />
55
+ )
56
+
57
+ /** Nothing here yet — a sheet with no content on it. */
58
+ export const IllustrationEmpty = ({ className, ...props }: IllustrationProps) => (
59
+ <Frame className={className} {...props}>
60
+ <Card className="h-10 w-8" />
61
+ </Frame>
62
+ )
63
+
64
+ /** Something to look through — a sheet behind a lens. */
65
+ export const IllustrationSearch = ({
46
66
  className,
47
67
  ...props
48
68
  }: IllustrationProps) => (
49
- <svg
50
- {...illustrationProps}
51
- className={cn("text-fg-tertiary", className)}
52
- aria-hidden
53
- {...props}
54
- >
55
- <rect
56
- x="47"
57
- y="28"
58
- width="146"
59
- height="104"
60
- rx="16"
61
- className="fill-background-secondary"
62
- stroke="currentColor"
63
- strokeWidth="2"
64
- />
65
- <path
66
- d="M47 54h146"
67
- stroke="currentColor"
68
- strokeWidth="2"
69
- />
70
- <circle cx="63" cy="41" r="3" className="fill-brand-primary" />
71
- <circle cx="74" cy="41" r="3" fill="currentColor" opacity=".45" />
72
- <circle cx="85" cy="41" r="3" fill="currentColor" opacity=".25" />
73
- <path
74
- d="m103 78 34 34m0-34-34 34"
75
- className="text-brand-primary"
76
- stroke="currentColor"
77
- strokeWidth="5"
78
- strokeLinecap="round"
79
- />
80
- </svg>
69
+ <Frame className={className} {...props}>
70
+ <Card className="h-10 w-8 gap-[3px]">
71
+ <Bar width="80%" />
72
+ <Bar width="55%" />
73
+ <Bar width="35%" />
74
+ </Card>
75
+ <div className="-ml-2.5 mt-4 size-4 rounded-full border-[1.5px] border-border-secondary bg-surface" />
76
+ </Frame>
81
77
  )
82
78
 
83
- export const IllustrationSuccess = ({
79
+ /** Something went wrong — the stack knocked out of true. */
80
+ export const IllustrationError = ({ className, ...props }: IllustrationProps) => (
81
+ <Frame className={className} {...props}>
82
+ <Card className="h-9 w-7 -rotate-6" />
83
+ <Card className="-ml-3 h-10 w-8 rotate-[7deg] gap-[3px]">
84
+ <Bar width="70%" />
85
+ <Bar width="45%" />
86
+ </Card>
87
+ </Frame>
88
+ )
89
+
90
+ /** Somewhere to put a file — a sheet going into a folder.
91
+ *
92
+ * Two earlier attempts failed for the same reason: an arrow stamped on a
93
+ * sheet, then a sheet on a plain grey slab, both read as a document with
94
+ * something next to it. A folder is its own silhouette — tab, body, a sheet
95
+ * tucked behind the front — so the arrangement carries the meaning and no
96
+ * symbol has to. It is also the one shape here that is not another sheet,
97
+ * which keeps it from reading as the error stack. */
98
+ export const IllustrationUpload = ({
84
99
  className,
85
100
  ...props
86
101
  }: IllustrationProps) => (
87
- <svg
88
- {...illustrationProps}
89
- className={cn("text-fg-tertiary", className)}
90
- aria-hidden
91
- {...props}
92
- >
93
- <circle
94
- cx="120"
95
- cy="80"
96
- r="53"
97
- className="fill-background-secondary"
98
- stroke="currentColor"
99
- strokeWidth="2"
100
- />
101
- <circle
102
- cx="120"
103
- cy="80"
104
- r="37"
105
- className="fill-brand-primary text-brand-primary"
106
- stroke="currentColor"
107
- strokeWidth="2"
108
- />
109
- <path
110
- d="m99 80 14 14 29-31"
111
- className="text-brand-foreground"
112
- stroke="currentColor"
113
- strokeWidth="6"
114
- strokeLinecap="round"
115
- strokeLinejoin="round"
116
- />
117
- <path
118
- d="M63 42h13M69.5 35.5v13M168 118h13m-6.5-6.5v13"
119
- className="text-brand-primary"
120
- stroke="currentColor"
121
- strokeWidth="3"
122
- strokeLinecap="round"
123
- />
124
- </svg>
102
+ <Frame className={className} {...props}>
103
+ <div className="relative h-11 w-12">
104
+ {/* Tucked deep enough that the folder front cuts across it. */}
105
+ <Card className="absolute inset-x-[13px] top-0 h-8 -rotate-6 gap-[3px]">
106
+ <Bar width="70%" />
107
+ <Bar width="45%" />
108
+ </Card>
109
+ {/* Laps 1px into the body so their borders read as one outline. */}
110
+ <div className="absolute bottom-[21px] left-1 h-1.5 w-4 rounded-t-[2px] border-x-[0.5px] border-t-[0.5px] border-border-secondary bg-background-tertiary" />
111
+ <div className="absolute inset-x-1 bottom-0 h-[22px] rounded-[3px] border-[0.5px] border-border-secondary bg-background-tertiary" />
112
+ </div>
113
+ </Frame>
125
114
  )
126
115
 
127
- export const IllustrationSearch = ({
116
+ /** Done — a sheet carrying a mark. */
117
+ export const IllustrationSuccess = ({
128
118
  className,
129
119
  ...props
130
120
  }: IllustrationProps) => (
131
- <svg
132
- {...illustrationProps}
133
- className={cn("text-fg-tertiary", className)}
121
+ <Frame className={className} {...props}>
122
+ <Card className="h-10 w-8 items-center justify-center">
123
+ {/* A check is cheaper as two borders on a rotated box than as a path. */}
124
+ <span className="mb-0.5 h-3 w-1.5 rotate-45 border-r-[1.5px] border-b-[1.5px] border-fg-tertiary" />
125
+ </Card>
126
+ </Frame>
127
+ )
128
+
129
+ /* -------------------------------------------------------------------------
130
+ * File types
131
+ *
132
+ * A second, smaller family. These sit inline — in an attachment row, a list,
133
+ * a preview slot — so they are a bare sheet at 24×30 rather than a hero on a
134
+ * 64×48 stage. Same grammar: a sheet, and marks on it standing in for whatever
135
+ * the file holds.
136
+ *
137
+ * Presentational, like Kbd: they render the type you name. Mapping an
138
+ * extension to a type is the consumer's job, since only the consumer knows
139
+ * what it wants to do with the ones it does not recognise.
140
+ * ---------------------------------------------------------------------- */
141
+
142
+ export type FileIllustrationType =
143
+ | "document"
144
+ | "image"
145
+ | "spreadsheet"
146
+ | "presentation"
147
+ | "video"
148
+ | "audio"
149
+ | "code"
150
+ | "archive"
151
+ | "generic"
152
+
153
+ export type IllustrationFileProps = IllustrationProps & {
154
+ type?: FileIllustrationType
155
+ }
156
+
157
+ /** A block of colour standing in for content rather than a line of it. */
158
+ const Block = ({ className }: { className?: string }) => (
159
+ <div className={cn("rounded-[1px] bg-background-tertiary", className)} />
160
+ )
161
+
162
+ const marks: Record<FileIllustrationType, ReactNode> = {
163
+ document: (
164
+ <>
165
+ <Bar width="100%" />
166
+ <Bar width="100%" />
167
+ <Bar width="60%" />
168
+ </>
169
+ ),
170
+ // A horizon and a sun — the smallest arrangement that reads as a picture.
171
+ image: (
172
+ <div className="relative mt-auto h-3.5 w-full">
173
+ <Block className="absolute top-0 right-1 size-1.5 rounded-full" />
174
+ <Block className="absolute inset-x-0 bottom-0 h-2 [clip-path:polygon(0_100%,38%_28%,62%_100%)]" />
175
+ <Block className="absolute right-0 bottom-0 h-1.5 w-1/2 [clip-path:polygon(0_100%,50%_20%,100%_100%)]" />
176
+ </div>
177
+ ),
178
+ spreadsheet: (
179
+ <div className="grid h-full w-full grid-cols-3 grid-rows-3 gap-[1.5px]">
180
+ {Array.from({ length: 9 }, (_, i) => (
181
+ <Block key={i} />
182
+ ))}
183
+ </div>
184
+ ),
185
+ presentation: (
186
+ <>
187
+ <Block className="h-2.5 w-full" />
188
+ <Bar width="70%" />
189
+ </>
190
+ ),
191
+ // A play triangle, cut from a block so it keeps the same fill as everything else.
192
+ video: (
193
+ <div className="m-auto size-3">
194
+ <Block className="size-full [clip-path:polygon(22%_10%,90%_50%,22%_90%)]" />
195
+ </div>
196
+ ),
197
+ audio: (
198
+ <div className="m-auto flex h-3.5 items-center gap-[2px]">
199
+ <Block className="h-1.5 w-[2px]" />
200
+ <Block className="h-3.5 w-[2px]" />
201
+ <Block className="h-2.5 w-[2px]" />
202
+ <Block className="h-3.5 w-[2px]" />
203
+ <Block className="h-1.5 w-[2px]" />
204
+ </div>
205
+ ),
206
+ code: (
207
+ <>
208
+ <Bar width="55%" />
209
+ <div className="flex w-full pl-1.5">
210
+ <Bar width="70%" />
211
+ </div>
212
+ <div className="flex w-full pl-1.5">
213
+ <Bar width="50%" />
214
+ </div>
215
+ <Bar width="40%" />
216
+ </>
217
+ ),
218
+ // A stack, since an archive is other files in a coat.
219
+ archive: (
220
+ <div className="m-auto flex flex-col gap-[2px]">
221
+ <Block className="h-1.5 w-2.5" />
222
+ <Block className="h-1.5 w-2.5" />
223
+ <Block className="h-1.5 w-2.5" />
224
+ </div>
225
+ ),
226
+ generic: null,
227
+ }
228
+
229
+ /** One file, drawn as the kind of thing it holds. */
230
+ export const IllustrationFile = ({
231
+ type = "generic",
232
+ className,
233
+ ...props
234
+ }: IllustrationFileProps) => (
235
+ <div
236
+ className={cn(
237
+ "relative flex h-[30px] w-6 shrink-0 flex-col gap-[3px] overflow-hidden rounded-[3px] border-[0.5px] border-border-primary bg-surface p-1 shadow-hairline",
238
+ className,
239
+ )}
134
240
  aria-hidden
135
241
  {...props}
136
242
  >
137
- <path
138
- d="M42 38h113a12 12 0 0 1 12 12v69H42V38Z"
139
- className="fill-background-secondary"
140
- stroke="currentColor"
141
- strokeWidth="2"
142
- />
143
- <path d="M42 58h125" stroke="currentColor" strokeWidth="2" />
144
- <path
145
- d="M59 76h48M59 89h34M59 102h53"
146
- stroke="currentColor"
147
- strokeWidth="4"
148
- strokeLinecap="round"
149
- opacity=".5"
150
- />
151
- <circle
152
- cx="155"
153
- cy="91"
154
- r="27"
155
- className="fill-surface text-brand-primary"
156
- stroke="currentColor"
157
- strokeWidth="5"
158
- />
159
- <path
160
- d="m174 111 22 22"
161
- className="text-brand-primary"
162
- stroke="currentColor"
163
- strokeWidth="7"
164
- strokeLinecap="round"
165
- />
166
- </svg>
243
+ {marks[type]}
244
+ </div>
167
245
  )
package/src/index.ts CHANGED
@@ -1,5 +1,12 @@
1
1
  export { Button, buttonVariants, type ButtonProps } from "./button";
2
2
  export { Input, inputVariants, type InputProps } from "./input";
3
+ export {
4
+ Kbd,
5
+ KbdGroup,
6
+ kbdVariants,
7
+ type KbdGroupProps,
8
+ type KbdProps,
9
+ } from "./kbd";
3
10
  export { Badge, badgeVariants, type BadgeProps } from "./badge";
4
11
  export { Switch, type SwitchProps } from "./switch";
5
12
  export { Checkbox, type CheckboxProps } from "./checkbox";
@@ -92,17 +99,27 @@ export {
92
99
  type AvatarFallbackProps,
93
100
  } from "./avatar";
94
101
  export { Calendar, type CalendarProps } from "./calendar";
102
+ export {
103
+ DatePicker,
104
+ type DatePickerProps,
105
+ type DatePickerSingleProps,
106
+ type DatePickerRangeProps,
107
+ } from "./date-picker";
108
+ export type { DateRange } from "react-day-picker";
95
109
  export {
96
110
  Card,
97
111
  CardHeader,
98
112
  CardTitle,
99
113
  CardDescription,
114
+ CardAction,
100
115
  CardContent,
101
116
  CardFooter,
117
+ cardVariants,
102
118
  type CardProps,
103
119
  type CardHeaderProps,
104
120
  type CardTitleProps,
105
121
  type CardDescriptionProps,
122
+ type CardActionProps,
106
123
  type CardContentProps,
107
124
  type CardFooterProps,
108
125
  } from "./card";
@@ -650,6 +667,7 @@ export {
650
667
  BreadcrumbLink,
651
668
  BreadcrumbPage,
652
669
  BreadcrumbSeparator,
670
+ breadcrumbSeparatorVariants,
653
671
  type BreadcrumbProps,
654
672
  type BreadcrumbListProps,
655
673
  type BreadcrumbItemProps,
@@ -773,6 +791,7 @@ export {
773
791
  TooltipPortal,
774
792
  TooltipPositioner,
775
793
  TooltipPopup,
794
+ tooltipPopupVariants,
776
795
  type TooltipProviderProps,
777
796
  type TooltipProps,
778
797
  type TooltipTriggerProps,
@@ -785,13 +804,19 @@ export {
785
804
  withCentralIconDefaults,
786
805
  iconGallery,
787
806
  IconBell,
807
+ IconCalendar1,
788
808
  IconChainLink1,
789
809
  IconCheckmark1,
790
810
  IconChevronBottom,
791
811
  IconChevronDownSmall,
792
812
  IconChevronRightSmall,
813
+ IconCircleCheck,
814
+ IconCircleInfo,
793
815
  IconClipboard,
794
816
  IconCrossSmall,
817
+ IconDotGrid1x3Horizontal,
818
+ IconExclamationCircle,
819
+ IconExclamationTriangle,
795
820
  IconHome,
796
821
  IconMagnifyingGlass,
797
822
  IconMinus,
@@ -800,6 +825,7 @@ export {
800
825
  IconPlus,
801
826
  IconSettingsGear1,
802
827
  IconSquareBehindSquare6,
828
+ IconStar,
803
829
  IconSun,
804
830
  IconX,
805
831
  type CentralIconProps,
@@ -821,7 +847,11 @@ export {
821
847
  IllustrationError,
822
848
  IllustrationSuccess,
823
849
  IllustrationSearch,
850
+ IllustrationUpload,
851
+ IllustrationFile,
824
852
  type IllustrationProps,
853
+ type IllustrationFileProps,
854
+ type FileIllustrationType,
825
855
  } from "./illustrations"
826
856
  export { BrandMark, BrandWordmark, type BrandMarkProps, type BrandWordmarkProps } from "./brand"
827
857
  export {
package/src/input.tsx CHANGED
@@ -5,7 +5,7 @@ import { forwardRef, type InputHTMLAttributes } from "react"
5
5
  import { cn } from "./lib/cn"
6
6
 
7
7
  const inputVariants = cva(
8
- "text-sm flex w-full cursor-text rounded-md text-fg-primary transition-[color,box-shadow] duration-150 ease-out placeholder:text-fg-quaternary outline-none aria-invalid:border-destructive disabled:cursor-not-allowed disabled:opacity-50",
8
+ "text-sm flex w-full cursor-text rounded-md text-fg-primary transition-[color,box-shadow] duration-[var(--duration-sm)] ease-enter placeholder:text-fg-quaternary outline-none aria-invalid:border-destructive disabled:cursor-not-allowed disabled:opacity-50",
9
9
  {
10
10
  variants: {
11
11
  variant: {
package/src/kbd.tsx ADDED
@@ -0,0 +1,46 @@
1
+ import { cva, type VariantProps } from "class-variance-authority"
2
+ import type { ComponentProps } from "react"
3
+ import { cn } from "./lib/cn"
4
+
5
+ const kbdVariants = cva(
6
+ // The border is bottom-only on purpose: it reads as the lip of a physical
7
+ // key without boxing the glyph in on all four sides, which at 12px turns a
8
+ // keycap into a cramped badge.
9
+ "inline-flex shrink-0 items-center justify-center border-b px-1 font-sans text-xs whitespace-nowrap select-none",
10
+ {
11
+ variants: {
12
+ variant: {
13
+ default: "border-border-primary bg-background-tertiary text-fg-secondary",
14
+ // The default fill is a light grey that vanishes on a dark surface, so
15
+ // an inverted keycap tints with the inverted foreground instead. It has
16
+ // to be that token rather than a literal `white`: the grey ramp flips
17
+ // under `.dark`, so a hardcoded white wash would sit invisibly on the
18
+ // now-light inverted surface.
19
+ inverted: "border-fg-inverted/10 bg-fg-inverted/10 text-fg-inverted",
20
+ },
21
+ size: {
22
+ sm: "h-4 min-w-3.5 rounded-2xs",
23
+ md: "h-5 min-w-4 rounded-xs",
24
+ },
25
+ },
26
+ defaultVariants: { variant: "default", size: "md" },
27
+ },
28
+ )
29
+
30
+ export type KbdProps = ComponentProps<"kbd"> & VariantProps<typeof kbdVariants>
31
+ export type KbdGroupProps = ComponentProps<"span">
32
+
33
+ /** A single keycap. Presentational — map "cmd" to ⌘ before it gets here. */
34
+ export const Kbd = ({ className, variant, size, ...props }: KbdProps) => (
35
+ <kbd className={cn(kbdVariants({ variant, size }), className)} {...props} />
36
+ )
37
+
38
+ /** A full shortcut. Keeps the caps on one line at a consistent gap. */
39
+ export const KbdGroup = ({ className, ...props }: KbdGroupProps) => (
40
+ <span
41
+ className={cn("inline-flex items-center gap-1 align-middle", className)}
42
+ {...props}
43
+ />
44
+ )
45
+
46
+ export { kbdVariants }
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