@rynx-ai/ui 0.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.
- package/README.md +37 -0
- package/dist/components/alert-dialog.d.ts +16 -0
- package/dist/components/alert-dialog.js +34 -0
- package/dist/components/button.d.ts +11 -0
- package/dist/components/button.js +34 -0
- package/dist/components/dialog.d.ts +16 -0
- package/dist/components/dialog.js +30 -0
- package/dist/components/dropdown-menu.d.ts +8 -0
- package/dist/components/dropdown-menu.js +14 -0
- package/dist/components/input.d.ts +2 -0
- package/dist/components/input.js +5 -0
- package/dist/components/interaction-question.d.ts +42 -0
- package/dist/components/interaction-question.js +79 -0
- package/dist/components/native-select.d.ts +7 -0
- package/dist/components/native-select.js +12 -0
- package/dist/components/scroll-area.d.ts +9 -0
- package/dist/components/scroll-area.js +11 -0
- package/dist/components/select.d.ts +14 -0
- package/dist/components/select.js +28 -0
- package/dist/components/tabs.d.ts +6 -0
- package/dist/components/tabs.js +13 -0
- package/dist/components/textarea.d.ts +2 -0
- package/dist/components/textarea.js +5 -0
- package/dist/index.d.ts +12 -0
- package/dist/index.js +12 -0
- package/dist/lib/utils.d.ts +2 -0
- package/dist/lib/utils.js +5 -0
- package/package.json +68 -0
- package/src/components/alert-dialog.tsx +162 -0
- package/src/components/button.tsx +63 -0
- package/src/components/dialog.tsx +137 -0
- package/src/components/dropdown-menu.tsx +57 -0
- package/src/components/input.tsx +19 -0
- package/src/components/interaction-question.tsx +364 -0
- package/src/components/native-select.tsx +57 -0
- package/src/components/scroll-area.tsx +84 -0
- package/src/components/select.tsx +191 -0
- package/src/components/tabs.tsx +53 -0
- package/src/components/textarea.tsx +16 -0
- package/src/index.ts +12 -0
- package/src/lib/utils.ts +6 -0
- package/styles/tailwind.css +31 -0
- package/styles/tokens.css +55 -0
- package/styles/utilities.css +55 -0
- package/styles.css +4 -0
|
@@ -0,0 +1,364 @@
|
|
|
1
|
+
import { Check, ChevronLeft, ChevronRight } from "lucide-react";
|
|
2
|
+
import * as React from "react";
|
|
3
|
+
|
|
4
|
+
import { Button } from "./button.js";
|
|
5
|
+
import { Input } from "./input.js";
|
|
6
|
+
import { Textarea } from "./textarea.js";
|
|
7
|
+
import { cn } from "../lib/utils.js";
|
|
8
|
+
|
|
9
|
+
export interface InteractionQuestionOption {
|
|
10
|
+
value: string;
|
|
11
|
+
label: string;
|
|
12
|
+
description?: string;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export type InteractionQuestionField =
|
|
16
|
+
| {
|
|
17
|
+
id: string;
|
|
18
|
+
type: "text";
|
|
19
|
+
label: string;
|
|
20
|
+
description?: string;
|
|
21
|
+
required?: boolean;
|
|
22
|
+
secret?: boolean;
|
|
23
|
+
multiline?: boolean;
|
|
24
|
+
placeholder?: string;
|
|
25
|
+
}
|
|
26
|
+
| {
|
|
27
|
+
id: string;
|
|
28
|
+
type: "select";
|
|
29
|
+
label: string;
|
|
30
|
+
description?: string;
|
|
31
|
+
required?: boolean;
|
|
32
|
+
multiple?: boolean;
|
|
33
|
+
allowOther?: boolean;
|
|
34
|
+
options: InteractionQuestionOption[];
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
export type InteractionQuestionAnswer = string | string[];
|
|
38
|
+
|
|
39
|
+
export interface InteractionQuestionProps {
|
|
40
|
+
idPrefix: string;
|
|
41
|
+
fields: readonly InteractionQuestionField[];
|
|
42
|
+
activeFieldId?: string;
|
|
43
|
+
answers: Record<string, InteractionQuestionAnswer>;
|
|
44
|
+
otherValues: Record<string, string>;
|
|
45
|
+
otherSelections: Record<string, boolean>;
|
|
46
|
+
errors?: Record<string, string | undefined>;
|
|
47
|
+
disabled?: boolean;
|
|
48
|
+
onNavigate: (fieldId: string) => void;
|
|
49
|
+
onAnswerChange: (fieldId: string, answer: InteractionQuestionAnswer) => void;
|
|
50
|
+
onOtherValueChange: (fieldId: string, value: string) => void;
|
|
51
|
+
onOtherSelectionChange: (fieldId: string, selected: boolean) => void;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/** Shared one-question-at-a-time editor used by every Rynx web surface. */
|
|
55
|
+
export function InteractionQuestion({
|
|
56
|
+
idPrefix,
|
|
57
|
+
fields,
|
|
58
|
+
activeFieldId,
|
|
59
|
+
answers,
|
|
60
|
+
otherValues,
|
|
61
|
+
otherSelections,
|
|
62
|
+
errors = {},
|
|
63
|
+
disabled = false,
|
|
64
|
+
onNavigate,
|
|
65
|
+
onAnswerChange,
|
|
66
|
+
onOtherValueChange,
|
|
67
|
+
onOtherSelectionChange,
|
|
68
|
+
}: InteractionQuestionProps) {
|
|
69
|
+
const questionRef = React.useRef<HTMLDivElement>(null);
|
|
70
|
+
const previousFieldIdRef = React.useRef<string | undefined>(undefined);
|
|
71
|
+
const previousErrorRef = React.useRef<string | undefined>(undefined);
|
|
72
|
+
const requestedIndex = fields.findIndex((candidate) => candidate.id === activeFieldId);
|
|
73
|
+
const index = requestedIndex >= 0 ? requestedIndex : 0;
|
|
74
|
+
const field = fields[index];
|
|
75
|
+
const error = field ? errors[field.id] : undefined;
|
|
76
|
+
const currentAnswer = field ? answers[field.id] : undefined;
|
|
77
|
+
|
|
78
|
+
React.useEffect(() => {
|
|
79
|
+
if (!field) return;
|
|
80
|
+
const fieldChanged = previousFieldIdRef.current !== undefined
|
|
81
|
+
&& previousFieldIdRef.current !== field.id;
|
|
82
|
+
const errorAppeared = Boolean(error) && previousErrorRef.current !== error;
|
|
83
|
+
previousFieldIdRef.current = field.id;
|
|
84
|
+
previousErrorRef.current = error;
|
|
85
|
+
if (!fieldChanged && !errorAppeared) return;
|
|
86
|
+
const container = questionRef.current;
|
|
87
|
+
const focusTarget = container?.querySelector<HTMLElement>(
|
|
88
|
+
'input[aria-invalid="true"]:not([disabled]), textarea[aria-invalid="true"]:not([disabled])',
|
|
89
|
+
) ?? container?.querySelector<HTMLElement>("input:checked:not([disabled])")
|
|
90
|
+
?? container?.querySelector<HTMLElement>("input:not([disabled]), textarea:not([disabled])");
|
|
91
|
+
focusTarget?.focus();
|
|
92
|
+
}, [error, field]);
|
|
93
|
+
|
|
94
|
+
if (!field) return null;
|
|
95
|
+
|
|
96
|
+
const domId = `${idPrefix}-field-${index}`;
|
|
97
|
+
const descriptionId = field.description ? `${domId}-description` : undefined;
|
|
98
|
+
const errorId = error ? `${domId}-error` : undefined;
|
|
99
|
+
const describedBy = [descriptionId, errorId].filter(Boolean).join(" ") || undefined;
|
|
100
|
+
|
|
101
|
+
return (
|
|
102
|
+
<div className="grid gap-3">
|
|
103
|
+
{fields.length > 1 && (
|
|
104
|
+
<div className="grid gap-1.5">
|
|
105
|
+
<div className="flex items-center justify-between gap-3">
|
|
106
|
+
<div aria-live="polite" className="font-mono text-[10px] uppercase tracking-[0.14em] text-muted-foreground">
|
|
107
|
+
Question {index + 1} of {fields.length}
|
|
108
|
+
</div>
|
|
109
|
+
<div className="text-[11px] text-muted-foreground">
|
|
110
|
+
{field.required ? "Required" : "Optional"}
|
|
111
|
+
</div>
|
|
112
|
+
</div>
|
|
113
|
+
<div
|
|
114
|
+
role="progressbar"
|
|
115
|
+
aria-label="Question progress"
|
|
116
|
+
aria-valuemin={1}
|
|
117
|
+
aria-valuemax={fields.length}
|
|
118
|
+
aria-valuenow={index + 1}
|
|
119
|
+
className="h-1 overflow-hidden rounded-full bg-muted"
|
|
120
|
+
>
|
|
121
|
+
<div
|
|
122
|
+
className="h-full rounded-full bg-primary transition-[width]"
|
|
123
|
+
style={{ width: `${((index + 1) / fields.length) * 100}%` }}
|
|
124
|
+
/>
|
|
125
|
+
</div>
|
|
126
|
+
</div>
|
|
127
|
+
)}
|
|
128
|
+
|
|
129
|
+
<div ref={questionRef}>
|
|
130
|
+
{field.type === "text" ? (
|
|
131
|
+
<div className="grid gap-1.5">
|
|
132
|
+
<label htmlFor={domId} className="text-xs font-medium text-foreground">
|
|
133
|
+
{field.label}
|
|
134
|
+
{field.required && <RequiredIndicator />}
|
|
135
|
+
</label>
|
|
136
|
+
{field.description && (
|
|
137
|
+
<p id={descriptionId} className="text-[11px] leading-relaxed text-muted-foreground">
|
|
138
|
+
{field.description}
|
|
139
|
+
</p>
|
|
140
|
+
)}
|
|
141
|
+
{field.multiline && !field.secret ? (
|
|
142
|
+
<Textarea
|
|
143
|
+
id={domId}
|
|
144
|
+
value={typeof currentAnswer === "string" ? currentAnswer : ""}
|
|
145
|
+
disabled={disabled}
|
|
146
|
+
placeholder={field.placeholder}
|
|
147
|
+
aria-invalid={error ? true : undefined}
|
|
148
|
+
aria-describedby={describedBy}
|
|
149
|
+
required={field.required}
|
|
150
|
+
onChange={(event) => onAnswerChange(field.id, event.target.value)}
|
|
151
|
+
/>
|
|
152
|
+
) : (
|
|
153
|
+
<Input
|
|
154
|
+
id={domId}
|
|
155
|
+
type={field.secret ? "password" : "text"}
|
|
156
|
+
autoComplete="off"
|
|
157
|
+
value={typeof currentAnswer === "string" ? currentAnswer : ""}
|
|
158
|
+
disabled={disabled}
|
|
159
|
+
placeholder={field.placeholder}
|
|
160
|
+
aria-invalid={error ? true : undefined}
|
|
161
|
+
aria-describedby={describedBy}
|
|
162
|
+
required={field.required}
|
|
163
|
+
onChange={(event) => onAnswerChange(field.id, event.target.value)}
|
|
164
|
+
/>
|
|
165
|
+
)}
|
|
166
|
+
<FieldError id={errorId} error={error} />
|
|
167
|
+
</div>
|
|
168
|
+
) : (
|
|
169
|
+
<fieldset
|
|
170
|
+
disabled={disabled}
|
|
171
|
+
role={field.multiple ? undefined : "radiogroup"}
|
|
172
|
+
aria-required={!field.multiple && field.required ? true : undefined}
|
|
173
|
+
aria-invalid={error ? true : undefined}
|
|
174
|
+
aria-describedby={describedBy}
|
|
175
|
+
className="grid gap-1.5"
|
|
176
|
+
>
|
|
177
|
+
<legend className="text-xs font-medium text-foreground">
|
|
178
|
+
{field.label}
|
|
179
|
+
{field.required && <RequiredIndicator />}
|
|
180
|
+
</legend>
|
|
181
|
+
{field.description && (
|
|
182
|
+
<p id={descriptionId} className="text-[11px] leading-relaxed text-muted-foreground">
|
|
183
|
+
{field.description}
|
|
184
|
+
</p>
|
|
185
|
+
)}
|
|
186
|
+
<div className="grid gap-1.5">
|
|
187
|
+
{field.options.map((option, optionIndex) => {
|
|
188
|
+
const optionId = `${domId}-option-${optionIndex}`;
|
|
189
|
+
const checked = field.multiple
|
|
190
|
+
? Array.isArray(currentAnswer) && currentAnswer.includes(option.value)
|
|
191
|
+
: otherSelections[field.id] !== true && currentAnswer === option.value;
|
|
192
|
+
return (
|
|
193
|
+
<ChoiceRow
|
|
194
|
+
key={option.value}
|
|
195
|
+
id={optionId}
|
|
196
|
+
name={field.multiple ? undefined : domId}
|
|
197
|
+
type={field.multiple ? "checkbox" : "radio"}
|
|
198
|
+
checked={checked}
|
|
199
|
+
label={option.label}
|
|
200
|
+
description={option.description}
|
|
201
|
+
onChange={(selected) => {
|
|
202
|
+
if (field.multiple) {
|
|
203
|
+
const values = Array.isArray(currentAnswer) ? currentAnswer : [];
|
|
204
|
+
onAnswerChange(
|
|
205
|
+
field.id,
|
|
206
|
+
selected
|
|
207
|
+
? [...values.filter((value) => value !== option.value), option.value]
|
|
208
|
+
: values.filter((value) => value !== option.value),
|
|
209
|
+
);
|
|
210
|
+
} else {
|
|
211
|
+
onAnswerChange(field.id, option.value);
|
|
212
|
+
onOtherSelectionChange(field.id, false);
|
|
213
|
+
}
|
|
214
|
+
}}
|
|
215
|
+
/>
|
|
216
|
+
);
|
|
217
|
+
})}
|
|
218
|
+
{field.allowOther && (
|
|
219
|
+
<div className="grid gap-1.5 rounded-lg border border-border bg-background/60 px-2.5 py-2 has-[:checked]:border-primary/50 has-[:checked]:bg-primary/5">
|
|
220
|
+
<ChoiceRow
|
|
221
|
+
id={`${domId}-other`}
|
|
222
|
+
name={field.multiple ? undefined : domId}
|
|
223
|
+
type={field.multiple ? "checkbox" : "radio"}
|
|
224
|
+
checked={otherSelections[field.id] === true}
|
|
225
|
+
label="Other"
|
|
226
|
+
embedded
|
|
227
|
+
onChange={(selected) => {
|
|
228
|
+
onOtherSelectionChange(field.id, selected);
|
|
229
|
+
if (selected && !field.multiple) onAnswerChange(field.id, "");
|
|
230
|
+
}}
|
|
231
|
+
/>
|
|
232
|
+
<Input
|
|
233
|
+
value={otherValues[field.id] ?? ""}
|
|
234
|
+
disabled={disabled || otherSelections[field.id] !== true}
|
|
235
|
+
aria-label={`Other answer for ${field.label}`}
|
|
236
|
+
aria-invalid={error ? true : undefined}
|
|
237
|
+
aria-describedby={describedBy}
|
|
238
|
+
placeholder="Enter another answer"
|
|
239
|
+
onChange={(event) => onOtherValueChange(field.id, event.target.value)}
|
|
240
|
+
/>
|
|
241
|
+
</div>
|
|
242
|
+
)}
|
|
243
|
+
{!field.required
|
|
244
|
+
&& !field.multiple
|
|
245
|
+
&& (otherSelections[field.id] === true
|
|
246
|
+
|| (typeof currentAnswer === "string" && currentAnswer.length > 0)) && (
|
|
247
|
+
<Button
|
|
248
|
+
type="button"
|
|
249
|
+
size="xs"
|
|
250
|
+
variant="ghost"
|
|
251
|
+
className="justify-self-start text-muted-foreground"
|
|
252
|
+
onClick={() => {
|
|
253
|
+
onAnswerChange(field.id, "");
|
|
254
|
+
onOtherSelectionChange(field.id, false);
|
|
255
|
+
}}
|
|
256
|
+
>
|
|
257
|
+
Clear selection
|
|
258
|
+
</Button>
|
|
259
|
+
)}
|
|
260
|
+
</div>
|
|
261
|
+
<FieldError id={errorId} error={error} />
|
|
262
|
+
</fieldset>
|
|
263
|
+
)}
|
|
264
|
+
</div>
|
|
265
|
+
|
|
266
|
+
{fields.length > 1 && (
|
|
267
|
+
<div className="flex items-center justify-between gap-2 border-t border-border/70 pt-2">
|
|
268
|
+
<Button
|
|
269
|
+
type="button"
|
|
270
|
+
size="xs"
|
|
271
|
+
variant="ghost"
|
|
272
|
+
disabled={disabled || index === 0}
|
|
273
|
+
onClick={() => onNavigate(fields[index - 1]!.id)}
|
|
274
|
+
>
|
|
275
|
+
<ChevronLeft />
|
|
276
|
+
Previous
|
|
277
|
+
</Button>
|
|
278
|
+
<Button
|
|
279
|
+
type="button"
|
|
280
|
+
size="xs"
|
|
281
|
+
variant="outline"
|
|
282
|
+
disabled={disabled || index === fields.length - 1}
|
|
283
|
+
onClick={() => onNavigate(fields[index + 1]!.id)}
|
|
284
|
+
>
|
|
285
|
+
Next
|
|
286
|
+
<ChevronRight />
|
|
287
|
+
</Button>
|
|
288
|
+
</div>
|
|
289
|
+
)}
|
|
290
|
+
</div>
|
|
291
|
+
);
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
function ChoiceRow({
|
|
295
|
+
id,
|
|
296
|
+
name,
|
|
297
|
+
type,
|
|
298
|
+
checked,
|
|
299
|
+
label,
|
|
300
|
+
description,
|
|
301
|
+
embedded = false,
|
|
302
|
+
onChange,
|
|
303
|
+
}: {
|
|
304
|
+
id: string;
|
|
305
|
+
name?: string;
|
|
306
|
+
type: "checkbox" | "radio";
|
|
307
|
+
checked: boolean;
|
|
308
|
+
label: string;
|
|
309
|
+
description?: string;
|
|
310
|
+
embedded?: boolean;
|
|
311
|
+
onChange: (checked: boolean) => void;
|
|
312
|
+
}) {
|
|
313
|
+
return (
|
|
314
|
+
<label
|
|
315
|
+
htmlFor={id}
|
|
316
|
+
className={cn(
|
|
317
|
+
"flex cursor-pointer items-start gap-2 text-xs text-foreground",
|
|
318
|
+
!embedded
|
|
319
|
+
&& "rounded-lg border border-border bg-background/60 px-2.5 py-2 transition-colors hover:bg-muted/60 has-[:checked]:border-primary/50 has-[:checked]:bg-primary/5",
|
|
320
|
+
)}
|
|
321
|
+
>
|
|
322
|
+
<span className="relative mt-0.5 size-4 shrink-0">
|
|
323
|
+
<input
|
|
324
|
+
id={id}
|
|
325
|
+
type={type}
|
|
326
|
+
name={name}
|
|
327
|
+
checked={checked}
|
|
328
|
+
className="peer size-4 appearance-none rounded-sm border border-input bg-background outline-none transition-colors checked:border-primary checked:bg-primary focus-visible:ring-3 focus-visible:ring-ring/50 disabled:cursor-not-allowed disabled:opacity-60"
|
|
329
|
+
onChange={(event) => onChange(event.target.checked)}
|
|
330
|
+
/>
|
|
331
|
+
<Check
|
|
332
|
+
aria-hidden="true"
|
|
333
|
+
className="pointer-events-none absolute inset-0 size-4 stroke-[3] text-primary-foreground opacity-0 peer-checked:opacity-100"
|
|
334
|
+
/>
|
|
335
|
+
</span>
|
|
336
|
+
<span className="min-w-0">
|
|
337
|
+
<span className="block">{label}</span>
|
|
338
|
+
{description && (
|
|
339
|
+
<span className="mt-0.5 block text-[11px] leading-relaxed text-muted-foreground">
|
|
340
|
+
{description}
|
|
341
|
+
</span>
|
|
342
|
+
)}
|
|
343
|
+
</span>
|
|
344
|
+
</label>
|
|
345
|
+
);
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
function FieldError({ id, error }: { id?: string; error?: string }) {
|
|
349
|
+
if (!error) return null;
|
|
350
|
+
return (
|
|
351
|
+
<p id={id} role="alert" className="text-[11px] text-destructive">
|
|
352
|
+
{error}
|
|
353
|
+
</p>
|
|
354
|
+
);
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
function RequiredIndicator() {
|
|
358
|
+
return (
|
|
359
|
+
<>
|
|
360
|
+
<span aria-hidden="true" className="ml-0.5 text-destructive">*</span>
|
|
361
|
+
<span className="sr-only"> (required)</span>
|
|
362
|
+
</>
|
|
363
|
+
);
|
|
364
|
+
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { ChevronDown } from "lucide-react";
|
|
2
|
+
import * as React from "react";
|
|
3
|
+
|
|
4
|
+
import { cn } from "../lib/utils.js";
|
|
5
|
+
|
|
6
|
+
export type NativeSelectProps = Omit<React.ComponentProps<"select">, "size"> & {
|
|
7
|
+
size?: "sm" | "default";
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
export function NativeSelect({
|
|
11
|
+
className,
|
|
12
|
+
size = "default",
|
|
13
|
+
...props
|
|
14
|
+
}: NativeSelectProps) {
|
|
15
|
+
return (
|
|
16
|
+
<div
|
|
17
|
+
data-slot="native-select-wrapper"
|
|
18
|
+
data-size={size}
|
|
19
|
+
className={cn(
|
|
20
|
+
"group/native-select relative w-fit has-[select:disabled]:opacity-50",
|
|
21
|
+
className,
|
|
22
|
+
)}
|
|
23
|
+
>
|
|
24
|
+
<select
|
|
25
|
+
data-slot="native-select"
|
|
26
|
+
data-size={size}
|
|
27
|
+
className="h-8 w-full min-w-0 appearance-none rounded-lg border border-input bg-transparent py-1 pr-8 pl-2.5 text-sm transition-colors outline-none select-none selection:bg-primary selection:text-primary-foreground placeholder:text-muted-foreground focus-visible:border-ring focus-visible:ring-3 focus-visible:ring-ring/50 disabled:pointer-events-none disabled:cursor-not-allowed aria-invalid:border-destructive aria-invalid:ring-3 aria-invalid:ring-destructive/20 data-[size=sm]:h-7 data-[size=sm]:rounded-[min(var(--radius-md),10px)] data-[size=sm]:py-0.5 dark:bg-input/30 dark:hover:bg-input/50 dark:aria-invalid:border-destructive/50 dark:aria-invalid:ring-destructive/40"
|
|
28
|
+
{...props}
|
|
29
|
+
/>
|
|
30
|
+
<ChevronDown
|
|
31
|
+
aria-hidden="true"
|
|
32
|
+
data-slot="native-select-icon"
|
|
33
|
+
className="pointer-events-none absolute top-1/2 right-2.5 size-4 -translate-y-1/2 text-muted-foreground select-none"
|
|
34
|
+
/>
|
|
35
|
+
</div>
|
|
36
|
+
);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export function NativeSelectOption({ className, ...props }: React.ComponentProps<"option">) {
|
|
40
|
+
return (
|
|
41
|
+
<option
|
|
42
|
+
data-slot="native-select-option"
|
|
43
|
+
className={cn("bg-[Canvas] text-[CanvasText]", className)}
|
|
44
|
+
{...props}
|
|
45
|
+
/>
|
|
46
|
+
);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export function NativeSelectOptGroup({ className, ...props }: React.ComponentProps<"optgroup">) {
|
|
50
|
+
return (
|
|
51
|
+
<optgroup
|
|
52
|
+
data-slot="native-select-optgroup"
|
|
53
|
+
className={cn("bg-[Canvas] text-[CanvasText]", className)}
|
|
54
|
+
{...props}
|
|
55
|
+
/>
|
|
56
|
+
);
|
|
57
|
+
}
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import * as ScrollAreaPrimitive from "@radix-ui/react-scroll-area";
|
|
2
|
+
import * as React from "react";
|
|
3
|
+
|
|
4
|
+
import { cn } from "../lib/utils.js";
|
|
5
|
+
|
|
6
|
+
type ScrollAreaProps = React.ComponentPropsWithoutRef<typeof ScrollAreaPrimitive.Root> & {
|
|
7
|
+
viewportClassName?: string;
|
|
8
|
+
viewportRef?: React.Ref<React.ElementRef<typeof ScrollAreaPrimitive.Viewport>>;
|
|
9
|
+
viewportProps?: Omit<
|
|
10
|
+
React.ComponentPropsWithoutRef<typeof ScrollAreaPrimitive.Viewport>,
|
|
11
|
+
"children" | "className"
|
|
12
|
+
>;
|
|
13
|
+
scrollbars?: "vertical" | "horizontal" | "both";
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
export const ScrollArea = React.forwardRef<
|
|
17
|
+
React.ElementRef<typeof ScrollAreaPrimitive.Root>,
|
|
18
|
+
ScrollAreaProps
|
|
19
|
+
>(({
|
|
20
|
+
className,
|
|
21
|
+
children,
|
|
22
|
+
viewportClassName,
|
|
23
|
+
viewportRef,
|
|
24
|
+
viewportProps,
|
|
25
|
+
scrollbars = "vertical",
|
|
26
|
+
type = "auto",
|
|
27
|
+
...props
|
|
28
|
+
}, ref) => (
|
|
29
|
+
<ScrollAreaPrimitive.Root
|
|
30
|
+
ref={ref}
|
|
31
|
+
data-slot="scroll-area"
|
|
32
|
+
type={type}
|
|
33
|
+
className={cn("relative overflow-hidden", className)}
|
|
34
|
+
{...props}
|
|
35
|
+
>
|
|
36
|
+
<ScrollAreaPrimitive.Viewport
|
|
37
|
+
ref={viewportRef}
|
|
38
|
+
data-slot="scroll-area-viewport"
|
|
39
|
+
className={cn(
|
|
40
|
+
"size-full rounded-[inherit] outline-none transition-[color,box-shadow] focus-visible:ring-2 focus-visible:ring-[var(--ring)]/45",
|
|
41
|
+
scrollbars === "vertical" && "[&>div]:!block [&>div]:min-w-0 [&>div]:w-full",
|
|
42
|
+
viewportClassName,
|
|
43
|
+
)}
|
|
44
|
+
{...viewportProps}
|
|
45
|
+
>
|
|
46
|
+
{children}
|
|
47
|
+
</ScrollAreaPrimitive.Viewport>
|
|
48
|
+
{scrollbars !== "horizontal" && <ScrollBar orientation="vertical" />}
|
|
49
|
+
{scrollbars !== "vertical" && <ScrollBar orientation="horizontal" />}
|
|
50
|
+
<ScrollAreaPrimitive.Corner data-slot="scroll-area-corner" />
|
|
51
|
+
</ScrollAreaPrimitive.Root>
|
|
52
|
+
));
|
|
53
|
+
ScrollArea.displayName = ScrollAreaPrimitive.Root.displayName;
|
|
54
|
+
|
|
55
|
+
export const ScrollBar = React.forwardRef<
|
|
56
|
+
React.ElementRef<typeof ScrollAreaPrimitive.ScrollAreaScrollbar>,
|
|
57
|
+
React.ComponentPropsWithoutRef<typeof ScrollAreaPrimitive.ScrollAreaScrollbar>
|
|
58
|
+
>(({ className, orientation = "vertical", style, ...props }, ref) => (
|
|
59
|
+
<ScrollAreaPrimitive.ScrollAreaScrollbar
|
|
60
|
+
ref={ref}
|
|
61
|
+
data-slot="scroll-area-scrollbar"
|
|
62
|
+
orientation={orientation}
|
|
63
|
+
className={cn(
|
|
64
|
+
"group flex touch-none select-none p-px transition-colors",
|
|
65
|
+
orientation === "vertical" && "h-full w-2",
|
|
66
|
+
orientation === "horizontal" && "h-2 flex-col",
|
|
67
|
+
className,
|
|
68
|
+
)}
|
|
69
|
+
style={{
|
|
70
|
+
...(orientation === "vertical" ? { right: 2 } : { bottom: 2 }),
|
|
71
|
+
...style,
|
|
72
|
+
}}
|
|
73
|
+
{...props}
|
|
74
|
+
>
|
|
75
|
+
<ScrollAreaPrimitive.ScrollAreaThumb
|
|
76
|
+
data-slot="scroll-area-thumb"
|
|
77
|
+
className={cn(
|
|
78
|
+
"relative flex-1 rounded-full bg-[var(--scrollbar-thumb)] transition-colors group-hover:bg-[var(--scrollbar-thumb-hover)] active:bg-[var(--scrollbar-thumb-active)]",
|
|
79
|
+
orientation === "vertical" ? "min-h-8 w-full" : "h-full min-w-8",
|
|
80
|
+
)}
|
|
81
|
+
/>
|
|
82
|
+
</ScrollAreaPrimitive.ScrollAreaScrollbar>
|
|
83
|
+
));
|
|
84
|
+
ScrollBar.displayName = ScrollAreaPrimitive.ScrollAreaScrollbar.displayName;
|
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
import * as SelectPrimitive from "@radix-ui/react-select";
|
|
2
|
+
import { Check, ChevronDown, ChevronUp } from "lucide-react";
|
|
3
|
+
import * as React from "react";
|
|
4
|
+
|
|
5
|
+
import { cn } from "../lib/utils.js";
|
|
6
|
+
|
|
7
|
+
export function Select(props: React.ComponentProps<typeof SelectPrimitive.Root>) {
|
|
8
|
+
return <SelectPrimitive.Root data-slot="select" {...props} />;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export const SelectGroup = React.forwardRef<
|
|
12
|
+
React.ElementRef<typeof SelectPrimitive.Group>,
|
|
13
|
+
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Group>
|
|
14
|
+
>(({ className, ...props }, ref) => (
|
|
15
|
+
<SelectPrimitive.Group
|
|
16
|
+
ref={ref}
|
|
17
|
+
data-slot="select-group"
|
|
18
|
+
className={cn("scroll-my-1 p-1", className)}
|
|
19
|
+
{...props}
|
|
20
|
+
/>
|
|
21
|
+
));
|
|
22
|
+
SelectGroup.displayName = SelectPrimitive.Group.displayName;
|
|
23
|
+
|
|
24
|
+
export const SelectValue = React.forwardRef<
|
|
25
|
+
React.ElementRef<typeof SelectPrimitive.Value>,
|
|
26
|
+
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Value>
|
|
27
|
+
>((props, ref) => <SelectPrimitive.Value ref={ref} data-slot="select-value" {...props} />);
|
|
28
|
+
SelectValue.displayName = SelectPrimitive.Value.displayName;
|
|
29
|
+
|
|
30
|
+
export const SelectTrigger = React.forwardRef<
|
|
31
|
+
React.ElementRef<typeof SelectPrimitive.Trigger>,
|
|
32
|
+
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Trigger> & {
|
|
33
|
+
size?: "sm" | "default";
|
|
34
|
+
}
|
|
35
|
+
>(({ className, size = "default", children, ...props }, ref) => (
|
|
36
|
+
<SelectPrimitive.Trigger
|
|
37
|
+
ref={ref}
|
|
38
|
+
data-slot="select-trigger"
|
|
39
|
+
data-size={size}
|
|
40
|
+
className={cn(
|
|
41
|
+
"flex w-full min-w-0 items-center justify-between gap-1.5 whitespace-nowrap rounded-lg border border-input bg-transparent py-2 pr-2 pl-2.5 text-sm transition-colors outline-none select-none focus-visible:border-ring focus-visible:ring-3 focus-visible:ring-ring/50 disabled:cursor-not-allowed disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-3 aria-invalid:ring-destructive/20 data-[placeholder]:text-muted-foreground data-[size=default]:h-8 data-[size=sm]:h-7 data-[size=sm]:rounded-[min(var(--radius-md),10px)] dark:bg-input/30 dark:hover:bg-input/50 dark:aria-invalid:border-destructive/50 dark:aria-invalid:ring-destructive/40 [&>span]:min-w-0 [&>span]:truncate [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
|
|
42
|
+
className,
|
|
43
|
+
)}
|
|
44
|
+
{...props}
|
|
45
|
+
>
|
|
46
|
+
{children}
|
|
47
|
+
<SelectPrimitive.Icon asChild>
|
|
48
|
+
<ChevronDown
|
|
49
|
+
aria-hidden="true"
|
|
50
|
+
className="pointer-events-none size-4 shrink-0 text-muted-foreground"
|
|
51
|
+
/>
|
|
52
|
+
</SelectPrimitive.Icon>
|
|
53
|
+
</SelectPrimitive.Trigger>
|
|
54
|
+
));
|
|
55
|
+
SelectTrigger.displayName = SelectPrimitive.Trigger.displayName;
|
|
56
|
+
|
|
57
|
+
export const SelectScrollUpButton = React.forwardRef<
|
|
58
|
+
React.ElementRef<typeof SelectPrimitive.ScrollUpButton>,
|
|
59
|
+
React.ComponentPropsWithoutRef<typeof SelectPrimitive.ScrollUpButton>
|
|
60
|
+
>(({ className, ...props }, ref) => (
|
|
61
|
+
<SelectPrimitive.ScrollUpButton
|
|
62
|
+
ref={ref}
|
|
63
|
+
data-slot="select-scroll-up-button"
|
|
64
|
+
className={cn(
|
|
65
|
+
"z-10 flex cursor-default items-center justify-center bg-popover py-1 [&_svg:not([class*='size-'])]:size-4",
|
|
66
|
+
className,
|
|
67
|
+
)}
|
|
68
|
+
{...props}
|
|
69
|
+
>
|
|
70
|
+
<ChevronUp aria-hidden="true" />
|
|
71
|
+
</SelectPrimitive.ScrollUpButton>
|
|
72
|
+
));
|
|
73
|
+
SelectScrollUpButton.displayName = SelectPrimitive.ScrollUpButton.displayName;
|
|
74
|
+
|
|
75
|
+
export const SelectScrollDownButton = React.forwardRef<
|
|
76
|
+
React.ElementRef<typeof SelectPrimitive.ScrollDownButton>,
|
|
77
|
+
React.ComponentPropsWithoutRef<typeof SelectPrimitive.ScrollDownButton>
|
|
78
|
+
>(({ className, ...props }, ref) => (
|
|
79
|
+
<SelectPrimitive.ScrollDownButton
|
|
80
|
+
ref={ref}
|
|
81
|
+
data-slot="select-scroll-down-button"
|
|
82
|
+
className={cn(
|
|
83
|
+
"z-10 flex cursor-default items-center justify-center bg-popover py-1 [&_svg:not([class*='size-'])]:size-4",
|
|
84
|
+
className,
|
|
85
|
+
)}
|
|
86
|
+
{...props}
|
|
87
|
+
>
|
|
88
|
+
<ChevronDown aria-hidden="true" />
|
|
89
|
+
</SelectPrimitive.ScrollDownButton>
|
|
90
|
+
));
|
|
91
|
+
SelectScrollDownButton.displayName = SelectPrimitive.ScrollDownButton.displayName;
|
|
92
|
+
|
|
93
|
+
export const SelectContent = React.forwardRef<
|
|
94
|
+
React.ElementRef<typeof SelectPrimitive.Content>,
|
|
95
|
+
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Content>
|
|
96
|
+
>(
|
|
97
|
+
(
|
|
98
|
+
{
|
|
99
|
+
className,
|
|
100
|
+
children,
|
|
101
|
+
position = "popper",
|
|
102
|
+
align = "center",
|
|
103
|
+
sideOffset = 4,
|
|
104
|
+
collisionPadding = 8,
|
|
105
|
+
...props
|
|
106
|
+
},
|
|
107
|
+
ref,
|
|
108
|
+
) => (
|
|
109
|
+
<SelectPrimitive.Portal>
|
|
110
|
+
<SelectPrimitive.Content
|
|
111
|
+
ref={ref}
|
|
112
|
+
data-slot="select-content"
|
|
113
|
+
data-align-trigger={position === "item-aligned"}
|
|
114
|
+
position={position}
|
|
115
|
+
align={align}
|
|
116
|
+
sideOffset={sideOffset}
|
|
117
|
+
collisionPadding={collisionPadding}
|
|
118
|
+
className={cn(
|
|
119
|
+
"cn-menu-translucent relative z-50 max-h-[var(--radix-select-content-available-height)] min-w-36 origin-[var(--radix-select-content-transform-origin)] overflow-x-hidden overflow-y-auto rounded-lg bg-popover text-popover-foreground shadow-md ring-1 ring-foreground/10 duration-100 data-[state=open]:animate-in data-[state=open]:fade-in-0 data-[state=open]:zoom-in-95 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
|
|
120
|
+
position === "popper" &&
|
|
121
|
+
"w-[var(--radix-select-trigger-width)] data-[side=bottom]:translate-y-1 data-[side=left]:-translate-x-1 data-[side=right]:translate-x-1 data-[side=top]:-translate-y-1",
|
|
122
|
+
className,
|
|
123
|
+
)}
|
|
124
|
+
{...props}
|
|
125
|
+
>
|
|
126
|
+
<SelectScrollUpButton />
|
|
127
|
+
<SelectPrimitive.Viewport
|
|
128
|
+
data-position={position}
|
|
129
|
+
className={cn(
|
|
130
|
+
"p-1",
|
|
131
|
+
position === "popper" &&
|
|
132
|
+
"min-w-[var(--radix-select-trigger-width)]",
|
|
133
|
+
)}
|
|
134
|
+
>
|
|
135
|
+
{children}
|
|
136
|
+
</SelectPrimitive.Viewport>
|
|
137
|
+
<SelectScrollDownButton />
|
|
138
|
+
</SelectPrimitive.Content>
|
|
139
|
+
</SelectPrimitive.Portal>
|
|
140
|
+
),
|
|
141
|
+
);
|
|
142
|
+
SelectContent.displayName = SelectPrimitive.Content.displayName;
|
|
143
|
+
|
|
144
|
+
export const SelectLabel = React.forwardRef<
|
|
145
|
+
React.ElementRef<typeof SelectPrimitive.Label>,
|
|
146
|
+
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Label>
|
|
147
|
+
>(({ className, ...props }, ref) => (
|
|
148
|
+
<SelectPrimitive.Label
|
|
149
|
+
ref={ref}
|
|
150
|
+
data-slot="select-label"
|
|
151
|
+
className={cn("px-1.5 py-1 text-xs text-muted-foreground", className)}
|
|
152
|
+
{...props}
|
|
153
|
+
/>
|
|
154
|
+
));
|
|
155
|
+
SelectLabel.displayName = SelectPrimitive.Label.displayName;
|
|
156
|
+
|
|
157
|
+
export const SelectItem = React.forwardRef<
|
|
158
|
+
React.ElementRef<typeof SelectPrimitive.Item>,
|
|
159
|
+
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Item>
|
|
160
|
+
>(({ className, children, ...props }, ref) => (
|
|
161
|
+
<SelectPrimitive.Item
|
|
162
|
+
ref={ref}
|
|
163
|
+
data-slot="select-item"
|
|
164
|
+
className={cn(
|
|
165
|
+
"relative flex w-full cursor-default items-center gap-1.5 rounded-md py-1 pr-8 pl-1.5 text-sm outline-hidden select-none focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
|
|
166
|
+
className,
|
|
167
|
+
)}
|
|
168
|
+
{...props}
|
|
169
|
+
>
|
|
170
|
+
<span className="pointer-events-none absolute right-2 flex size-4 items-center justify-center">
|
|
171
|
+
<SelectPrimitive.ItemIndicator>
|
|
172
|
+
<Check aria-hidden="true" className="pointer-events-none" />
|
|
173
|
+
</SelectPrimitive.ItemIndicator>
|
|
174
|
+
</span>
|
|
175
|
+
<SelectPrimitive.ItemText>{children}</SelectPrimitive.ItemText>
|
|
176
|
+
</SelectPrimitive.Item>
|
|
177
|
+
));
|
|
178
|
+
SelectItem.displayName = SelectPrimitive.Item.displayName;
|
|
179
|
+
|
|
180
|
+
export const SelectSeparator = React.forwardRef<
|
|
181
|
+
React.ElementRef<typeof SelectPrimitive.Separator>,
|
|
182
|
+
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Separator>
|
|
183
|
+
>(({ className, ...props }, ref) => (
|
|
184
|
+
<SelectPrimitive.Separator
|
|
185
|
+
ref={ref}
|
|
186
|
+
data-slot="select-separator"
|
|
187
|
+
className={cn("pointer-events-none -mx-1 my-1 h-px bg-border", className)}
|
|
188
|
+
{...props}
|
|
189
|
+
/>
|
|
190
|
+
));
|
|
191
|
+
SelectSeparator.displayName = SelectPrimitive.Separator.displayName;
|