@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.
- package/package.json +1 -1
- package/src/components/data-display/accordion.tsx +10 -1
- package/src/components/feedback/empty-state.tsx +1 -1
- package/src/components/feedback/file-upload-status.tsx +14 -6
- package/src/components/feedback/progress-bar.tsx +3 -2
- package/src/components/feedback/spinner.tsx +3 -4
- package/src/components/feedback/update-guard.stories.tsx +1 -1
- package/src/components/feedback/update-guard.tsx +2 -2
- package/src/components/filters/active-filters-bar.stories.tsx +2 -0
- package/src/components/filters/active-filters-bar.tsx +21 -23
- package/src/components/filters/filter-select.tsx +4 -2
- package/src/components/filters/index.ts +0 -4
- package/src/components/forms/date-month-select.tsx +2 -2
- package/src/components/forms/date-picker.tsx +2 -2
- package/src/components/forms/drop-zone.stories.tsx +112 -28
- package/src/components/forms/drop-zone.tsx +265 -112
- package/src/components/forms/input.tsx +5 -1
- package/src/components/forms/radio-group.tsx +3 -2
- package/src/components/forms/select.tsx +14 -4
- package/src/components/forms/slider.stories.tsx +2 -2
- package/src/components/forms/slider.tsx +12 -9
- package/src/components/forms/switch.tsx +5 -2
- package/src/components/forms/textarea.tsx +5 -1
- package/src/components/layout/index.ts +0 -2
- package/src/components/layout/section-intro.tsx +1 -1
- package/src/components/layout/surface-panel.tsx +10 -2
- package/src/components/navigation/tabs.tsx +4 -1
- package/src/components/overlays/command.tsx +39 -14
- package/src/components/overlays/popover.stories.tsx +16 -4
- package/src/components/primitives/badge.tsx +1 -1
- package/src/components/primitives/button.tsx +6 -1
- package/src/components/filters/filters.stories.tsx +0 -72
- package/src/components/filters/filters.tsx +0 -72
- package/src/components/filters/search-filter-bar.stories.tsx +0 -39
- package/src/components/filters/search-filter-bar.tsx +0 -46
|
@@ -1,146 +1,299 @@
|
|
|
1
|
-
import
|
|
2
|
-
|
|
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
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
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
|
-
|
|
17
|
-
|
|
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
|
-
/**
|
|
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
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
onDragLeave,
|
|
27
|
-
onChange,
|
|
66
|
+
status,
|
|
67
|
+
file = null,
|
|
68
|
+
error = null,
|
|
69
|
+
notice = null,
|
|
28
70
|
accept,
|
|
29
|
-
multiple =
|
|
71
|
+
multiple = false,
|
|
72
|
+
disabled = false,
|
|
30
73
|
className,
|
|
31
|
-
|
|
32
|
-
|
|
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
|
|
37
|
-
|
|
38
|
-
e.preventDefault();
|
|
39
|
-
e.stopPropagation();
|
|
40
|
-
onDragLeave();
|
|
99
|
+
const isBusy = status === "busy";
|
|
100
|
+
const isInteractive = !disabled && !isBusy;
|
|
41
101
|
|
|
42
|
-
|
|
43
|
-
|
|
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
|
-
[
|
|
114
|
+
[isInteractive, onFiles],
|
|
47
115
|
);
|
|
48
116
|
|
|
49
|
-
const
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
117
|
+
const onDragEnter = (event: DragEvent) => {
|
|
118
|
+
event.preventDefault();
|
|
119
|
+
event.stopPropagation();
|
|
120
|
+
if (!isInteractive) return;
|
|
121
|
+
setDragging(true);
|
|
122
|
+
};
|
|
54
123
|
|
|
55
|
-
const
|
|
56
|
-
(
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
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
|
|
65
|
-
(
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
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
|
|
74
|
-
(
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
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
|
|
82
|
-
(
|
|
83
|
-
|
|
84
|
-
|
|
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
|
|
93
|
-
(
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
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
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
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
|
-
"
|
|
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
|
|
27
|
-
"
|
|
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
|
-
"
|
|
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 = "
|
|
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
|
|
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-
|
|
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-
|
|
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-
|
|
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.
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
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
|
|
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-
|
|
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
|
-
"
|
|
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-
|
|
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"
|
|
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
|
|
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
|
|
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}
|