@ponti-studios/ui 1.2.0 → 1.3.0

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 (35) hide show
  1. package/package.json +1 -1
  2. package/src/components/data-display/accordion.tsx +10 -1
  3. package/src/components/feedback/empty-state.tsx +1 -1
  4. package/src/components/feedback/file-upload-status.tsx +14 -6
  5. package/src/components/feedback/progress-bar.tsx +3 -2
  6. package/src/components/feedback/spinner.tsx +3 -4
  7. package/src/components/feedback/update-guard.stories.tsx +1 -1
  8. package/src/components/feedback/update-guard.tsx +2 -2
  9. package/src/components/filters/active-filters-bar.stories.tsx +2 -0
  10. package/src/components/filters/active-filters-bar.tsx +21 -23
  11. package/src/components/filters/filter-select.tsx +4 -2
  12. package/src/components/filters/index.ts +0 -4
  13. package/src/components/forms/date-month-select.tsx +2 -2
  14. package/src/components/forms/date-picker.tsx +2 -2
  15. package/src/components/forms/drop-zone.stories.tsx +112 -28
  16. package/src/components/forms/drop-zone.tsx +265 -112
  17. package/src/components/forms/input.tsx +5 -1
  18. package/src/components/forms/radio-group.tsx +3 -2
  19. package/src/components/forms/select.tsx +14 -4
  20. package/src/components/forms/slider.stories.tsx +2 -2
  21. package/src/components/forms/slider.tsx +12 -9
  22. package/src/components/forms/switch.tsx +5 -2
  23. package/src/components/forms/textarea.tsx +5 -1
  24. package/src/components/layout/index.ts +0 -2
  25. package/src/components/layout/section-intro.tsx +1 -1
  26. package/src/components/layout/surface-panel.tsx +10 -2
  27. package/src/components/navigation/tabs.tsx +4 -1
  28. package/src/components/overlays/command.tsx +39 -14
  29. package/src/components/overlays/popover.stories.tsx +16 -4
  30. package/src/components/primitives/badge.tsx +1 -1
  31. package/src/components/primitives/button.tsx +6 -1
  32. package/src/components/filters/filters.stories.tsx +0 -72
  33. package/src/components/filters/filters.tsx +0 -72
  34. package/src/components/filters/search-filter-bar.stories.tsx +0 -39
  35. package/src/components/filters/search-filter-bar.tsx +0 -46
@@ -1,146 +1,299 @@
1
- import type React from "react";
2
- import { useCallback, useId } from "react";
1
+ import {
2
+ useCallback,
3
+ useId,
4
+ useRef,
5
+ useState,
6
+ type ChangeEvent,
7
+ type DragEvent,
8
+ type KeyboardEvent,
9
+ type ReactNode,
10
+ } from "react";
11
+ import { FileIcon, UploadIcon, XIcon } from "lucide-react";
3
12
 
4
13
  import { cn } from "../../lib/utils";
14
+ import { Button } from "../primitives/button";
5
15
 
6
- export interface DropZoneProps {
7
- isImporting: boolean;
8
- dragActive: boolean;
9
- onDrop: (files: File[]) => void;
10
- onDragOver: () => void;
11
- onDragLeave: () => void;
12
- onChange?: (files: File[]) => void;
16
+ export type DropZoneStatus = "empty" | "armed" | "busy" | "failed";
17
+
18
+ export type DropZoneFileInfo = {
19
+ name: string;
20
+ size: number;
21
+ };
22
+
23
+ export type DropZoneProps = {
24
+ status: DropZoneStatus;
25
+ /** Selected file metadata for armed / busy / failed (with file retained). */
26
+ file?: DropZoneFileInfo | null;
27
+ error?: string | null;
28
+ notice?: string | null;
29
+ /** File input accept attribute, e.g. `.pdf,application/pdf`. */
13
30
  accept?: string;
14
31
  multiple?: boolean;
32
+ disabled?: boolean;
15
33
  className?: string;
16
- label?: string;
17
- helpText?: string;
34
+ /** Empty idle copy */
35
+ emptyLabel?: ReactNode;
36
+ emptyHint?: ReactNode;
37
+ draggingLabel?: ReactNode;
38
+ /** Busy copy */
39
+ busyLabel?: ReactNode;
40
+ busyDescription?: ReactNode;
41
+ /** Action labels — parent owns domain language */
42
+ submitLabel?: string;
43
+ retryLabel?: string;
44
+ clearLabel?: string;
45
+ cancelLabel?: string;
46
+ /** Extra content under error (e.g. sign-in link). */
47
+ failedActions?: ReactNode;
48
+ onFiles: (files: File[]) => void;
49
+ onSubmit?: () => void;
50
+ onClear?: () => void;
51
+ onCancel?: () => void;
52
+ onRetry?: () => void;
53
+ };
54
+
55
+ function formatFileSize(bytes: number) {
56
+ if (bytes < 1024) return `${bytes} B`;
57
+ if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
58
+ return `${(bytes / (1024 * 1024)).toFixed(2)} MB`;
18
59
  }
19
60
 
20
- /** Drag-and-drop file picker with a click-to-browse fallback. */
61
+ /**
62
+ * Generic file drop surface. Parent owns validation, network, and domain copy.
63
+ * Modes: empty | armed | busy | failed, plus an internal drag overlay.
64
+ */
21
65
  export function DropZone({
22
- isImporting,
23
- dragActive,
24
- onDrop,
25
- onDragOver,
26
- onDragLeave,
27
- onChange,
66
+ status,
67
+ file = null,
68
+ error = null,
69
+ notice = null,
28
70
  accept,
29
- multiple = true,
71
+ multiple = false,
72
+ disabled = false,
30
73
  className,
31
- label = "Drag and drop files here, or",
32
- helpText,
74
+ emptyLabel = (
75
+ <>
76
+ Drop a file here, or <span className="text-primary font-medium">browse</span>
77
+ </>
78
+ ),
79
+ emptyHint,
80
+ draggingLabel = "Release to attach",
81
+ busyLabel = "Processing…",
82
+ busyDescription,
83
+ submitLabel = "Upload",
84
+ retryLabel = "Try again",
85
+ clearLabel = "Use a different file",
86
+ cancelLabel = "Cancel",
87
+ failedActions,
88
+ onFiles,
89
+ onSubmit,
90
+ onClear,
91
+ onCancel,
92
+ onRetry,
33
93
  }: DropZoneProps) {
34
94
  const inputId = useId();
95
+ const inputRef = useRef<HTMLInputElement>(null);
96
+ const zoneRef = useRef<HTMLDivElement>(null);
97
+ const [dragging, setDragging] = useState(false);
35
98
 
36
- const handleDrop = useCallback(
37
- (e: React.DragEvent<HTMLButtonElement>) => {
38
- e.preventDefault();
39
- e.stopPropagation();
40
- onDragLeave();
99
+ const isBusy = status === "busy";
100
+ const isInteractive = !disabled && !isBusy;
41
101
 
42
- if (e.dataTransfer.files && e.dataTransfer.files.length > 0) {
43
- onDrop(Array.from(e.dataTransfer.files));
44
- }
102
+ const openPicker = useCallback(() => {
103
+ if (!isInteractive) return;
104
+ inputRef.current?.click();
105
+ }, [isInteractive]);
106
+
107
+ const handleFiles = useCallback(
108
+ (list: FileList | File[] | null) => {
109
+ if (!list || !isInteractive) return;
110
+ const files = Array.from(list);
111
+ if (files.length === 0) return;
112
+ onFiles(files);
45
113
  },
46
- [onDrop, onDragLeave],
114
+ [isInteractive, onFiles],
47
115
  );
48
116
 
49
- const triggerFileInput = useCallback(() => {
50
- if (!isImporting) {
51
- document.getElementById(inputId)?.click();
52
- }
53
- }, [isImporting, inputId]);
117
+ const onDragEnter = (event: DragEvent) => {
118
+ event.preventDefault();
119
+ event.stopPropagation();
120
+ if (!isInteractive) return;
121
+ setDragging(true);
122
+ };
54
123
 
55
- const handleDragOver = useCallback(
56
- (e: React.DragEvent<HTMLButtonElement>) => {
57
- e.preventDefault();
58
- e.stopPropagation();
59
- onDragOver();
60
- },
61
- [onDragOver],
62
- );
124
+ const onDragOver = (event: DragEvent) => {
125
+ event.preventDefault();
126
+ event.stopPropagation();
127
+ if (event.dataTransfer) event.dataTransfer.dropEffect = "copy";
128
+ };
63
129
 
64
- const handleDragLeave = useCallback(
65
- (e: React.DragEvent<HTMLButtonElement>) => {
66
- e.preventDefault();
67
- e.stopPropagation();
68
- onDragLeave();
69
- },
70
- [onDragLeave],
71
- );
130
+ const onDragLeave = (event: DragEvent) => {
131
+ event.preventDefault();
132
+ event.stopPropagation();
133
+ const next = event.relatedTarget;
134
+ if (next instanceof Node && zoneRef.current?.contains(next)) return;
135
+ setDragging(false);
136
+ };
72
137
 
73
- const handleClick = useCallback(
74
- (e: React.MouseEvent<HTMLButtonElement>) => {
75
- e.stopPropagation();
76
- triggerFileInput();
77
- },
78
- [triggerFileInput],
79
- );
138
+ const onDrop = (event: DragEvent) => {
139
+ event.preventDefault();
140
+ event.stopPropagation();
141
+ setDragging(false);
142
+ if (!isInteractive) return;
143
+ handleFiles(event.dataTransfer.files);
144
+ };
80
145
 
81
- const handleKeyDown = useCallback(
82
- (e: React.KeyboardEvent<HTMLButtonElement>) => {
83
- if (e.key === "Enter" || e.key === " ") {
84
- e.preventDefault();
85
- e.stopPropagation();
86
- triggerFileInput();
87
- }
88
- },
89
- [triggerFileInput],
90
- );
146
+ const onInputChange = (event: ChangeEvent<HTMLInputElement>) => {
147
+ handleFiles(event.target.files);
148
+ event.target.value = "";
149
+ };
91
150
 
92
- const handleInputChange = useCallback(
93
- (e: React.ChangeEvent<HTMLInputElement>) => {
94
- e.stopPropagation();
95
- if (onChange) {
96
- onChange(e.target.files ? Array.from(e.target.files) : []);
97
- }
98
- },
99
- [onChange],
151
+ const onKeyDown = (event: KeyboardEvent) => {
152
+ if (event.key === "Enter" || event.key === " ") {
153
+ event.preventDefault();
154
+ openPicker();
155
+ }
156
+ };
157
+
158
+ const showDragging = dragging && isInteractive;
159
+ const frameTone = cn(
160
+ showDragging && "border-primary bg-primary/5",
161
+ status === "failed" && !showDragging && "border-destructive/50 bg-destructive/5",
162
+ status === "busy" && "border-border bg-muted/30",
163
+ status !== "failed" && status !== "busy" && !showDragging && "border-border bg-card",
100
164
  );
101
165
 
102
166
  return (
103
- <div>
104
- <button
105
- type="button"
106
- className={cn(
107
- "relative border-2 border-dashed p-12",
108
- {
109
- "border-primary bg-primary/5": dragActive,
110
- "border-border bg-muted": !dragActive,
111
- },
112
- "flex flex-col items-center justify-center gap-4",
113
- {
114
- "pointer-events-none cursor-not-allowed": isImporting,
115
- },
116
- className,
117
- )}
118
- onDragOver={handleDragOver}
119
- onDragLeave={handleDragLeave}
120
- onDrop={handleDrop}
121
- onClick={handleClick}
122
- onKeyDown={handleKeyDown}
123
- tabIndex={isImporting ? -1 : 0}
124
- aria-disabled={isImporting}
125
- aria-label="Upload file area. Press Enter or Space to open file browser"
126
- >
127
- <div className="space-y-2 text-center">
128
- <p className="text-muted-foreground">
129
- {label} <span className="text-accent font-medium">click to browse</span>
130
- </p>
131
- {helpText ? <p className="text-muted-foreground text-sm">{helpText}</p> : null}
132
- </div>
133
- </button>
167
+ <div className={cn("w-full space-y-3", className)}>
134
168
  <input
169
+ ref={inputRef}
135
170
  id={inputId}
136
171
  type="file"
137
- multiple={multiple}
138
172
  accept={accept}
139
- onChange={handleInputChange}
140
- className="sr-only"
141
- disabled={isImporting}
142
- aria-label="File input"
173
+ multiple={multiple}
174
+ disabled={!isInteractive}
175
+ className="pointer-events-none sr-only"
176
+ onChange={onInputChange}
177
+ tabIndex={-1}
178
+ aria-hidden
143
179
  />
180
+
181
+ <div
182
+ ref={zoneRef}
183
+ role="button"
184
+ tabIndex={isInteractive ? 0 : -1}
185
+ aria-disabled={!isInteractive}
186
+ aria-busy={isBusy}
187
+ aria-label="File upload drop zone"
188
+ className={cn(
189
+ "relative flex min-h-44 w-full flex-col items-center justify-center overflow-hidden rounded-2xl border border-dashed p-6 text-center outline-none",
190
+ "focus-visible:ring-ring focus-visible:ring-2 focus-visible:ring-offset-2",
191
+ "motion-safe:transition-[transform,opacity] motion-safe:duration-200 motion-safe:ease-in-out motion-reduce:transition-none",
192
+ isInteractive ? "cursor-pointer" : "cursor-default",
193
+ showDragging ? "scale-[1.01] motion-reduce:scale-100" : "scale-100",
194
+ frameTone,
195
+ )}
196
+ onClick={isInteractive ? openPicker : undefined}
197
+ onKeyDown={isInteractive ? onKeyDown : undefined}
198
+ onDragEnter={onDragEnter}
199
+ onDragOver={onDragOver}
200
+ onDragLeave={onDragLeave}
201
+ onDrop={onDrop}
202
+ >
203
+ <div className="flex w-full max-w-sm flex-col items-center gap-3">
204
+ {showDragging ? (
205
+ <>
206
+ <UploadIcon
207
+ className="text-primary size-8 motion-safe:transition-[transform,opacity] motion-safe:duration-200 motion-safe:ease-in-out motion-reduce:transition-none motion-safe:-translate-y-0.5 motion-reduce:translate-y-0"
208
+ aria-hidden
209
+ />
210
+ <p className="body-3 text-foreground">{draggingLabel}</p>
211
+ </>
212
+ ) : null}
213
+
214
+ {!showDragging && status === "empty" ? (
215
+ <>
216
+ <UploadIcon className="size-8 text-muted-foreground" aria-hidden />
217
+ <div className="space-y-1">
218
+ <p className="body-3 text-foreground">{emptyLabel}</p>
219
+ {emptyHint ? <p className="body-4 text-muted-foreground">{emptyHint}</p> : null}
220
+ </div>
221
+ </>
222
+ ) : null}
223
+
224
+ {!showDragging && status === "armed" && file ? (
225
+ <>
226
+ <FileIcon className="size-8 text-muted-foreground" aria-hidden />
227
+ <div className="min-w-0 max-w-full space-y-1">
228
+ <p className="subheading-4 truncate text-foreground">{file.name}</p>
229
+ <p className="body-4 text-muted-foreground">{formatFileSize(file.size)}</p>
230
+ </div>
231
+ </>
232
+ ) : null}
233
+
234
+ {!showDragging && status === "busy" ? (
235
+ <div className="w-full space-y-3">
236
+ <div className="space-y-1">
237
+ <p className="subheading-4 text-foreground">{busyLabel}</p>
238
+ {busyDescription ? (
239
+ <p className="body-4 text-muted-foreground">{busyDescription}</p>
240
+ ) : null}
241
+ {file ? <p className="body-4 truncate text-muted-foreground">{file.name}</p> : null}
242
+ </div>
243
+ <div
244
+ className="bg-muted h-1.5 w-full overflow-hidden rounded-full"
245
+ role="progressbar"
246
+ aria-label={typeof busyLabel === "string" ? busyLabel : "Processing"}
247
+ >
248
+ <div className="bg-primary h-full w-2/5 rounded-full opacity-80 motion-safe:animate-pulse" />
249
+ </div>
250
+ </div>
251
+ ) : null}
252
+
253
+ {!showDragging && status === "failed" ? (
254
+ <div className="w-full space-y-2">
255
+ {file ? <p className="subheading-4 truncate text-foreground">{file.name}</p> : null}
256
+ {error ? <p className="body-3 text-destructive">{error}</p> : null}
257
+ {failedActions}
258
+ </div>
259
+ ) : null}
260
+ </div>
261
+ </div>
262
+
263
+ {notice ? <p className="body-4 text-muted-foreground">{notice}</p> : null}
264
+
265
+ <div className="grid gap-2">
266
+ {status === "armed" && onSubmit ? (
267
+ <Button type="button" className="w-full" onClick={onSubmit}>
268
+ <UploadIcon className="size-4" />
269
+ {submitLabel}
270
+ </Button>
271
+ ) : null}
272
+ {status === "failed" && onRetry ? (
273
+ <Button type="button" className="w-full" onClick={onRetry}>
274
+ {retryLabel}
275
+ </Button>
276
+ ) : null}
277
+ {status === "busy" && onCancel ? (
278
+ <Button type="button" variant="outline" className="w-full" onClick={onCancel}>
279
+ <XIcon className="size-4" />
280
+ {cancelLabel}
281
+ </Button>
282
+ ) : null}
283
+ {(status === "armed" || status === "failed") && onClear ? (
284
+ <Button
285
+ type="button"
286
+ variant="ghost"
287
+ className="w-full"
288
+ onClick={(event) => {
289
+ event.stopPropagation();
290
+ onClear();
291
+ }}
292
+ >
293
+ {clearLabel}
294
+ </Button>
295
+ ) : null}
296
+ </div>
144
297
  </div>
145
298
  );
146
299
  }
@@ -8,7 +8,11 @@ function Input({ className, type, ...props }: React.ComponentProps<"input">) {
8
8
  type={type}
9
9
  data-slot="input"
10
10
  className={cn(
11
- "file:text-foreground placeholder:text-muted-foreground selection:bg-primary selection:text-primary-foreground bg-background text-foreground focus-visible:ring-ring border-input h-9 w-full min-w-0 rounded-md border px-3 py-1 text-base transition-colors file:inline-flex file:h-7 file:border-0 file:bg-transparent file:text-sm file:font-medium focus-visible:ring-2 focus-visible:ring-offset-2 focus-visible:outline-none disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50 md:text-sm",
11
+ "bg-background text-foreground border-input h-9 w-full min-w-0 rounded-md border px-3 py-1 text-base transition-colors md:text-sm",
12
+ "file:text-foreground file:inline-flex file:h-7 file:border-0 file:bg-transparent file:text-sm file:font-medium",
13
+ "placeholder:text-muted-foreground selection:bg-primary selection:text-primary-foreground",
14
+ "focus-visible:ring-ring focus-visible:ring-2 focus-visible:ring-offset-2 focus-visible:outline-none",
15
+ "disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50",
12
16
  "aria-invalid:ring-destructive/20 aria-invalid:border-destructive",
13
17
  className,
14
18
  )}
@@ -23,8 +23,9 @@ const RadioGroupItem = React.forwardRef<
23
23
  <RadioPrimitive.Root
24
24
  ref={ref}
25
25
  className={cn(
26
- "border-primary bg-background focus-visible:border-ring focus-visible:bg-muted/25 focus-visible:ring-ring aspect-square size-6 rounded-full border focus-visible:ring-2 focus-visible:ring-offset-2 focus-visible:outline-none disabled:cursor-not-allowed disabled:opacity-50",
27
- "flex items-center justify-center",
26
+ "border-primary bg-background flex aspect-square size-6 items-center justify-center rounded-full border",
27
+ "focus-visible:border-ring focus-visible:bg-muted/25 focus-visible:ring-ring focus-visible:ring-2 focus-visible:ring-offset-2 focus-visible:outline-none",
28
+ "disabled:cursor-not-allowed disabled:opacity-50",
28
29
  className,
29
30
  )}
30
31
  {...props}
@@ -44,7 +44,14 @@ function SelectTrigger({
44
44
  data-slot="select-trigger"
45
45
  data-size={size}
46
46
  className={cn(
47
- "data-placeholder:text-muted-foreground [&_svg:not([class*='text-'])]:text-muted-foreground focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 aria-invalid:border-destructive border-input flex w-fit items-center justify-between gap-2 rounded-md border bg-transparent px-3 py-2 text-sm whitespace-nowrap transition-[color,box-shadow] outline-none focus-visible:ring-[3px] focus-visible:ring-inset disabled:cursor-not-allowed disabled:opacity-50 data-[size=default]:h-9 data-[size=sm]:h-8 *:data-[slot=select-value]:line-clamp-1 *:data-[slot=select-value]:flex *:data-[slot=select-value]:items-center *:data-[slot=select-value]:gap-2 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
47
+ "border-input flex w-fit items-center justify-between gap-2 rounded-md border bg-transparent px-3 py-2 text-sm whitespace-nowrap transition-[color,box-shadow] outline-none",
48
+ "data-placeholder:text-muted-foreground [&_svg:not([class*='text-'])]:text-muted-foreground",
49
+ "focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] focus-visible:ring-inset",
50
+ "aria-invalid:ring-destructive/20 aria-invalid:border-destructive",
51
+ "disabled:cursor-not-allowed disabled:opacity-50",
52
+ "data-[size=default]:h-9 data-[size=sm]:h-8",
53
+ "*:data-[slot=select-value]:line-clamp-1 *:data-[slot=select-value]:flex *:data-[slot=select-value]:items-center *:data-[slot=select-value]:gap-2",
54
+ "[&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
48
55
  className,
49
56
  )}
50
57
  {...props}
@@ -60,18 +67,21 @@ function SelectTrigger({
60
67
  function SelectContent({
61
68
  className,
62
69
  children,
63
- position: _position = "item-aligned",
70
+ position: _position = "popper",
64
71
  ...props
65
72
  }: React.ComponentProps<typeof SelectPrimitive.Popup> & {
66
73
  position?: "item-aligned" | "popper";
67
74
  }) {
68
75
  return (
69
76
  <SelectPrimitive.Portal>
70
- <SelectPrimitive.Positioner sideOffset={4}>
77
+ <SelectPrimitive.Positioner
78
+ alignItemWithTrigger={_position === "item-aligned"}
79
+ sideOffset={4}
80
+ >
71
81
  <SelectPrimitive.Popup
72
82
  data-slot="select-content"
73
83
  className={cn(
74
- "bg-background text-popover-foreground data-open:animate-in data-closed:animate-out data-closed:zoom-out-95 data-open:zoom-in-95 relative z-50 max-h-(--available-height) min-w-32 origin-(--transform-origin) overflow-x-hidden overflow-y-auto rounded-md border p-1 outline-none",
84
+ "bg-background text-popover-foreground data-open:animate-in data-closed:animate-out data-closed:zoom-out-95 data-open:zoom-in-95 relative z-50 max-h-(--available-height) min-w-(--anchor-width) origin-(--transform-origin) overflow-x-hidden overflow-y-auto rounded-md border p-1 outline-none",
75
85
  className,
76
86
  )}
77
87
  {...props}
@@ -9,7 +9,7 @@ const meta: Meta<typeof Slider> = {
9
9
  tags: ["autodocs"],
10
10
  decorators: [
11
11
  (Story) => (
12
- <div className="w-full max-w-sm px-1">
12
+ <div className="w-80 px-1">
13
13
  <Story />
14
14
  </div>
15
15
  ),
@@ -48,7 +48,7 @@ export const Controlled: Story = {
48
48
  render: () => {
49
49
  const [value, setValue] = React.useState([33]);
50
50
  return (
51
- <div className="w-full max-w-sm space-y-4 px-1">
51
+ <div className="w-80 space-y-4 px-1">
52
52
  <Slider aria-label="Volume" value={value} onValueChange={setValue} min={0} max={100} />
53
53
  <div className="flex justify-between text-sm">
54
54
  <span className="text-muted-foreground">Volume</span>
@@ -27,18 +27,21 @@ const Slider = React.forwardRef<HTMLDivElement, SliderProps>(
27
27
  ) => (
28
28
  <SliderPrimitive.Root
29
29
  ref={ref}
30
- className={cn("relative flex w-full touch-none items-center select-none", className)}
31
30
  {...props}
32
31
  onValueChange={(value) => onValueChange?.(Array.isArray(value) ? [...value] : [value])}
33
32
  >
34
- <SliderPrimitive.Track className="bg-muted relative h-2 w-full grow overflow-hidden rounded-full">
35
- <SliderPrimitive.Indicator className="bg-primary absolute h-full" />
36
- </SliderPrimitive.Track>
37
- <SliderPrimitive.Thumb
38
- aria-label={ariaLabel ?? "Slider"}
39
- aria-labelledby={ariaLabelledby}
40
- className="border-border focus-visible:border-ring focus-visible:bg-primary focus-visible:ring-ring bg-background block h-6 w-6 rounded-full border shadow-sm ring-1 ring-black/5 transition-colors focus-visible:ring-2 focus-visible:ring-offset-2 focus-visible:outline-none disabled:pointer-events-none disabled:opacity-50"
41
- />
33
+ <SliderPrimitive.Control
34
+ className={cn("relative flex w-full touch-none items-center py-2 select-none", className)}
35
+ >
36
+ <SliderPrimitive.Track className="bg-muted relative h-2 w-full grow rounded-full">
37
+ <SliderPrimitive.Indicator className="bg-primary absolute h-full rounded-full" />
38
+ <SliderPrimitive.Thumb
39
+ aria-label={ariaLabel ?? "Slider"}
40
+ aria-labelledby={ariaLabelledby}
41
+ className="border-border focus-visible:border-ring focus-visible:bg-primary focus-visible:ring-ring bg-background z-10 block h-6 w-6 rounded-full border shadow-sm ring-1 ring-border/5 transition-colors focus-visible:ring-2 focus-visible:ring-offset-2 focus-visible:outline-none disabled:pointer-events-none disabled:opacity-50"
42
+ />
43
+ </SliderPrimitive.Track>
44
+ </SliderPrimitive.Control>
42
45
  </SliderPrimitive.Root>
43
46
  ),
44
47
  );
@@ -15,7 +15,10 @@ function Switch({
15
15
  data-slot="switch"
16
16
  data-size={size}
17
17
  className={cn(
18
- "peer data-checked:bg-primary data-unchecked:bg-input focus-visible:ring-ring inline-flex shrink-0 items-center rounded-full border border-transparent p-px focus-visible:ring-2 focus-visible:ring-offset-2 focus-visible:outline-none disabled:cursor-not-allowed disabled:opacity-50 data-checked:justify-end data-unchecked:justify-start",
18
+ "peer inline-flex shrink-0 items-center rounded-full border border-transparent p-px",
19
+ "data-checked:bg-primary data-unchecked:bg-input data-checked:justify-end data-unchecked:justify-start",
20
+ "focus-visible:ring-ring focus-visible:ring-2 focus-visible:ring-offset-2 focus-visible:outline-none",
21
+ "disabled:cursor-not-allowed disabled:opacity-50",
19
22
  size === "default" ? "h-[1.15rem] w-8" : "h-3.5 w-6",
20
23
  className,
21
24
  )}
@@ -25,7 +28,7 @@ function Switch({
25
28
  data-slot="switch-thumb"
26
29
  data-size={size}
27
30
  className={cn(
28
- "pointer-events-none block rounded-full bg-white shadow-sm ring-1 ring-black/5 transition-all",
31
+ "pointer-events-none block rounded-full bg-background shadow-sm ring-1 ring-border/5 transition-all",
29
32
  size === "default" ? "size-4" : "size-3",
30
33
  )}
31
34
  />
@@ -7,7 +7,11 @@ function Textarea({ className, ...props }: React.ComponentProps<"textarea">) {
7
7
  <textarea
8
8
  data-slot="textarea"
9
9
  className={cn(
10
- "placeholder:text-muted-foreground aria-invalid:ring-destructive/20 aria-invalid:border-destructive focus-visible:ring-ring border-input flex field-sizing-content min-h-16 w-full rounded-md border bg-transparent px-3 py-2 text-base transition-colors focus-visible:ring-2 focus-visible:ring-offset-2 focus-visible:outline-none disabled:cursor-not-allowed disabled:opacity-50 md:text-sm",
10
+ "border-input flex field-sizing-content min-h-16 w-full rounded-md border bg-transparent px-3 py-2 text-base transition-colors md:text-sm",
11
+ "placeholder:text-muted-foreground",
12
+ "focus-visible:ring-ring focus-visible:ring-2 focus-visible:ring-offset-2 focus-visible:outline-none",
13
+ "disabled:cursor-not-allowed disabled:opacity-50",
14
+ "aria-invalid:ring-destructive/20 aria-invalid:border-destructive",
11
15
  className,
12
16
  )}
13
17
  {...props}
@@ -1,7 +1,5 @@
1
1
  export { ScrollArea } from "./scroll-area";
2
2
  export type { ScrollAreaProps } from "./scroll-area";
3
- export { SearchFilterBar } from "../filters/search-filter-bar";
4
- export type { SearchFilterBarProps } from "../filters/search-filter-bar";
5
3
  export { SectionIntro } from "./section-intro";
6
4
  export type { SectionIntroProps } from "./section-intro";
7
5
  export { SurfacePanel } from "./surface-panel";
@@ -21,7 +21,7 @@ export function SectionIntro({ eyebrow, title, description, actions }: SectionIn
21
21
  </div>
22
22
  ) : null}
23
23
  <div className="space-y-3">
24
- <h1 className="text-title-2 text-foreground font-semibold tracking-tight">{title}</h1>
24
+ <h1 className="text-2xl text-foreground font-semibold tracking-tight">{title}</h1>
25
25
  {description ? <p className="text-muted-foreground text-sm">{description}</p> : null}
26
26
  </div>
27
27
  </div>
@@ -1,15 +1,23 @@
1
1
  import type { ComponentPropsWithoutRef, ElementType } from "react";
2
2
 
3
+ import { cn } from "../../lib/utils";
4
+
3
5
  type SurfacePanelProps<T extends ElementType = "div"> = { as?: T } & Omit<
4
6
  ComponentPropsWithoutRef<T>,
5
- "as" | "className"
7
+ "as"
6
8
  >;
7
9
 
8
10
  /** Rounded, bordered surface panel — a generic content container. */
9
11
  export function SurfacePanel<T extends ElementType = "div">({
10
12
  as,
13
+ className,
11
14
  ...props
12
15
  }: SurfacePanelProps<T>) {
13
16
  const Component = as ?? "div";
14
- return <Component className="border-border bg-surface rounded-3xl border p-5" {...props} />;
17
+ return (
18
+ <Component
19
+ className={cn("border-border bg-card rounded-3xl border p-5", className)}
20
+ {...props}
21
+ />
22
+ );
15
23
  }
@@ -25,7 +25,10 @@ function TabsTrigger({ className, ...props }: React.ComponentProps<typeof TabsPr
25
25
  <TabsPrimitive.Tab
26
26
  data-slot="tabs-trigger"
27
27
  className={cn(
28
- "text-foreground focus-visible:ring-ring data-active:bg-background data-active:text-foreground data-active:border-border flex flex-1 items-center justify-center gap-2 rounded-full px-3 py-2 text-sm font-medium whitespace-nowrap transition-all focus-visible:ring-2 focus-visible:outline-none disabled:pointer-events-none disabled:opacity-50 data-active:border",
28
+ "text-foreground flex flex-1 items-center justify-center gap-2 rounded-full px-3 py-2 text-sm font-medium whitespace-nowrap transition-all",
29
+ "disabled:pointer-events-none disabled:opacity-50",
30
+ "focus-visible:ring-ring focus-visible:ring-2 focus-visible:outline-none",
31
+ "data-active:ring-1 data-active:ring-inset data-active:bg-background data-active:text-foreground data-active:ring-border",
29
32
  className,
30
33
  )}
31
34
  {...props}