@voila.dev/ui 1.1.9

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (99) hide show
  1. package/package.json +78 -0
  2. package/src/components/ui/accordion.tsx +79 -0
  3. package/src/components/ui/alert-dialog.tsx +198 -0
  4. package/src/components/ui/alert.tsx +105 -0
  5. package/src/components/ui/aspect-ratio.tsx +30 -0
  6. package/src/components/ui/avatar.tsx +125 -0
  7. package/src/components/ui/badge-variants.ts +166 -0
  8. package/src/components/ui/badge.tsx +45 -0
  9. package/src/components/ui/banner.tsx +84 -0
  10. package/src/components/ui/breadcrumb.tsx +127 -0
  11. package/src/components/ui/button-group.tsx +99 -0
  12. package/src/components/ui/button-variants.ts +108 -0
  13. package/src/components/ui/button.tsx +54 -0
  14. package/src/components/ui/calendar.tsx +266 -0
  15. package/src/components/ui/card.tsx +155 -0
  16. package/src/components/ui/carousel.tsx +319 -0
  17. package/src/components/ui/chat.tsx +896 -0
  18. package/src/components/ui/checkbox-group.tsx +44 -0
  19. package/src/components/ui/checkbox.tsx +34 -0
  20. package/src/components/ui/chip.tsx +60 -0
  21. package/src/components/ui/collapsible.tsx +44 -0
  22. package/src/components/ui/color-picker.tsx +191 -0
  23. package/src/components/ui/combobox.tsx +317 -0
  24. package/src/components/ui/command.tsx +187 -0
  25. package/src/components/ui/confirm-dialog.tsx +125 -0
  26. package/src/components/ui/context-menu.tsx +254 -0
  27. package/src/components/ui/copyable-text.tsx +65 -0
  28. package/src/components/ui/date-picker.tsx +316 -0
  29. package/src/components/ui/date-time-picker.tsx +1071 -0
  30. package/src/components/ui/dialog.tsx +176 -0
  31. package/src/components/ui/direction.tsx +15 -0
  32. package/src/components/ui/drawer.tsx +178 -0
  33. package/src/components/ui/dropdown-menu.tsx +247 -0
  34. package/src/components/ui/empty.tsx +131 -0
  35. package/src/components/ui/field.tsx +257 -0
  36. package/src/components/ui/formatted-input.tsx +219 -0
  37. package/src/components/ui/gallery.tsx +161 -0
  38. package/src/components/ui/hover-card.tsx +77 -0
  39. package/src/components/ui/icon-picker.tsx +193 -0
  40. package/src/components/ui/image-cropper.tsx +787 -0
  41. package/src/components/ui/image-upload-field.tsx +344 -0
  42. package/src/components/ui/input-group.tsx +175 -0
  43. package/src/components/ui/input-otp.tsx +83 -0
  44. package/src/components/ui/input.tsx +21 -0
  45. package/src/components/ui/item.tsx +212 -0
  46. package/src/components/ui/kbd.tsx +62 -0
  47. package/src/components/ui/label.tsx +19 -0
  48. package/src/components/ui/list.tsx +46 -0
  49. package/src/components/ui/menu-variants.ts +71 -0
  50. package/src/components/ui/menubar.tsx +226 -0
  51. package/src/components/ui/money-input.tsx +96 -0
  52. package/src/components/ui/native-date-picker.tsx +120 -0
  53. package/src/components/ui/native-select.tsx +74 -0
  54. package/src/components/ui/navigation-menu.tsx +183 -0
  55. package/src/components/ui/pagination.tsx +150 -0
  56. package/src/components/ui/popover.tsx +104 -0
  57. package/src/components/ui/profile-header.tsx +196 -0
  58. package/src/components/ui/progress.tsx +71 -0
  59. package/src/components/ui/radio-group.tsx +115 -0
  60. package/src/components/ui/rating.tsx +200 -0
  61. package/src/components/ui/resizable.tsx +61 -0
  62. package/src/components/ui/responsive-dialog.tsx +278 -0
  63. package/src/components/ui/responsive-select.tsx +289 -0
  64. package/src/components/ui/responsive-sheet.tsx +225 -0
  65. package/src/components/ui/scroll-area.tsx +51 -0
  66. package/src/components/ui/section.tsx +106 -0
  67. package/src/components/ui/segmented-control.tsx +148 -0
  68. package/src/components/ui/select.tsx +216 -0
  69. package/src/components/ui/separator.tsx +69 -0
  70. package/src/components/ui/sheet.tsx +154 -0
  71. package/src/components/ui/shortcut.tsx +41 -0
  72. package/src/components/ui/sidebar.tsx +739 -0
  73. package/src/components/ui/skeleton.tsx +23 -0
  74. package/src/components/ui/slider.tsx +74 -0
  75. package/src/components/ui/sonner.tsx +64 -0
  76. package/src/components/ui/spinner.tsx +19 -0
  77. package/src/components/ui/stat-card.tsx +138 -0
  78. package/src/components/ui/stepper.tsx +181 -0
  79. package/src/components/ui/sticky-action-bar.tsx +42 -0
  80. package/src/components/ui/switch.tsx +30 -0
  81. package/src/components/ui/table.tsx +132 -0
  82. package/src/components/ui/tabs.tsx +77 -0
  83. package/src/components/ui/textarea.tsx +24 -0
  84. package/src/components/ui/time-picker.tsx +221 -0
  85. package/src/components/ui/toggle-group.tsx +86 -0
  86. package/src/components/ui/toggle.tsx +43 -0
  87. package/src/components/ui/tooltip.tsx +79 -0
  88. package/src/components/ui/translation-input.tsx +126 -0
  89. package/src/components/ui/user-avatar.tsx +74 -0
  90. package/src/css-modules.d.ts +4 -0
  91. package/src/hooks/use-command-palette.ts +28 -0
  92. package/src/hooks/use-mobile.ts +21 -0
  93. package/src/hooks/use-pagination.ts +81 -0
  94. package/src/hooks/use-picker-state.ts +44 -0
  95. package/src/lib/cva.ts +18 -0
  96. package/src/lib/time-math.ts +62 -0
  97. package/src/lib/utils.ts +14 -0
  98. package/src/styles/globals.css +36 -0
  99. package/src/styles.css +10 -0
@@ -0,0 +1,23 @@
1
+ import type * as React from "react";
2
+
3
+ import { cn } from "#/lib/utils.ts";
4
+
5
+ /**
6
+ * Purely visual placeholder with no semantics of its own: wrap the loading
7
+ * region in `role="status"` with sr-only "Loading..." text (or mark the
8
+ * skeletons `aria-hidden`) so screen readers announce the load.
9
+ */
10
+ function Skeleton({ className, ...props }: React.ComponentProps<"div">) {
11
+ return (
12
+ <div
13
+ data-slot="skeleton"
14
+ className={cn(
15
+ "animate-pulse rounded-md bg-muted motion-reduce:animate-none",
16
+ className,
17
+ )}
18
+ {...props}
19
+ />
20
+ );
21
+ }
22
+
23
+ export { Skeleton };
@@ -0,0 +1,74 @@
1
+ import { Slider as SliderPrimitive } from "@base-ui/react/slider";
2
+
3
+ import { cn } from "#/lib/utils.ts";
4
+
5
+ function resolveThumbValues(
6
+ value: SliderPrimitive.Root.Props["value"],
7
+ defaultValue: SliderPrimitive.Root.Props["defaultValue"],
8
+ min: number,
9
+ ): readonly number[] {
10
+ const source = value ?? defaultValue;
11
+ if (Array.isArray(source)) return source;
12
+ if (typeof source === "number") return [source];
13
+ return [min];
14
+ }
15
+
16
+ function Slider({
17
+ className,
18
+ children,
19
+ defaultValue,
20
+ value,
21
+ min = 0,
22
+ max = 100,
23
+ ...props
24
+ }: SliderPrimitive.Root.Props) {
25
+ const thumbValues = resolveThumbValues(value, defaultValue, min);
26
+
27
+ return (
28
+ <SliderPrimitive.Root
29
+ className={cn(
30
+ "data-[orientation=horizontal]:w-full data-[orientation=vertical]:h-full",
31
+ className,
32
+ )}
33
+ data-slot="slider"
34
+ defaultValue={defaultValue}
35
+ value={value}
36
+ min={min}
37
+ max={max}
38
+ thumbAlignment="edge"
39
+ {...props}
40
+ >
41
+ <SliderPrimitive.Control className="relative flex w-full touch-none items-center select-none data-disabled:opacity-50 data-[orientation=vertical]:h-full data-[orientation=vertical]:min-h-40 data-[orientation=vertical]:w-auto data-[orientation=vertical]:flex-col">
42
+ <SliderPrimitive.Track
43
+ data-slot="slider-track"
44
+ className="relative grow overflow-hidden rounded-full bg-muted select-none data-[orientation=horizontal]:h-1 data-[orientation=horizontal]:w-full data-[orientation=vertical]:h-full data-[orientation=vertical]:w-1"
45
+ >
46
+ <SliderPrimitive.Indicator
47
+ data-slot="slider-range"
48
+ className="bg-primary select-none data-[orientation=horizontal]:h-full data-[orientation=vertical]:w-full"
49
+ />
50
+ </SliderPrimitive.Track>
51
+ {Array.from({ length: thumbValues.length }, (_, index) => (
52
+ <SliderPrimitive.Thumb
53
+ data-slot="slider-thumb"
54
+ key={index}
55
+ className="relative block size-3 shrink-0 rounded-full border border-ring bg-background ring-ring/50 transition-[color,box-shadow] select-none after:absolute after:-inset-2 hover:ring-3 focus-visible:ring-3 focus-visible:outline-none active:ring-3 data-disabled:pointer-events-none data-disabled:opacity-50"
56
+ />
57
+ ))}
58
+ </SliderPrimitive.Control>
59
+ {children}
60
+ </SliderPrimitive.Root>
61
+ );
62
+ }
63
+
64
+ function SliderValue({ className, ...props }: SliderPrimitive.Value.Props) {
65
+ return (
66
+ <SliderPrimitive.Value
67
+ data-slot="slider-value"
68
+ className={cn("text-sm text-muted-foreground tabular-nums", className)}
69
+ {...props}
70
+ />
71
+ );
72
+ }
73
+
74
+ export { Slider, SliderValue };
@@ -0,0 +1,64 @@
1
+ import {
2
+ CheckCircleIcon,
3
+ InfoIcon,
4
+ WarningIcon,
5
+ WarningOctagonIcon,
6
+ } from "@phosphor-icons/react";
7
+ import * as React from "react";
8
+ import { Toaster as Sonner, type ToasterProps } from "sonner";
9
+ import { Spinner } from "#/components/ui/spinner.tsx";
10
+
11
+ /* Theme is resolved from the `.dark` class on <html> — the same mechanism the
12
+ * design tokens use — so the Toaster works without any theme provider. */
13
+ function subscribeToRootThemeClass(onChange: () => void): () => void {
14
+ const observer = new MutationObserver(onChange);
15
+ observer.observe(document.documentElement, {
16
+ attributes: true,
17
+ attributeFilter: ["class"],
18
+ });
19
+ return () => observer.disconnect();
20
+ }
21
+
22
+ function getResolvedTheme(): "light" | "dark" {
23
+ return document.documentElement.classList.contains("dark") ? "dark" : "light";
24
+ }
25
+
26
+ function useResolvedTheme(): "light" | "dark" {
27
+ return React.useSyncExternalStore(
28
+ subscribeToRootThemeClass,
29
+ getResolvedTheme,
30
+ () => "light",
31
+ );
32
+ }
33
+
34
+ function Toaster(props: ToasterProps) {
35
+ const theme = useResolvedTheme();
36
+
37
+ return (
38
+ <Sonner
39
+ theme={theme}
40
+ className="toaster group"
41
+ closeButton
42
+ icons={{
43
+ success: <CheckCircleIcon className="size-4 text-success" />,
44
+ info: <InfoIcon className="size-4" />,
45
+ warning: <WarningIcon className="size-4 text-warning" />,
46
+ error: <WarningOctagonIcon className="size-4 text-destructive" />,
47
+ loading: <Spinner />,
48
+ }}
49
+ style={
50
+ {
51
+ "--normal-bg": "var(--popover)",
52
+ "--normal-text": "var(--popover-foreground)",
53
+ "--normal-border": "var(--border)",
54
+ "--border-radius": "var(--radius)",
55
+ } as React.CSSProperties
56
+ }
57
+ {...props}
58
+ />
59
+ );
60
+ }
61
+
62
+ export { toast } from "sonner";
63
+ export type { ToasterProps };
64
+ export { Toaster };
@@ -0,0 +1,19 @@
1
+ import { SpinnerGapIcon } from "@phosphor-icons/react";
2
+ import { cn } from "#/lib/utils.ts";
3
+
4
+ function Spinner({ className, ...props }: React.ComponentProps<"svg">) {
5
+ return (
6
+ <SpinnerGapIcon
7
+ data-slot="spinner"
8
+ role="status"
9
+ aria-label="Loading"
10
+ className={cn(
11
+ "size-4 animate-spin motion-reduce:animate-[spin_3s_linear_infinite]",
12
+ className,
13
+ )}
14
+ {...props}
15
+ />
16
+ );
17
+ }
18
+
19
+ export { Spinner };
@@ -0,0 +1,138 @@
1
+ import { TrendDownIcon, TrendUpIcon } from "@phosphor-icons/react";
2
+
3
+ import { Card } from "#/components/ui/card.tsx";
4
+ import { cva, type VariantProps } from "#/lib/cva.ts";
5
+ import { cn } from "#/lib/utils.ts";
6
+
7
+ /**
8
+ * KPI tile for dashboards: a Card composition pairing a muted label, a large
9
+ * value, an optional trend delta and an optional full-bleed sparkline. Put a
10
+ * `Chart.Root` inside `StatCardChart` — or `Chart.Skeleton` / `Chart.Empty`
11
+ * (from `@voila.dev/ui-chart`) while the metric is loading or has no data.
12
+ */
13
+ function StatCard({ className, ...props }: React.ComponentProps<typeof Card>) {
14
+ return (
15
+ <Card
16
+ data-slot="stat-card"
17
+ className={cn("gap-1.5", className)}
18
+ {...props}
19
+ />
20
+ );
21
+ }
22
+
23
+ function StatCardHeader({ className, ...props }: React.ComponentProps<"div">) {
24
+ return (
25
+ <div
26
+ data-slot="stat-card-header"
27
+ className={cn(
28
+ "flex items-center justify-between gap-2 px-4 group-data-[size=sm]/card:px-3",
29
+ className,
30
+ )}
31
+ {...props}
32
+ />
33
+ );
34
+ }
35
+
36
+ function StatCardLabel({ className, ...props }: React.ComponentProps<"div">) {
37
+ return (
38
+ <div
39
+ data-slot="stat-card-label"
40
+ className={cn("truncate text-sm text-muted-foreground", className)}
41
+ {...props}
42
+ />
43
+ );
44
+ }
45
+
46
+ function StatCardValue({ className, ...props }: React.ComponentProps<"div">) {
47
+ return (
48
+ <div
49
+ data-slot="stat-card-value"
50
+ className={cn(
51
+ "px-4 text-2xl font-semibold tracking-tight tabular-nums group-data-[size=sm]/card:px-3",
52
+ className,
53
+ )}
54
+ {...props}
55
+ />
56
+ );
57
+ }
58
+
59
+ type StatCardTrend = "up" | "down" | "neutral";
60
+
61
+ const statCardDeltaVariants = cva({
62
+ base: "inline-flex shrink-0 items-center gap-1 text-xs font-medium tabular-nums [&_svg]:size-3.5 [&_svg]:shrink-0",
63
+ variants: {
64
+ trend: {
65
+ up: "text-success",
66
+ down: "text-destructive",
67
+ neutral: "text-muted-foreground",
68
+ },
69
+ },
70
+ defaultVariants: {
71
+ trend: "neutral",
72
+ },
73
+ });
74
+
75
+ const trendIcons: Record<
76
+ StatCardTrend,
77
+ React.ComponentType<{ "aria-hidden"?: boolean }> | null
78
+ > = {
79
+ up: TrendUpIcon,
80
+ down: TrendDownIcon,
81
+ neutral: null,
82
+ };
83
+
84
+ /**
85
+ * Trend indicator next to the label or value. `up` renders in the success
86
+ * color and `down` in the destructive color - for metrics where down is the
87
+ * good direction (e.g. cancellations), invert with `className` ("text-success"
88
+ * / "text-destructive" win over the variant color).
89
+ */
90
+ function StatCardDelta({
91
+ className,
92
+ trend = "neutral",
93
+ children,
94
+ ...props
95
+ }: React.ComponentProps<"div"> & VariantProps<typeof statCardDeltaVariants>) {
96
+ const TrendIcon = trendIcons[trend];
97
+ return (
98
+ <div
99
+ data-slot="stat-card-delta"
100
+ data-trend={trend}
101
+ className={cn(statCardDeltaVariants({ trend }), className)}
102
+ {...props}
103
+ >
104
+ {TrendIcon ? <TrendIcon aria-hidden /> : null}
105
+ {children}
106
+ </div>
107
+ );
108
+ }
109
+
110
+ /**
111
+ * Full-bleed chart slot pinned to the bottom edge of the card: the negative
112
+ * margin cancels the card's bottom padding and the card's `overflow-hidden`
113
+ * clips the chart to the rounded corners. Constrain the chart's height on the
114
+ * `Chart.Root` itself (e.g. `className="h-16 w-full"`).
115
+ */
116
+ function StatCardChart({ className, ...props }: React.ComponentProps<"div">) {
117
+ return (
118
+ <div
119
+ data-slot="stat-card-chart"
120
+ className={cn(
121
+ "mt-auto -mb-4 pt-2 group-data-[size=sm]/card:-mb-3",
122
+ className,
123
+ )}
124
+ {...props}
125
+ />
126
+ );
127
+ }
128
+
129
+ export {
130
+ StatCard,
131
+ StatCardChart,
132
+ StatCardDelta,
133
+ StatCardHeader,
134
+ StatCardLabel,
135
+ type StatCardTrend,
136
+ StatCardValue,
137
+ statCardDeltaVariants,
138
+ };
@@ -0,0 +1,181 @@
1
+ import { CheckIcon } from "@phosphor-icons/react";
2
+ import * as React from "react";
3
+
4
+ import { cn } from "#/lib/utils.ts";
5
+
6
+ type StepperOrientation = "horizontal" | "vertical";
7
+ type StepperStepState = "completed" | "active" | "inactive";
8
+
9
+ const StepperContext = React.createContext<{
10
+ value: number;
11
+ orientation: StepperOrientation;
12
+ } | null>(null);
13
+
14
+ function useStepperContext(componentName: string) {
15
+ const context = React.useContext(StepperContext);
16
+ if (!context) {
17
+ throw new Error(`<${componentName}> must be used within <Stepper>`);
18
+ }
19
+ return context;
20
+ }
21
+
22
+ const StepperItemContext = React.createContext<{
23
+ step: number;
24
+ state: StepperStepState;
25
+ } | null>(null);
26
+
27
+ function useStepperItemContext(componentName: string) {
28
+ const context = React.useContext(StepperItemContext);
29
+ if (!context) {
30
+ throw new Error(`<${componentName}> must be used within <StepperItem>`);
31
+ }
32
+ return context;
33
+ }
34
+
35
+ /**
36
+ * Presentational progress indicator for multi-step flows (onboarding,
37
+ * booking). `value` is the 1-based active step: steps below it render as
38
+ * completed, the step equal to it as active, the rest as inactive. Navigation
39
+ * stays with the consumer - wire buttons or router links around it.
40
+ */
41
+ function Stepper({
42
+ className,
43
+ value,
44
+ orientation = "horizontal",
45
+ ...props
46
+ }: React.ComponentProps<"ol"> & {
47
+ value: number;
48
+ orientation?: StepperOrientation;
49
+ }) {
50
+ const contextValue = React.useMemo(
51
+ () => ({ value, orientation }),
52
+ [value, orientation],
53
+ );
54
+ return (
55
+ <StepperContext.Provider value={contextValue}>
56
+ <ol
57
+ data-slot="stepper"
58
+ data-orientation={orientation}
59
+ className={cn(
60
+ "group/stepper flex w-full data-[orientation=horizontal]:items-center data-[orientation=horizontal]:gap-2 data-[orientation=vertical]:flex-col",
61
+ className,
62
+ )}
63
+ {...props}
64
+ />
65
+ </StepperContext.Provider>
66
+ );
67
+ }
68
+
69
+ function StepperItem({
70
+ className,
71
+ step,
72
+ ...props
73
+ }: React.ComponentProps<"li"> & {
74
+ step: number;
75
+ }) {
76
+ const { value, orientation } = useStepperContext("StepperItem");
77
+ const state: StepperStepState =
78
+ step < value ? "completed" : step === value ? "active" : "inactive";
79
+ const contextValue = React.useMemo(() => ({ step, state }), [step, state]);
80
+ return (
81
+ <StepperItemContext.Provider value={contextValue}>
82
+ <li
83
+ data-slot="stepper-item"
84
+ data-state={state}
85
+ data-orientation={orientation}
86
+ aria-current={state === "active" ? "step" : undefined}
87
+ className={cn(
88
+ "group/stepper-item flex items-center gap-2 data-[orientation=vertical]:relative data-[orientation=vertical]:items-start data-[orientation=vertical]:pb-6 data-[orientation=vertical]:last:pb-0 data-[orientation=horizontal]:[&:not(:last-child)]:flex-1",
89
+ className,
90
+ )}
91
+ {...props}
92
+ />
93
+ </StepperItemContext.Provider>
94
+ );
95
+ }
96
+
97
+ /**
98
+ * Step circle: renders the step number (or custom children) and swaps to a
99
+ * check mark once the step is completed.
100
+ */
101
+ function StepperIndicator({
102
+ className,
103
+ children,
104
+ ...props
105
+ }: React.ComponentProps<"span">) {
106
+ const { step, state } = useStepperItemContext("StepperIndicator");
107
+ return (
108
+ <span
109
+ data-slot="stepper-indicator"
110
+ className={cn(
111
+ "flex size-6 shrink-0 items-center justify-center rounded-full bg-muted text-xs font-medium text-muted-foreground tabular-nums transition-colors group-data-[state=active]/stepper-item:bg-primary group-data-[state=completed]/stepper-item:bg-primary group-data-[state=active]/stepper-item:text-primary-foreground group-data-[state=completed]/stepper-item:text-primary-foreground [&>svg]:size-3.5",
112
+ className,
113
+ )}
114
+ {...props}
115
+ >
116
+ {state === "completed" ? <CheckIcon aria-hidden /> : (children ?? step)}
117
+ </span>
118
+ );
119
+ }
120
+
121
+ function StepperTitle({ className, ...props }: React.ComponentProps<"div">) {
122
+ return (
123
+ <div
124
+ data-slot="stepper-title"
125
+ className={cn(
126
+ "text-sm leading-tight font-medium whitespace-nowrap group-data-[state=inactive]/stepper-item:text-muted-foreground",
127
+ className,
128
+ )}
129
+ {...props}
130
+ />
131
+ );
132
+ }
133
+
134
+ function StepperDescription({
135
+ className,
136
+ ...props
137
+ }: React.ComponentProps<"div">) {
138
+ return (
139
+ <div
140
+ data-slot="stepper-description"
141
+ className={cn("text-xs text-muted-foreground", className)}
142
+ {...props}
143
+ />
144
+ );
145
+ }
146
+
147
+ /**
148
+ * Connecting line to the next step. Horizontal: a flexible rule filling the
149
+ * gap between items. Vertical: an absolute line dropping from the indicator,
150
+ * sized by the item's bottom padding.
151
+ */
152
+ function StepperSeparator({
153
+ className,
154
+ ...props
155
+ }: React.ComponentProps<"div">) {
156
+ const { orientation } = useStepperContext("StepperSeparator");
157
+ return (
158
+ <div
159
+ data-slot="stepper-separator"
160
+ data-orientation={orientation}
161
+ aria-hidden
162
+ className={cn(
163
+ "bg-border transition-colors group-data-[state=completed]/stepper-item:bg-primary",
164
+ "data-[orientation=horizontal]:h-px data-[orientation=horizontal]:min-w-4 data-[orientation=horizontal]:flex-1",
165
+ "data-[orientation=vertical]:absolute data-[orientation=vertical]:top-7 data-[orientation=vertical]:bottom-1 data-[orientation=vertical]:left-3 data-[orientation=vertical]:w-px data-[orientation=vertical]:-translate-x-1/2",
166
+ className,
167
+ )}
168
+ {...props}
169
+ />
170
+ );
171
+ }
172
+
173
+ export {
174
+ Stepper,
175
+ StepperDescription,
176
+ StepperIndicator,
177
+ StepperItem,
178
+ StepperSeparator,
179
+ type StepperStepState,
180
+ StepperTitle,
181
+ };
@@ -0,0 +1,42 @@
1
+ import type * as React from "react";
2
+ import { cn } from "#/lib/utils.ts";
3
+
4
+ /**
5
+ * Bottom-anchored in-page action bar for a mobile primary action — a sticky
6
+ * footer, NOT a dialog. It sticks to the bottom of the viewport with a
7
+ * top border and a background fill, and reserves extra bottom padding for the
8
+ * iOS home indicator via `env(safe-area-inset-bottom)`.
9
+ *
10
+ * It's shown on mobile and hidden from `md` up by default; pass
11
+ * `hideOnDesktop={false}` (or override via `className`) to keep it on every
12
+ * breakpoint. Children are laid out in a flex row, so multiple buttons sit
13
+ * side by side.
14
+ */
15
+ function StickyActionBar({
16
+ className,
17
+ children,
18
+ hideOnDesktop = true,
19
+ ...props
20
+ }: React.ComponentProps<"div"> & {
21
+ /** Hide the bar from the `md` breakpoint up (default `true`). */
22
+ hideOnDesktop?: boolean;
23
+ }) {
24
+ return (
25
+ <div
26
+ data-slot="sticky-action-bar"
27
+ className={cn(
28
+ "sticky inset-x-0 bottom-0 z-40 flex items-center gap-2 border-t bg-background px-4 pt-3",
29
+ // Extra bottom padding clears the iOS home indicator.
30
+ "pb-[calc(0.75rem+env(safe-area-inset-bottom))]",
31
+ "*:data-[slot=button]:flex-1",
32
+ hideOnDesktop && "md:hidden",
33
+ className,
34
+ )}
35
+ {...props}
36
+ >
37
+ {children}
38
+ </div>
39
+ );
40
+ }
41
+
42
+ export { StickyActionBar };
@@ -0,0 +1,30 @@
1
+ import { Switch as SwitchPrimitive } from "@base-ui/react/switch";
2
+
3
+ import { cn } from "#/lib/utils.ts";
4
+
5
+ type SwitchProps = SwitchPrimitive.Root.Props & {
6
+ size?: "sm" | "default";
7
+ };
8
+
9
+ function Switch({ className, size = "default", ...props }: SwitchProps) {
10
+ return (
11
+ <SwitchPrimitive.Root
12
+ data-slot="switch"
13
+ data-size={size}
14
+ className={cn(
15
+ // Track dimensions live on the spacing scale (default 32×18.4, sm
16
+ // 24×14); the thumb's translate math depends on these widths.
17
+ "peer group/switch relative inline-flex shrink-0 items-center rounded-full border border-transparent transition-all outline-none after:absolute after:-inset-x-3 after:-inset-y-2 focus-visible:border-ring focus-visible:ring-3 focus-visible:ring-ring/50 aria-invalid:border-destructive aria-invalid:ring-3 aria-invalid:ring-destructive/20 data-[size=default]:h-[1.15rem] data-[size=default]:w-8 data-[size=sm]:h-3.5 data-[size=sm]:w-6 motion-reduce:transition-none dark:aria-invalid:border-destructive/50 dark:aria-invalid:ring-destructive/40 data-checked:bg-primary data-unchecked:border-input data-unchecked:bg-input dark:data-unchecked:bg-input/80 data-disabled:cursor-not-allowed data-disabled:opacity-50",
18
+ className,
19
+ )}
20
+ {...props}
21
+ >
22
+ <SwitchPrimitive.Thumb
23
+ data-slot="switch-thumb"
24
+ className="pointer-events-none block rounded-full bg-background transition-transform group-data-[size=default]/switch:size-4 group-data-[size=sm]/switch:size-3 group-data-[size=default]/switch:data-checked:translate-x-[calc(100%-2px)] group-data-[size=sm]/switch:data-checked:translate-x-[calc(100%-2px)] motion-reduce:transition-none dark:data-checked:bg-primary-foreground group-data-[size=default]/switch:data-unchecked:translate-x-0 group-data-[size=sm]/switch:data-unchecked:translate-x-0 dark:data-unchecked:bg-foreground"
25
+ />
26
+ </SwitchPrimitive.Root>
27
+ );
28
+ }
29
+
30
+ export { Switch, type SwitchProps };