@boyernick/standard-ui-react 0.1.1-canary.3 → 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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@boyernick/standard-ui-react",
3
- "version": "0.1.1-canary.3",
3
+ "version": "0.1.1-canary.4",
4
4
  "description": "React components for StandardUI",
5
5
  "license": "MIT",
6
6
  "type": "module",
package/src/badge.tsx CHANGED
@@ -1,28 +1,92 @@
1
1
  import { cva, type VariantProps } from "class-variance-authority"
2
- import type { HTMLAttributes } from "react"
2
+ import type { HTMLAttributes, ReactNode } from "react"
3
3
  import { cn } from "./lib/cn"
4
4
 
5
5
  const badgeVariants = cva(
6
- "text-xs-strong inline-flex items-center rounded-full py-0.5",
6
+ "inline-flex shrink-0 items-center justify-center whitespace-nowrap align-middle [&>[data-badge-label]]:min-w-0 [&>[data-badge-label]]:truncate [&>[data-badge-slot]]:inline-flex [&>[data-badge-slot]]:shrink-0 [&>[data-badge-slot]]:items-center [&>[data-badge-slot]]:justify-center [&>[data-badge-slot]>svg]:size-full",
7
7
  {
8
8
  variants: {
9
9
  variant: {
10
- default: "bg-background-tertiary px-3 text-fg-primary",
11
- outline: "border border-border-primary px-2 text-fg-secondary",
12
- destructive: "bg-destructive/10 px-2 text-destructive",
10
+ default: "bg-background-tertiary text-fg-primary",
11
+ outline: "border border-border-primary text-fg-secondary",
12
+ info: "bg-status-info-background text-status-info",
13
+ success: "bg-status-success-background text-status-success",
14
+ warning: "bg-status-warning-background text-status-warning",
15
+ critical: "bg-status-critical-background text-status-critical",
16
+ destructive: "bg-status-critical-background text-status-critical",
17
+ },
18
+ size: {
19
+ xxs: "text-2xs-strong h-4 gap-0.5 px-1.5 [&>[data-badge-slot]]:size-2.5",
20
+ xs: "text-2xs-strong h-5 gap-1 px-1.5 [&>[data-badge-slot]]:size-3",
21
+ sm: "text-xs-strong h-6 gap-1 px-2 [&>[data-badge-slot]]:size-3.5",
22
+ md: "text-xs-strong h-7 gap-1.5 px-2.5 [&>[data-badge-slot]]:size-3.5",
23
+ lg: "text-sm-strong h-8 gap-1.5 px-3 [&>[data-badge-slot]]:size-4",
24
+ },
25
+ rounded: {
26
+ true: "rounded-full",
27
+ false: "rounded-sm",
28
+ },
29
+ iconOnly: {
30
+ true: "p-0",
31
+ false: "",
13
32
  },
14
33
  },
34
+ compoundVariants: [
35
+ { iconOnly: true, size: "xxs", class: "size-4" },
36
+ { iconOnly: true, size: "xs", class: "size-5" },
37
+ { iconOnly: true, size: "sm", class: "size-6" },
38
+ { iconOnly: true, size: "md", class: "size-7" },
39
+ { iconOnly: true, size: "lg", class: "size-8" },
40
+ ],
15
41
  defaultVariants: {
16
42
  variant: "default",
43
+ size: "sm",
44
+ rounded: false,
45
+ iconOnly: false,
17
46
  },
18
47
  },
19
48
  )
20
49
 
21
- export type BadgeProps = HTMLAttributes<HTMLSpanElement> &
22
- VariantProps<typeof badgeVariants>
50
+ export type BadgeProps = Omit<HTMLAttributes<HTMLSpanElement>, "prefix"> &
51
+ VariantProps<typeof badgeVariants> & {
52
+ /** Square badge sized to the height of `size`. */
53
+ iconOnly?: boolean
54
+ /** Pill shape (`rounded-full`) instead of the default light radius. */
55
+ rounded?: boolean
56
+ /** Icon or element before the label. */
57
+ prefix?: ReactNode
58
+ /** Icon or element after the label. */
59
+ suffix?: ReactNode
60
+ }
23
61
 
24
- export const Badge = ({ className, variant, ...props }: BadgeProps) => (
25
- <span className={cn(badgeVariants({ variant }), className)} {...props} />
62
+ export const Badge = ({
63
+ className,
64
+ variant,
65
+ size,
66
+ rounded,
67
+ iconOnly,
68
+ prefix,
69
+ suffix,
70
+ children,
71
+ ...props
72
+ }: BadgeProps) => (
73
+ <span
74
+ className={cn(
75
+ badgeVariants({ variant, size, rounded, iconOnly }),
76
+ className,
77
+ )}
78
+ {...props}
79
+ >
80
+ {iconOnly ? (
81
+ <span data-badge-slot="icon">{children}</span>
82
+ ) : (
83
+ <>
84
+ {prefix ? <span data-badge-slot="prefix">{prefix}</span> : null}
85
+ {children != null ? <span data-badge-label>{children}</span> : null}
86
+ {suffix ? <span data-badge-slot="suffix">{suffix}</span> : null}
87
+ </>
88
+ )}
89
+ </span>
26
90
  )
27
91
 
28
92
  export { badgeVariants }
@@ -1,16 +1,30 @@
1
- import type { ComponentProps, ReactNode } from "react";
2
- import { IconChevronRightSmall } from "./icons";
3
- import { cn } from "./lib/cn";
4
- import { motion } from "./lib/motion";
1
+ import { cva, type VariantProps } from "class-variance-authority"
2
+ import type { ComponentProps, ReactNode } from "react"
3
+ import { IconChevronRightSmall } from "./icons"
4
+ import { cn } from "./lib/cn"
5
+ import { motion } from "./lib/motion"
6
+
7
+ export type BreadcrumbProps = ComponentProps<"nav">
8
+ export type BreadcrumbListProps = ComponentProps<"ol">
9
+ export type BreadcrumbItemProps = ComponentProps<"li">
10
+ export type BreadcrumbLinkProps = ComponentProps<"a">
11
+ export type BreadcrumbPageProps = ComponentProps<"span">
12
+
13
+ const breadcrumbSeparatorVariants = cva("text-fg-tertiary", {
14
+ variants: {
15
+ variant: {
16
+ chevron: "[&_svg]:size-3.5",
17
+ slash: "px-0.5",
18
+ },
19
+ },
20
+ defaultVariants: {
21
+ variant: "chevron",
22
+ },
23
+ })
5
24
 
6
- export type BreadcrumbProps = ComponentProps<"nav">;
7
- export type BreadcrumbListProps = ComponentProps<"ol">;
8
- export type BreadcrumbItemProps = ComponentProps<"li">;
9
- export type BreadcrumbLinkProps = ComponentProps<"a">;
10
- export type BreadcrumbPageProps = ComponentProps<"span">;
11
25
  export type BreadcrumbSeparatorProps = ComponentProps<"li"> & {
12
- children?: ReactNode;
13
- };
26
+ children?: ReactNode
27
+ } & VariantProps<typeof breadcrumbSeparatorVariants>
14
28
 
15
29
  export const Breadcrumb = ({ className, ...props }: BreadcrumbProps) => (
16
30
  <nav
@@ -18,27 +32,27 @@ export const Breadcrumb = ({ className, ...props }: BreadcrumbProps) => (
18
32
  className={cn("text-sm text-fg-secondary", className)}
19
33
  {...props}
20
34
  />
21
- );
35
+ )
22
36
 
23
37
  export const BreadcrumbList = ({
24
38
  className,
25
39
  ...props
26
40
  }: BreadcrumbListProps) => (
27
41
  <ol
28
- className={cn("flex flex-wrap items-center gap-1.5", className)}
42
+ className={cn("flex flex-wrap items-center gap-0.5", className)}
29
43
  {...props}
30
44
  />
31
- );
45
+ )
32
46
 
33
47
  export const BreadcrumbItem = ({
34
48
  className,
35
49
  ...props
36
50
  }: BreadcrumbItemProps) => (
37
51
  <li
38
- className={cn("inline-flex items-center gap-1.5", className)}
52
+ className={cn("inline-flex items-center gap-1.5 px-1", className)}
39
53
  {...props}
40
54
  />
41
- );
55
+ )
42
56
 
43
57
  export const BreadcrumbLink = ({
44
58
  className,
@@ -52,7 +66,7 @@ export const BreadcrumbLink = ({
52
66
  )}
53
67
  {...props}
54
68
  />
55
- );
69
+ )
56
70
 
57
71
  export const BreadcrumbPage = ({
58
72
  className,
@@ -63,19 +77,23 @@ export const BreadcrumbPage = ({
63
77
  className={cn("font-medium text-fg-primary", className)}
64
78
  {...props}
65
79
  />
66
- );
80
+ )
67
81
 
68
82
  export const BreadcrumbSeparator = ({
69
83
  className,
70
84
  children,
85
+ variant,
71
86
  ...props
72
87
  }: BreadcrumbSeparatorProps) => (
73
88
  <li
74
89
  role="presentation"
75
90
  aria-hidden
76
- className={cn("text-fg-tertiary [&_svg]:size-3.5", className)}
91
+ className={cn(breadcrumbSeparatorVariants({ variant }), className)}
77
92
  {...props}
78
93
  >
79
- {children ?? <IconChevronRightSmall />}
94
+ {children ??
95
+ (variant === "slash" ? "/" : <IconChevronRightSmall aria-hidden />)}
80
96
  </li>
81
- );
97
+ )
98
+
99
+ export { breadcrumbSeparatorVariants }
package/src/button.tsx CHANGED
@@ -25,7 +25,7 @@ const buttonVariants = cva(
25
25
  },
26
26
  rounded: {
27
27
  true: "rounded-full",
28
- false: "rounded-md",
28
+ false: "rounded-lg",
29
29
  },
30
30
  iconOnly: {
31
31
  true: "px-0",
package/src/calendar.tsx CHANGED
@@ -8,9 +8,9 @@ import { motion } from "./lib/motion"
8
8
  export type CalendarProps = DayPickerProps
9
9
 
10
10
  const navButtonClassName = cn(
11
- "inline-flex size-8 items-center justify-center rounded-md text-fg-tertiary",
11
+ "inline-flex size-6 items-center justify-center rounded-md border border-transparent text-fg-tertiary",
12
12
  motion.colors,
13
- "hover:bg-background-tertiary 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 disabled:cursor-not-allowed disabled:opacity-50",
13
+ "hover:bg-background-tertiary hover:text-fg-primary outline-none focus-visible:border-ring focus-visible:bg-background-tertiary focus-visible:text-fg-primary focus-visible:ring-2 focus-visible:ring-ring/20 disabled:cursor-not-allowed disabled:opacity-50",
14
14
  )
15
15
 
16
16
  const CalendarChevron = ({
@@ -40,34 +40,45 @@ export const Calendar = ({
40
40
  className,
41
41
  classNames,
42
42
  showOutsideDays = true,
43
+ weekStartsOn = 1,
43
44
  components,
44
45
  ...props
45
46
  }: CalendarProps) => (
46
47
  <DayPicker
47
48
  showOutsideDays={showOutsideDays}
48
- className={cn("w-fit p-3", className)}
49
+ weekStartsOn={weekStartsOn}
50
+ className={cn(
51
+ "w-64 rounded-xl border border-border-primary bg-surface p-3",
52
+ className,
53
+ )}
49
54
  classNames={{
50
55
  months: "relative flex flex-col gap-4 sm:flex-row",
51
- month: "flex w-full flex-col gap-3",
52
- month_caption: "relative flex h-8 items-center justify-center px-8",
56
+ month: "flex w-full flex-col",
57
+ month_caption: "flex h-8 items-start justify-start",
53
58
  caption_label: "text-sm-strong text-fg-primary",
54
- nav: "absolute inset-x-0 top-0 flex items-center justify-between",
59
+ nav: "absolute top-0 right-0 flex items-center",
55
60
  button_previous: navButtonClassName,
56
61
  button_next: navButtonClassName,
57
62
  month_grid: "w-full border-collapse",
58
63
  weekdays: "flex",
59
64
  weekday:
60
- "flex size-8 items-center justify-center text-xs font-normal text-fg-tertiary",
61
- weeks: "flex flex-col",
62
- week: "mt-0.5 flex w-full",
65
+ "flex w-8 items-center justify-center pb-1 text-xs font-normal text-fg-tertiary select-none",
66
+ weeks: "flex flex-col gap-1",
67
+ week: "flex w-full",
63
68
  day: "relative p-0 text-center",
64
69
  day_button: cn(
65
- "inline-flex size-8 items-center justify-center rounded-md text-sm text-fg-primary",
70
+ "inline-flex size-8 items-center justify-center rounded-lg text-sm text-fg-primary tabular-nums",
66
71
  motion.colors,
67
- "hover:bg-background-tertiary 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 disabled:cursor-not-allowed disabled:opacity-50 aria-selected:opacity-100",
72
+ "hover:bg-background-tertiary outline-none focus-visible:z-10 focus-visible:ring-2 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50 aria-selected:opacity-100",
68
73
  ),
69
74
  selected:
70
75
  "[&>button]:bg-brand-primary [&>button]:text-brand-foreground [&>button]:ring-0 [&>button]:hover:bg-brand-primary-hover [&>button]:hover:text-brand-foreground",
76
+ range_start:
77
+ "rounded-l-lg bg-brand-primary/10 last:rounded-r-lg [&>button]:relative [&>button]:z-[1]",
78
+ range_middle:
79
+ "bg-brand-primary/10 first:rounded-l-lg last:rounded-r-lg [&>button]:!rounded-none [&>button]:!bg-transparent [&>button]:!text-fg-primary [&>button]:hover:!bg-brand-primary/15",
80
+ range_end:
81
+ "rounded-r-lg bg-brand-primary/10 first:rounded-l-lg [&>button]:relative [&>button]:z-[1]",
71
82
  today:
72
83
  "[&>button]:font-medium [&>button]:ring-1 [&>button]:ring-inset [&>button]:ring-border-secondary",
73
84
  outside: "[&>button]:text-fg-quaternary",
package/src/card.tsx CHANGED
@@ -1,32 +1,59 @@
1
+ import { cva, type VariantProps } from "class-variance-authority"
1
2
  import type { HTMLAttributes } from "react"
2
3
  import { cn } from "./lib/cn"
3
4
 
4
- export type CardProps = HTMLAttributes<HTMLDivElement>
5
+ const cardVariants = cva("flex flex-col rounded-xl bg-surface text-fg-primary", {
6
+ variants: {
7
+ variant: {
8
+ elevated: "shadow-lg",
9
+ outline: "border border-border-primary",
10
+ ghost: "",
11
+ },
12
+ padding: {
13
+ md: "gap-6 py-6",
14
+ none: "",
15
+ },
16
+ },
17
+ defaultVariants: {
18
+ variant: "elevated",
19
+ padding: "md",
20
+ },
21
+ })
22
+
23
+ export type CardProps = HTMLAttributes<HTMLDivElement> &
24
+ VariantProps<typeof cardVariants>
5
25
  export type CardHeaderProps = HTMLAttributes<HTMLDivElement>
6
26
  export type CardTitleProps = HTMLAttributes<HTMLHeadingElement>
7
27
  export type CardDescriptionProps = HTMLAttributes<HTMLParagraphElement>
28
+ export type CardActionProps = HTMLAttributes<HTMLDivElement>
8
29
  export type CardContentProps = HTMLAttributes<HTMLDivElement>
9
30
  export type CardFooterProps = HTMLAttributes<HTMLDivElement>
10
31
 
11
- export const Card = ({ className, ...props }: CardProps) => (
32
+ export const Card = ({ className, variant, padding, ...props }: CardProps) => (
12
33
  <div
13
- className={cn(
14
- "flex flex-col overflow-hidden rounded-xl border border-border-primary bg-surface",
15
- className,
16
- )}
17
34
  {...props}
35
+ data-slot="card"
36
+ className={cn(cardVariants({ variant, padding }), className)}
18
37
  />
19
38
  )
20
39
 
21
40
  export const CardHeader = ({ className, ...props }: CardHeaderProps) => (
22
41
  <div
23
- className={cn("flex flex-col gap-1 px-5 pt-5 pb-3", className)}
24
42
  {...props}
43
+ data-slot="card-header"
44
+ className={cn(
45
+ "grid auto-rows-min items-start gap-1.5 px-6 has-data-[slot=card-action]:grid-cols-[1fr_auto]",
46
+ className,
47
+ )}
25
48
  />
26
49
  )
27
50
 
28
51
  export const CardTitle = ({ className, ...props }: CardTitleProps) => (
29
- <h3 className={cn("text-sm-strong text-fg-primary", className)} {...props} />
52
+ <h3
53
+ {...props}
54
+ data-slot="card-title"
55
+ className={cn("text-sm-strong leading-none text-fg-primary", className)}
56
+ />
30
57
  )
31
58
 
32
59
  export const CardDescription = ({
@@ -34,21 +61,37 @@ export const CardDescription = ({
34
61
  ...props
35
62
  }: CardDescriptionProps) => (
36
63
  <p
37
- className={cn("text-sm leading-relaxed text-fg-secondary", className)}
38
64
  {...props}
65
+ data-slot="card-description"
66
+ className={cn("text-sm text-fg-secondary", className)}
67
+ />
68
+ )
69
+
70
+ export const CardAction = ({ className, ...props }: CardActionProps) => (
71
+ <div
72
+ {...props}
73
+ data-slot="card-action"
74
+ className={cn(
75
+ "col-start-2 row-span-2 row-start-1 self-start justify-self-end",
76
+ className,
77
+ )}
39
78
  />
40
79
  )
41
80
 
42
81
  export const CardContent = ({ className, ...props }: CardContentProps) => (
43
- <div className={cn("px-5 pb-5", className)} {...props} />
82
+ <div
83
+ {...props}
84
+ data-slot="card-content"
85
+ className={cn("px-6", className)}
86
+ />
44
87
  )
45
88
 
46
89
  export const CardFooter = ({ className, ...props }: CardFooterProps) => (
47
90
  <div
48
- className={cn(
49
- "flex items-center gap-2 border-t border-border-primary px-5 py-4",
50
- className,
51
- )}
52
91
  {...props}
92
+ data-slot="card-footer"
93
+ className={cn("flex items-center gap-2 px-6", className)}
53
94
  />
54
95
  )
96
+
97
+ export { cardVariants }
package/src/carousel.tsx CHANGED
@@ -8,6 +8,7 @@ import {
8
8
  useCallback,
9
9
  useContext,
10
10
  useEffect,
11
+ useRef,
11
12
  useSyncExternalStore,
12
13
  type ComponentProps,
13
14
  type KeyboardEvent,
@@ -21,6 +22,9 @@ type UseCarouselParameters = Parameters<typeof useEmblaCarousel>
21
22
  type CarouselOptions = UseCarouselParameters[0]
22
23
  type CarouselPlugin = UseCarouselParameters[1]
23
24
 
25
+ const TRACKPAD_SWIPE_THRESHOLD = 24
26
+ const TRACKPAD_GESTURE_END_DELAY = 140
27
+
24
28
  type CarouselContextValue = {
25
29
  carouselRef: ReturnType<typeof useEmblaCarousel>[0]
26
30
  api: CarouselApi
@@ -46,6 +50,8 @@ export type CarouselProps = ComponentProps<"div"> & {
46
50
  plugins?: CarouselPlugin
47
51
  orientation?: "horizontal" | "vertical"
48
52
  setApi?: (api: CarouselApi) => void
53
+ /** Adds directional edge fades wherever more slides are available. */
54
+ fade?: boolean
49
55
  }
50
56
 
51
57
  export type CarouselContentProps = ComponentProps<"div">
@@ -53,16 +59,54 @@ export type CarouselItemProps = ComponentProps<"div">
53
59
  export type CarouselPreviousProps = ComponentProps<typeof Button>
54
60
  export type CarouselNextProps = ComponentProps<typeof Button>
55
61
 
62
+ const CarouselEdgeFades = () => {
63
+ const { orientation, canScrollPrev, canScrollNext } = useCarousel()
64
+ const horizontal = orientation === "horizontal"
65
+ const sharedClassName =
66
+ "pointer-events-none absolute z-10 opacity-0 transition-opacity duration-[var(--duration-lg)] ease-enter data-[visible=true]:opacity-100 motion-reduce:transition-none"
67
+
68
+ return (
69
+ <>
70
+ <div
71
+ aria-hidden="true"
72
+ data-slot={horizontal ? "carousel-fade-left" : "carousel-fade-top"}
73
+ data-visible={canScrollPrev}
74
+ className={cn(
75
+ sharedClassName,
76
+ horizontal
77
+ ? "-top-px -bottom-px -left-px w-20 bg-linear-to-r from-surface via-surface/60 via-40% to-transparent"
78
+ : "-top-px -right-px -left-px h-20 bg-linear-to-b from-surface via-surface/60 via-40% to-transparent",
79
+ )}
80
+ />
81
+ <div
82
+ aria-hidden="true"
83
+ data-slot={horizontal ? "carousel-fade-right" : "carousel-fade-bottom"}
84
+ data-visible={canScrollNext}
85
+ className={cn(
86
+ sharedClassName,
87
+ horizontal
88
+ ? "-top-px -right-px -bottom-px w-20 bg-linear-to-l from-surface via-surface/60 via-40% to-transparent"
89
+ : "-right-px -bottom-px -left-px h-20 bg-linear-to-t from-surface via-surface/60 via-40% to-transparent",
90
+ )}
91
+ />
92
+ </>
93
+ )
94
+ }
95
+
56
96
  export const Carousel = ({
57
97
  orientation = "horizontal",
98
+ fade = false,
58
99
  opts,
59
100
  setApi,
60
101
  plugins,
61
102
  className,
62
103
  children,
63
104
  onKeyDownCapture,
105
+ tabIndex,
106
+ ref: forwardedRef,
64
107
  ...props
65
108
  }: CarouselProps) => {
109
+ const carouselRootRef = useRef<HTMLDivElement | null>(null)
66
110
  const [carouselRef, api] = useEmblaCarousel(
67
111
  {
68
112
  ...opts,
@@ -107,6 +151,35 @@ export const Carousel = ({
107
151
  api?.scrollNext()
108
152
  }, [api])
109
153
 
154
+ const canScrollPrevRef = useRef(canScrollPrev)
155
+ const canScrollNextRef = useRef(canScrollNext)
156
+ const scrollPrevRef = useRef(scrollPrev)
157
+ const scrollNextRef = useRef(scrollNext)
158
+
159
+ useEffect(() => {
160
+ canScrollPrevRef.current = canScrollPrev
161
+ canScrollNextRef.current = canScrollNext
162
+ scrollPrevRef.current = scrollPrev
163
+ scrollNextRef.current = scrollNext
164
+ }, [canScrollNext, canScrollPrev, scrollNext, scrollPrev])
165
+
166
+ const trackpadDelta = useRef(0)
167
+ const trackpadGestureHandled = useRef(false)
168
+ const trackpadGestureTimeout = useRef<ReturnType<typeof setTimeout> | null>(
169
+ null,
170
+ )
171
+
172
+ const scheduleTrackpadGestureEnd = useCallback(() => {
173
+ if (trackpadGestureTimeout.current) {
174
+ clearTimeout(trackpadGestureTimeout.current)
175
+ }
176
+ trackpadGestureTimeout.current = setTimeout(() => {
177
+ trackpadDelta.current = 0
178
+ trackpadGestureHandled.current = false
179
+ trackpadGestureTimeout.current = null
180
+ }, TRACKPAD_GESTURE_END_DELAY)
181
+ }, [])
182
+
110
183
  const handleKeyDown = useCallback(
111
184
  (event: KeyboardEvent<HTMLDivElement>) => {
112
185
  onKeyDownCapture?.(event)
@@ -122,6 +195,72 @@ export const Carousel = ({
122
195
  [onKeyDownCapture, scrollNext, scrollPrev],
123
196
  )
124
197
 
198
+ const handleWheel = useCallback(
199
+ (event: globalThis.WheelEvent) => {
200
+ if (event.defaultPrevented || orientation !== "horizontal") return
201
+
202
+ const deltaMultiplier =
203
+ event.deltaMode === 1 ? 16 : event.deltaMode === 2 ? 100 : 1
204
+ const horizontalDelta = event.deltaX * deltaMultiplier
205
+ const verticalDelta = event.deltaY * deltaMultiplier
206
+
207
+ if (
208
+ Math.abs(horizontalDelta) <= Math.abs(verticalDelta) ||
209
+ Math.abs(horizontalDelta) < 1
210
+ ) {
211
+ return
212
+ }
213
+
214
+ if (trackpadGestureHandled.current) {
215
+ event.preventDefault()
216
+ scheduleTrackpadGestureEnd()
217
+ return
218
+ }
219
+
220
+ const canScrollInDirection =
221
+ horizontalDelta > 0
222
+ ? canScrollNextRef.current
223
+ : canScrollPrevRef.current
224
+ if (!canScrollInDirection) return
225
+
226
+ event.preventDefault()
227
+ trackpadDelta.current += horizontalDelta
228
+ scheduleTrackpadGestureEnd()
229
+
230
+ if (Math.abs(trackpadDelta.current) < TRACKPAD_SWIPE_THRESHOLD) return
231
+
232
+ trackpadGestureHandled.current = true
233
+ trackpadDelta.current = 0
234
+ if (horizontalDelta > 0) scrollNextRef.current()
235
+ else scrollPrevRef.current()
236
+ },
237
+ [orientation, scheduleTrackpadGestureEnd],
238
+ )
239
+
240
+ useEffect(() => {
241
+ const root = carouselRootRef.current
242
+ if (!root) return
243
+
244
+ root.addEventListener("wheel", handleWheel, { passive: false })
245
+ return () => {
246
+ root.removeEventListener("wheel", handleWheel)
247
+ if (trackpadGestureTimeout.current) {
248
+ clearTimeout(trackpadGestureTimeout.current)
249
+ }
250
+ trackpadDelta.current = 0
251
+ trackpadGestureHandled.current = false
252
+ }
253
+ }, [handleWheel])
254
+
255
+ const setCarouselRootRef = useCallback(
256
+ (node: HTMLDivElement | null) => {
257
+ carouselRootRef.current = node
258
+ if (typeof forwardedRef === "function") forwardedRef(node)
259
+ else if (forwardedRef) forwardedRef.current = node
260
+ },
261
+ [forwardedRef],
262
+ )
263
+
125
264
  useEffect(() => {
126
265
  if (!api || !setApi) return
127
266
  setApi(api)
@@ -140,14 +279,23 @@ export const Carousel = ({
140
279
  }}
141
280
  >
142
281
  <div
282
+ ref={setCarouselRootRef}
143
283
  role="region"
144
284
  aria-roledescription="carousel"
145
285
  data-slot="carousel"
146
- className={cn("relative", className)}
286
+ data-fade={fade || undefined}
287
+ className={cn(
288
+ "relative",
289
+ fade &&
290
+ "rounded-xl outline-none focus-visible:ring-[3px] focus-visible:ring-offset-1 focus-visible:ring-offset-background-primary focus-visible:ring-ring/20",
291
+ className,
292
+ )}
147
293
  onKeyDownCapture={handleKeyDown}
294
+ tabIndex={tabIndex ?? (fade ? 0 : undefined)}
148
295
  {...props}
149
296
  >
150
297
  {children}
298
+ {fade ? <CarouselEdgeFades /> : null}
151
299
  </div>
152
300
  </CarouselContext.Provider>
153
301
  )
@@ -160,7 +308,16 @@ export const CarouselContent = ({
160
308
  const { carouselRef, orientation } = useCarousel()
161
309
 
162
310
  return (
163
- <div ref={carouselRef} className="overflow-hidden" data-slot="carousel-viewport">
311
+ <div
312
+ ref={carouselRef}
313
+ className={cn(
314
+ "overflow-hidden",
315
+ orientation === "horizontal"
316
+ ? "touch-pan-y overscroll-x-contain"
317
+ : "touch-pan-x overscroll-y-contain",
318
+ )}
319
+ data-slot="carousel-viewport"
320
+ >
164
321
  <div
165
322
  data-slot="carousel-content"
166
323
  className={cn(
@@ -194,7 +351,7 @@ export const CarouselItem = ({ className, ...props }: CarouselItemProps) => {
194
351
 
195
352
  export const CarouselPrevious = ({
196
353
  className,
197
- variant = "outline",
354
+ variant = "ghost",
198
355
  size = "md",
199
356
  ...props
200
357
  }: CarouselPreviousProps) => {
@@ -211,7 +368,7 @@ export const CarouselPrevious = ({
211
368
  aria-label="Previous slide"
212
369
  data-slot="carousel-previous"
213
370
  className={cn(
214
- "absolute size-9",
371
+ "absolute size-9 border-0 focus-visible:border-0",
215
372
  orientation === "horizontal"
216
373
  ? "top-1/2 -left-12 -translate-y-1/2 active:!-translate-y-1/2"
217
374
  : "-top-12 left-1/2 -translate-x-1/2 rotate-90 active:!-translate-x-1/2 active:!translate-y-0",
@@ -227,7 +384,7 @@ export const CarouselPrevious = ({
227
384
 
228
385
  export const CarouselNext = ({
229
386
  className,
230
- variant = "outline",
387
+ variant = "ghost",
231
388
  size = "md",
232
389
  ...props
233
390
  }: CarouselNextProps) => {
@@ -244,7 +401,7 @@ export const CarouselNext = ({
244
401
  aria-label="Next slide"
245
402
  data-slot="carousel-next"
246
403
  className={cn(
247
- "absolute size-9",
404
+ "absolute size-9 border-0 focus-visible:border-0",
248
405
  orientation === "horizontal"
249
406
  ? "top-1/2 -right-12 -translate-y-1/2 active:!-translate-y-1/2"
250
407
  : "-bottom-12 left-1/2 -translate-x-1/2 rotate-90 active:!-translate-x-1/2 active:!translate-y-0",
@@ -0,0 +1,169 @@
1
+ "use client"
2
+
3
+ import { useState, type ReactNode } from "react"
4
+ import type { DateRange, PropsBase } from "react-day-picker"
5
+ import { Button } from "./button"
6
+ import { Calendar } from "./calendar"
7
+ import { IconCalendar1 } from "./icons"
8
+ import { cn } from "./lib/cn"
9
+ import {
10
+ Popover,
11
+ PopoverPopup,
12
+ PopoverPortal,
13
+ PopoverPositioner,
14
+ PopoverTrigger,
15
+ } from "./popover"
16
+
17
+ const dateFormatter = new Intl.DateTimeFormat("en-US", {
18
+ month: "short",
19
+ day: "numeric",
20
+ year: "numeric",
21
+ })
22
+
23
+ const formatDate = (date: Date) => dateFormatter.format(date)
24
+
25
+ type DatePickerBaseProps = {
26
+ /** Options forwarded to Calendar, excluding its selection mode. */
27
+ calendarProps?: Omit<PropsBase, "mode" | "required">
28
+ /** Classes applied to the trigger button. */
29
+ className?: string
30
+ /** Disables the trigger button. */
31
+ disabled?: boolean
32
+ /** Placeholder shown when no date is selected. */
33
+ placeholder?: string
34
+ }
35
+
36
+ export type DatePickerSingleProps = DatePickerBaseProps & {
37
+ mode?: "single"
38
+ selected?: Date
39
+ onSelect?: (date: Date | undefined) => void
40
+ }
41
+
42
+ export type DatePickerRangeProps = DatePickerBaseProps & {
43
+ mode: "range"
44
+ selected?: DateRange
45
+ onSelect?: (range: DateRange | undefined) => void
46
+ }
47
+
48
+ export type DatePickerProps = DatePickerSingleProps | DatePickerRangeProps
49
+
50
+ const DatePickerTrigger = ({
51
+ className,
52
+ disabled,
53
+ empty,
54
+ label,
55
+ }: {
56
+ className?: string
57
+ disabled?: boolean
58
+ empty: boolean
59
+ label: string
60
+ }) => (
61
+ <PopoverTrigger
62
+ render={
63
+ <Button
64
+ type="button"
65
+ size="sm"
66
+ variant="outline"
67
+ disabled={disabled}
68
+ prefix={<IconCalendar1 aria-hidden />}
69
+ className={cn("w-56 justify-start", className)}
70
+ />
71
+ }
72
+ >
73
+ <span
74
+ className={cn(
75
+ "min-w-0 flex-1 truncate text-left",
76
+ empty && "text-fg-tertiary",
77
+ )}
78
+ >
79
+ {label}
80
+ </span>
81
+ </PopoverTrigger>
82
+ )
83
+
84
+ const DatePickerPopup = ({ children }: { children: ReactNode }) => (
85
+ <PopoverPortal>
86
+ <PopoverPositioner align="start" sideOffset={6}>
87
+ <PopoverPopup className="w-auto gap-0 p-0">{children}</PopoverPopup>
88
+ </PopoverPositioner>
89
+ </PopoverPortal>
90
+ )
91
+
92
+ const SingleDatePicker = ({
93
+ calendarProps,
94
+ className,
95
+ disabled,
96
+ onSelect,
97
+ placeholder = "Pick a date",
98
+ selected,
99
+ }: DatePickerSingleProps) => {
100
+ const [open, setOpen] = useState(false)
101
+
102
+ const handleSelect = (date: Date | undefined) => {
103
+ onSelect?.(date)
104
+ if (date) setOpen(false)
105
+ }
106
+
107
+ return (
108
+ <Popover open={open} onOpenChange={setOpen}>
109
+ <DatePickerTrigger
110
+ className={className}
111
+ disabled={disabled}
112
+ empty={!selected}
113
+ label={selected ? formatDate(selected) : placeholder}
114
+ />
115
+ <DatePickerPopup>
116
+ <Calendar
117
+ {...calendarProps}
118
+ className={cn("border-0", calendarProps?.className)}
119
+ mode="single"
120
+ selected={selected}
121
+ onSelect={handleSelect}
122
+ />
123
+ </DatePickerPopup>
124
+ </Popover>
125
+ )
126
+ }
127
+
128
+ const RangeDatePicker = ({
129
+ calendarProps,
130
+ className,
131
+ disabled,
132
+ onSelect,
133
+ placeholder = "Pick a date range",
134
+ selected,
135
+ }: DatePickerRangeProps) => {
136
+ const [open, setOpen] = useState(false)
137
+ const label = selected?.from
138
+ ? selected.to
139
+ ? `${formatDate(selected.from)} – ${formatDate(selected.to)}`
140
+ : `${formatDate(selected.from)} – Select end date`
141
+ : placeholder
142
+
143
+ return (
144
+ <Popover open={open} onOpenChange={setOpen}>
145
+ <DatePickerTrigger
146
+ className={className}
147
+ disabled={disabled}
148
+ empty={!selected?.from}
149
+ label={label}
150
+ />
151
+ <DatePickerPopup>
152
+ <Calendar
153
+ {...calendarProps}
154
+ className={cn("border-0", calendarProps?.className)}
155
+ mode="range"
156
+ selected={selected}
157
+ onSelect={onSelect}
158
+ />
159
+ </DatePickerPopup>
160
+ </Popover>
161
+ )
162
+ }
163
+
164
+ export const DatePicker = (props: DatePickerProps) =>
165
+ props.mode === "range" ? (
166
+ <RangeDatePicker {...props} />
167
+ ) : (
168
+ <SingleDatePicker {...props} />
169
+ )
package/src/icons.tsx CHANGED
@@ -2,13 +2,19 @@
2
2
 
3
3
  import type { CentralIconBaseProps } from "@central-icons-react/round-outlined-radius-2-stroke-2/CentralIconBase"
4
4
  import { IconBell as IconBellBase } from "@central-icons-react/round-outlined-radius-2-stroke-2/IconBell"
5
+ import { IconCalendar1 as IconCalendar1Base } from "@central-icons-react/round-outlined-radius-2-stroke-2/IconCalendar1"
5
6
  import { IconChainLink1 as IconChainLink1Base } from "@central-icons-react/round-outlined-radius-2-stroke-2/IconChainLink1"
6
7
  import { IconCheckmark1 as IconCheckmark1Base } from "@central-icons-react/round-outlined-radius-2-stroke-2/IconCheckmark1"
7
8
  import { IconChevronBottom as IconChevronBottomBase } from "@central-icons-react/round-outlined-radius-2-stroke-2/IconChevronBottom"
8
9
  import { IconChevronDownSmall as IconChevronDownSmallBase } from "@central-icons-react/round-outlined-radius-2-stroke-2/IconChevronDownSmall"
9
10
  import { IconChevronRightSmall as IconChevronRightSmallBase } from "@central-icons-react/round-outlined-radius-2-stroke-2/IconChevronRightSmall"
11
+ import { IconCircleCheck as IconCircleCheckBase } from "@central-icons-react/round-outlined-radius-2-stroke-2/IconCircleCheck"
12
+ import { IconCircleInfo as IconCircleInfoBase } from "@central-icons-react/round-outlined-radius-2-stroke-2/IconCircleInfo"
10
13
  import { IconClipboard as IconClipboardBase } from "@central-icons-react/round-outlined-radius-2-stroke-2/IconClipboard"
11
14
  import { IconCrossSmall as IconCrossSmallBase } from "@central-icons-react/round-outlined-radius-2-stroke-2/IconCrossSmall"
15
+ import { IconDotGrid1x3Horizontal as IconDotGrid1x3HorizontalBase } from "@central-icons-react/round-outlined-radius-2-stroke-2/IconDotGrid1x3Horizontal"
16
+ import { IconExclamationCircle as IconExclamationCircleBase } from "@central-icons-react/round-outlined-radius-2-stroke-2/IconExclamationCircle"
17
+ import { IconExclamationTriangle as IconExclamationTriangleBase } from "@central-icons-react/round-outlined-radius-2-stroke-2/IconExclamationTriangle"
12
18
  import { IconHome as IconHomeBase } from "@central-icons-react/round-outlined-radius-2-stroke-2/IconHome"
13
19
  import { IconMagnifyingGlass as IconMagnifyingGlassBase } from "@central-icons-react/round-outlined-radius-2-stroke-2/IconMagnifyingGlass"
14
20
  import { IconMinusMedium as IconMinusBase } from "@central-icons-react/round-outlined-radius-2-stroke-2/IconMinusMedium"
@@ -17,6 +23,7 @@ import { IconPeople as IconPeopleBase } from "@central-icons-react/round-outline
17
23
  import { IconPlusMedium as IconPlusBase } from "@central-icons-react/round-outlined-radius-2-stroke-2/IconPlusMedium"
18
24
  import { IconSettingsGear1 as IconSettingsGear1Base } from "@central-icons-react/round-outlined-radius-2-stroke-2/IconSettingsGear1"
19
25
  import { IconSquareBehindSquare6 as IconSquareBehindSquare6Base } from "@central-icons-react/round-outlined-radius-2-stroke-2/IconSquareBehindSquare6"
26
+ import { IconStar as IconStarBase } from "@central-icons-react/round-outlined-radius-2-stroke-2/IconStar"
20
27
  import { IconSun as IconSunBase } from "@central-icons-react/round-outlined-radius-2-stroke-2/IconSun"
21
28
  import { IconX as IconXBase } from "@central-icons-react/round-outlined-radius-2-stroke-2/IconX"
22
29
  import type { ComponentType } from "react"
@@ -43,6 +50,7 @@ export const withCentralIconDefaults = (
43
50
  }
44
51
 
45
52
  export const IconBell = withCentralIconDefaults(IconBellBase)
53
+ export const IconCalendar1 = withCentralIconDefaults(IconCalendar1Base)
46
54
  export const IconChainLink1 = withCentralIconDefaults(IconChainLink1Base)
47
55
  export const IconCheckmark1 = withCentralIconDefaults(IconCheckmark1Base)
48
56
  export const IconChevronBottom = withCentralIconDefaults(IconChevronBottomBase)
@@ -52,8 +60,19 @@ export const IconChevronDownSmall = withCentralIconDefaults(
52
60
  export const IconChevronRightSmall = withCentralIconDefaults(
53
61
  IconChevronRightSmallBase,
54
62
  )
63
+ export const IconCircleCheck = withCentralIconDefaults(IconCircleCheckBase)
64
+ export const IconCircleInfo = withCentralIconDefaults(IconCircleInfoBase)
55
65
  export const IconClipboard = withCentralIconDefaults(IconClipboardBase)
56
66
  export const IconCrossSmall = withCentralIconDefaults(IconCrossSmallBase)
67
+ export const IconDotGrid1x3Horizontal = withCentralIconDefaults(
68
+ IconDotGrid1x3HorizontalBase,
69
+ )
70
+ export const IconExclamationCircle = withCentralIconDefaults(
71
+ IconExclamationCircleBase,
72
+ )
73
+ export const IconExclamationTriangle = withCentralIconDefaults(
74
+ IconExclamationTriangleBase,
75
+ )
57
76
  export const IconHome = withCentralIconDefaults(IconHomeBase)
58
77
  export const IconMagnifyingGlass = withCentralIconDefaults(
59
78
  IconMagnifyingGlassBase,
@@ -66,11 +85,13 @@ export const IconSettingsGear1 = withCentralIconDefaults(IconSettingsGear1Base)
66
85
  export const IconSquareBehindSquare6 = withCentralIconDefaults(
67
86
  IconSquareBehindSquare6Base,
68
87
  )
88
+ export const IconStar = withCentralIconDefaults(IconStarBase)
69
89
  export const IconSun = withCentralIconDefaults(IconSunBase)
70
90
  export const IconX = withCentralIconDefaults(IconXBase)
71
91
 
72
92
  export const iconGallery = [
73
93
  { name: "IconHome", Icon: IconHome },
94
+ { name: "IconCalendar1", Icon: IconCalendar1 },
74
95
  { name: "IconMagnifyingGlass", Icon: IconMagnifyingGlass },
75
96
  { name: "IconSettingsGear1", Icon: IconSettingsGear1 },
76
97
  { name: "IconBell", Icon: IconBell },
@@ -85,7 +106,13 @@ export const iconGallery = [
85
106
  { name: "IconChevronBottom", Icon: IconChevronBottom },
86
107
  { name: "IconChevronDownSmall", Icon: IconChevronDownSmall },
87
108
  { name: "IconChevronRightSmall", Icon: IconChevronRightSmall },
109
+ { name: "IconCircleCheck", Icon: IconCircleCheck },
110
+ { name: "IconCircleInfo", Icon: IconCircleInfo },
88
111
  { name: "IconClipboard", Icon: IconClipboard },
112
+ { name: "IconDotGrid1x3Horizontal", Icon: IconDotGrid1x3Horizontal },
89
113
  { name: "IconSquareBehindSquare6", Icon: IconSquareBehindSquare6 },
90
114
  { name: "IconChainLink1", Icon: IconChainLink1 },
115
+ { name: "IconExclamationCircle", Icon: IconExclamationCircle },
116
+ { name: "IconExclamationTriangle", Icon: IconExclamationTriangle },
117
+ { name: "IconStar", Icon: IconStar },
91
118
  ] as const
package/src/index.ts CHANGED
@@ -92,17 +92,27 @@ export {
92
92
  type AvatarFallbackProps,
93
93
  } from "./avatar";
94
94
  export { Calendar, type CalendarProps } from "./calendar";
95
+ export {
96
+ DatePicker,
97
+ type DatePickerProps,
98
+ type DatePickerSingleProps,
99
+ type DatePickerRangeProps,
100
+ } from "./date-picker";
101
+ export type { DateRange } from "react-day-picker";
95
102
  export {
96
103
  Card,
97
104
  CardHeader,
98
105
  CardTitle,
99
106
  CardDescription,
107
+ CardAction,
100
108
  CardContent,
101
109
  CardFooter,
110
+ cardVariants,
102
111
  type CardProps,
103
112
  type CardHeaderProps,
104
113
  type CardTitleProps,
105
114
  type CardDescriptionProps,
115
+ type CardActionProps,
106
116
  type CardContentProps,
107
117
  type CardFooterProps,
108
118
  } from "./card";
@@ -650,6 +660,7 @@ export {
650
660
  BreadcrumbLink,
651
661
  BreadcrumbPage,
652
662
  BreadcrumbSeparator,
663
+ breadcrumbSeparatorVariants,
653
664
  type BreadcrumbProps,
654
665
  type BreadcrumbListProps,
655
666
  type BreadcrumbItemProps,
@@ -785,13 +796,19 @@ export {
785
796
  withCentralIconDefaults,
786
797
  iconGallery,
787
798
  IconBell,
799
+ IconCalendar1,
788
800
  IconChainLink1,
789
801
  IconCheckmark1,
790
802
  IconChevronBottom,
791
803
  IconChevronDownSmall,
792
804
  IconChevronRightSmall,
805
+ IconCircleCheck,
806
+ IconCircleInfo,
793
807
  IconClipboard,
794
808
  IconCrossSmall,
809
+ IconDotGrid1x3Horizontal,
810
+ IconExclamationCircle,
811
+ IconExclamationTriangle,
795
812
  IconHome,
796
813
  IconMagnifyingGlass,
797
814
  IconMinus,
@@ -800,6 +817,7 @@ export {
800
817
  IconPlus,
801
818
  IconSettingsGear1,
802
819
  IconSquareBehindSquare6,
820
+ IconStar,
803
821
  IconSun,
804
822
  IconX,
805
823
  type CentralIconProps,
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
  )}