@xenide-io/the-old-ui-theme 0.2.9 → 0.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/README.md +1 -1
- package/dist/{chunk-SHF57J7U.mjs → chunk-FDMD3IIN.mjs} +1573 -652
- package/dist/index-D1RTT2Ap.d.mts +1044 -0
- package/dist/index-D1RTT2Ap.d.ts +1044 -0
- package/dist/index.d.mts +10 -7
- package/dist/index.d.ts +10 -7
- package/dist/index.js +1705 -744
- package/dist/index.mjs +61 -1
- package/dist/tailwind-preset.js +7 -0
- package/dist/tailwind-preset.mjs +7 -0
- package/dist/ui.d.mts +3 -2
- package/dist/ui.d.ts +3 -2
- package/dist/ui.js +1584 -666
- package/dist/ui.mjs +31 -1
- package/package.json +18 -4
- package/src/app/globals.css +1005 -227
- package/src/components/icons/IconBase.tsx +20 -12
- package/src/components/icons/createIconBase.tsx +12 -7
- package/src/components/icons/icons.tsx +208 -18
- package/src/components/sections/AlertShowcase.tsx +2 -1
- package/src/components/sections/BadgeShowcase.tsx +1 -1
- package/src/components/sections/ButtonShowcase.tsx +6 -10
- package/src/components/sections/CardShowcase.tsx +22 -6
- package/src/components/sections/DropdownShowcase.tsx +4 -6
- package/src/components/sections/FilterChipsShowcase.tsx +74 -26
- package/src/components/sections/FormShowcase.tsx +2 -1
- package/src/components/sections/IconShowcase.tsx +86 -206
- package/src/components/sections/KbdShowcase.tsx +47 -21
- package/src/components/sections/ModalShowcase.tsx +7 -6
- package/src/components/sections/ProductLayoutsShowcase.tsx +138 -0
- package/src/components/sections/ProgressShowcase.tsx +50 -53
- package/src/components/sections/SwapShowcase.tsx +13 -5
- package/src/components/ui/Alert.tsx +28 -19
- package/src/components/ui/AuthLayout.tsx +58 -0
- package/src/components/ui/Badge.tsx +10 -5
- package/src/components/ui/Button.tsx +22 -12
- package/src/components/ui/ButtonChrome.tsx +1 -1
- package/src/components/ui/Calendar.tsx +6 -7
- package/src/components/ui/Card.tsx +21 -11
- package/src/components/ui/CollapsibleSection.tsx +11 -11
- package/src/components/ui/DropdownMenu.tsx +189 -65
- package/src/components/ui/FilterBar.tsx +165 -0
- package/src/components/ui/FilterChips.tsx +65 -35
- package/src/components/ui/FilterControls.tsx +30 -0
- package/src/components/ui/FormField.tsx +69 -0
- package/src/components/ui/Input.tsx +166 -140
- package/src/components/ui/Kbd.tsx +88 -13
- package/src/components/ui/Loader.tsx +96 -0
- package/src/components/ui/Modal.tsx +66 -36
- package/src/components/ui/SettingsLayout.tsx +94 -0
- package/src/components/ui/ShowcaseWrapper.tsx +4 -16
- package/src/components/ui/Stepper.tsx +5 -6
- package/src/components/ui/first-slice.test.tsx +98 -0
- package/src/components/ui/index.ts +62 -3
- package/src/components/ui/interaction-primitives.test.tsx +77 -0
- package/src/components/ui/kbd-icons.test.tsx +90 -0
- package/src/components/ui/product-patterns.test.tsx +77 -0
- package/src/styles/themes.css +411 -1
- package/tailwind-preset.ts +7 -0
- package/dist/index-CmS6hcUk.d.mts +0 -753
- package/dist/index-CmS6hcUk.d.ts +0 -753
|
@@ -1,5 +1,6 @@
|
|
|
1
|
-
import type
|
|
2
|
-
import {
|
|
1
|
+
import { forwardRef, useId, type ComponentPropsWithoutRef, type ReactNode } from "react";
|
|
2
|
+
import { FormField } from "@/components/ui/FormField";
|
|
3
|
+
import { IconCheck } from "@/components/icons";
|
|
3
4
|
import { cn } from "@/lib/cn";
|
|
4
5
|
|
|
5
6
|
export type InputSize = "sm" | "md" | "lg";
|
|
@@ -21,7 +22,12 @@ const sizeMap: Record<InputSize, string> = {
|
|
|
21
22
|
lg: "ph-input-lg",
|
|
22
23
|
};
|
|
23
24
|
|
|
24
|
-
|
|
25
|
+
function joinIds(...ids: Array<string | undefined>) {
|
|
26
|
+
const value = ids.filter(Boolean).join(" ");
|
|
27
|
+
return value || undefined;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export const Input = forwardRef<HTMLInputElement, InputProps>(function Input({
|
|
25
31
|
id: idProp,
|
|
26
32
|
label,
|
|
27
33
|
hideLabel = false,
|
|
@@ -32,49 +38,48 @@ export function Input({
|
|
|
32
38
|
className,
|
|
33
39
|
wrapperClassName,
|
|
34
40
|
disabled,
|
|
41
|
+
required,
|
|
42
|
+
"aria-describedby": ariaDescribedBy,
|
|
35
43
|
...props
|
|
36
|
-
}
|
|
44
|
+
}, ref) {
|
|
37
45
|
const autoId = useId();
|
|
38
46
|
const id = idProp ?? autoId;
|
|
39
47
|
const hasError = Boolean(error);
|
|
40
48
|
|
|
41
49
|
return (
|
|
42
|
-
<
|
|
43
|
-
{
|
|
44
|
-
|
|
45
|
-
|
|
50
|
+
<FormField
|
|
51
|
+
controlId={id}
|
|
52
|
+
label={label}
|
|
53
|
+
hideLabel={hideLabel}
|
|
54
|
+
helperText={helperText}
|
|
55
|
+
error={error}
|
|
56
|
+
required={required}
|
|
57
|
+
disabled={disabled}
|
|
58
|
+
className={wrapperClassName}
|
|
59
|
+
>
|
|
60
|
+
{(fieldProps) => (
|
|
61
|
+
<input
|
|
62
|
+
{...fieldProps}
|
|
63
|
+
ref={ref}
|
|
64
|
+
disabled={disabled}
|
|
65
|
+
required={required}
|
|
66
|
+
aria-describedby={joinIds(ariaDescribedBy, fieldProps["aria-describedby"])}
|
|
46
67
|
className={cn(
|
|
47
|
-
"ph-
|
|
48
|
-
|
|
49
|
-
|
|
68
|
+
"ph-input w-full",
|
|
69
|
+
sizeMap[size],
|
|
70
|
+
variant === "ghost" && "ph-input-ghost",
|
|
71
|
+
hasError && "ph-input-error",
|
|
72
|
+
disabled && "cursor-not-allowed opacity-60",
|
|
73
|
+
className
|
|
50
74
|
)}
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
</label>
|
|
75
|
+
{...props}
|
|
76
|
+
/>
|
|
54
77
|
)}
|
|
55
|
-
|
|
56
|
-
id={id}
|
|
57
|
-
disabled={disabled}
|
|
58
|
-
aria-invalid={hasError || undefined}
|
|
59
|
-
className={cn(
|
|
60
|
-
"ph-input w-full",
|
|
61
|
-
sizeMap[size],
|
|
62
|
-
variant === "ghost" && "ph-input-ghost",
|
|
63
|
-
hasError && "ph-input-error",
|
|
64
|
-
disabled && "cursor-not-allowed opacity-60",
|
|
65
|
-
className
|
|
66
|
-
)}
|
|
67
|
-
{...props}
|
|
68
|
-
/>
|
|
69
|
-
{helperText && !hasError && (
|
|
70
|
-
<p className="mt-1 text-xs text-ph-subtle">{helperText}</p>
|
|
71
|
-
)}
|
|
72
|
-
{error && (
|
|
73
|
-
<p className="mt-1 text-xs font-medium text-ph-danger">{error}</p>
|
|
74
|
-
)}
|
|
75
|
-
</div>
|
|
78
|
+
</FormField>
|
|
76
79
|
);
|
|
77
|
-
}
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
Input.displayName = "Input";
|
|
78
83
|
|
|
79
84
|
export interface SelectProps extends Omit<ComponentPropsWithoutRef<"select">, "size"> {
|
|
80
85
|
label?: string;
|
|
@@ -86,7 +91,7 @@ export interface SelectProps extends Omit<ComponentPropsWithoutRef<"select">, "s
|
|
|
86
91
|
children: ReactNode;
|
|
87
92
|
}
|
|
88
93
|
|
|
89
|
-
export function Select({
|
|
94
|
+
export const Select = forwardRef<HTMLSelectElement, SelectProps>(function Select({
|
|
90
95
|
id: idProp,
|
|
91
96
|
label,
|
|
92
97
|
hideLabel = false,
|
|
@@ -97,46 +102,40 @@ export function Select({
|
|
|
97
102
|
wrapperClassName,
|
|
98
103
|
disabled,
|
|
99
104
|
children,
|
|
105
|
+
required,
|
|
106
|
+
"aria-describedby": ariaDescribedBy,
|
|
100
107
|
...props
|
|
101
|
-
}
|
|
108
|
+
}, ref) {
|
|
102
109
|
const autoId = useId();
|
|
103
110
|
const id = idProp ?? autoId;
|
|
104
111
|
const hasError = Boolean(error);
|
|
105
112
|
|
|
106
113
|
return (
|
|
107
|
-
<
|
|
108
|
-
{
|
|
109
|
-
<
|
|
110
|
-
|
|
111
|
-
|
|
114
|
+
<FormField controlId={id} label={label} hideLabel={hideLabel} helperText={helperText} error={error} required={required} disabled={disabled} className={wrapperClassName}>
|
|
115
|
+
{(fieldProps) => (
|
|
116
|
+
<select
|
|
117
|
+
{...fieldProps}
|
|
118
|
+
ref={ref}
|
|
119
|
+
disabled={disabled}
|
|
120
|
+
required={required}
|
|
121
|
+
aria-describedby={joinIds(ariaDescribedBy, fieldProps["aria-describedby"])}
|
|
122
|
+
className={cn(
|
|
123
|
+
"ph-select w-full",
|
|
124
|
+
sizeMap[size],
|
|
125
|
+
hasError && "ph-input-error",
|
|
126
|
+
disabled && "cursor-not-allowed opacity-60",
|
|
127
|
+
className
|
|
128
|
+
)}
|
|
129
|
+
{...props}
|
|
112
130
|
>
|
|
113
|
-
{
|
|
114
|
-
</
|
|
131
|
+
{children}
|
|
132
|
+
</select>
|
|
115
133
|
)}
|
|
116
|
-
|
|
117
|
-
id={id}
|
|
118
|
-
disabled={disabled}
|
|
119
|
-
aria-invalid={hasError || undefined}
|
|
120
|
-
className={cn(
|
|
121
|
-
"ph-select w-full",
|
|
122
|
-
sizeMap[size],
|
|
123
|
-
hasError && "border-ph-danger focus:border-ph-danger focus:ring-ph-danger/20",
|
|
124
|
-
disabled && "cursor-not-allowed opacity-60",
|
|
125
|
-
className
|
|
126
|
-
)}
|
|
127
|
-
{...props}
|
|
128
|
-
>
|
|
129
|
-
{children}
|
|
130
|
-
</select>
|
|
131
|
-
{helperText && !hasError && (
|
|
132
|
-
<p className="mt-1 text-xs text-ph-subtle">{helperText}</p>
|
|
133
|
-
)}
|
|
134
|
-
{error && (
|
|
135
|
-
<p className="mt-1 text-xs font-medium text-ph-danger">{error}</p>
|
|
136
|
-
)}
|
|
137
|
-
</div>
|
|
134
|
+
</FormField>
|
|
138
135
|
);
|
|
139
|
-
}
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
Select.displayName = "Select";
|
|
140
139
|
|
|
141
140
|
export interface TextareaProps extends Omit<ComponentPropsWithoutRef<"textarea">, "size"> {
|
|
142
141
|
label?: string;
|
|
@@ -147,7 +146,7 @@ export interface TextareaProps extends Omit<ComponentPropsWithoutRef<"textarea">
|
|
|
147
146
|
wrapperClassName?: string;
|
|
148
147
|
}
|
|
149
148
|
|
|
150
|
-
export function Textarea({
|
|
149
|
+
export const Textarea = forwardRef<HTMLTextAreaElement, TextareaProps>(function Textarea({
|
|
151
150
|
id: idProp,
|
|
152
151
|
label,
|
|
153
152
|
hideLabel = false,
|
|
@@ -157,44 +156,38 @@ export function Textarea({
|
|
|
157
156
|
className,
|
|
158
157
|
wrapperClassName,
|
|
159
158
|
disabled,
|
|
159
|
+
required,
|
|
160
|
+
"aria-describedby": ariaDescribedBy,
|
|
160
161
|
...props
|
|
161
|
-
}
|
|
162
|
+
}, ref) {
|
|
162
163
|
const autoId = useId();
|
|
163
164
|
const id = idProp ?? autoId;
|
|
164
165
|
const hasError = Boolean(error);
|
|
165
166
|
|
|
166
167
|
return (
|
|
167
|
-
<
|
|
168
|
-
{
|
|
169
|
-
<
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
{
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
disabled && "cursor-not-allowed opacity-60",
|
|
185
|
-
className
|
|
186
|
-
)}
|
|
187
|
-
{...props}
|
|
188
|
-
/>
|
|
189
|
-
{helperText && !hasError && (
|
|
190
|
-
<p className="mt-1 text-xs text-ph-subtle">{helperText}</p>
|
|
191
|
-
)}
|
|
192
|
-
{error && (
|
|
193
|
-
<p className="mt-1 text-xs font-medium text-ph-danger">{error}</p>
|
|
168
|
+
<FormField controlId={id} label={label} hideLabel={hideLabel} helperText={helperText} error={error} required={required} disabled={disabled} className={wrapperClassName}>
|
|
169
|
+
{(fieldProps) => (
|
|
170
|
+
<textarea
|
|
171
|
+
{...fieldProps}
|
|
172
|
+
ref={ref}
|
|
173
|
+
disabled={disabled}
|
|
174
|
+
required={required}
|
|
175
|
+
aria-describedby={joinIds(ariaDescribedBy, fieldProps["aria-describedby"])}
|
|
176
|
+
className={cn(
|
|
177
|
+
"ph-textarea w-full",
|
|
178
|
+
sizeMap[size],
|
|
179
|
+
hasError && "ph-input-error",
|
|
180
|
+
disabled && "cursor-not-allowed opacity-60",
|
|
181
|
+
className
|
|
182
|
+
)}
|
|
183
|
+
{...props}
|
|
184
|
+
/>
|
|
194
185
|
)}
|
|
195
|
-
</
|
|
186
|
+
</FormField>
|
|
196
187
|
);
|
|
197
|
-
}
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
Textarea.displayName = "Textarea";
|
|
198
191
|
|
|
199
192
|
export interface CheckboxProps extends Omit<ComponentPropsWithoutRef<"input">, "type"> {
|
|
200
193
|
label?: ReactNode;
|
|
@@ -203,7 +196,7 @@ export interface CheckboxProps extends Omit<ComponentPropsWithoutRef<"input">, "
|
|
|
203
196
|
wrapperClassName?: string;
|
|
204
197
|
}
|
|
205
198
|
|
|
206
|
-
export function Checkbox({
|
|
199
|
+
export const Checkbox = forwardRef<HTMLInputElement, CheckboxProps>(function Checkbox({
|
|
207
200
|
id: idProp,
|
|
208
201
|
label,
|
|
209
202
|
error,
|
|
@@ -211,59 +204,69 @@ export function Checkbox({
|
|
|
211
204
|
className,
|
|
212
205
|
wrapperClassName,
|
|
213
206
|
disabled,
|
|
207
|
+
"aria-describedby": ariaDescribedBy,
|
|
214
208
|
...props
|
|
215
|
-
}
|
|
209
|
+
}, ref) {
|
|
216
210
|
const autoId = useId();
|
|
217
211
|
const id = idProp ?? autoId;
|
|
218
212
|
const hasError = Boolean(error);
|
|
213
|
+
const descriptionId = helperText && !hasError ? `${id}-description` : undefined;
|
|
214
|
+
const errorId = error ? `${id}-error` : undefined;
|
|
219
215
|
|
|
220
216
|
return (
|
|
221
217
|
<div className={cn("ph-control-row", wrapperClassName)}>
|
|
222
|
-
<
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
className
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
<
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
<
|
|
243
|
-
|
|
218
|
+
<span className="ph-checkbox-wrap">
|
|
219
|
+
<input
|
|
220
|
+
ref={ref}
|
|
221
|
+
id={id}
|
|
222
|
+
type="checkbox"
|
|
223
|
+
disabled={disabled}
|
|
224
|
+
aria-invalid={hasError || undefined}
|
|
225
|
+
aria-describedby={joinIds(ariaDescribedBy, descriptionId)}
|
|
226
|
+
aria-errormessage={errorId}
|
|
227
|
+
className={cn(
|
|
228
|
+
"ph-checkbox",
|
|
229
|
+
hasError && "border-ph-danger",
|
|
230
|
+
disabled && "cursor-not-allowed opacity-60",
|
|
231
|
+
className
|
|
232
|
+
)}
|
|
233
|
+
{...props}
|
|
234
|
+
/>
|
|
235
|
+
<IconCheck className="ph-checkbox-icon" aria-hidden />
|
|
236
|
+
</span>
|
|
237
|
+
<div className="ph-control-copy">
|
|
238
|
+
<label htmlFor={id} className={cn("cursor-pointer", disabled && "opacity-60")}>
|
|
239
|
+
{label}
|
|
240
|
+
</label>
|
|
241
|
+
{descriptionId ? <p id={descriptionId} className="text-xs text-ph-subtle">{helperText}</p> : null}
|
|
242
|
+
{errorId ? <p id={errorId} className="text-xs font-medium text-ph-danger">{error}</p> : null}
|
|
243
|
+
</div>
|
|
244
244
|
</div>
|
|
245
245
|
);
|
|
246
|
-
}
|
|
246
|
+
});
|
|
247
|
+
|
|
248
|
+
Checkbox.displayName = "Checkbox";
|
|
247
249
|
|
|
248
250
|
export interface RadioProps extends Omit<ComponentPropsWithoutRef<"input">, "type"> {
|
|
249
251
|
label?: ReactNode;
|
|
250
252
|
wrapperClassName?: string;
|
|
251
253
|
}
|
|
252
254
|
|
|
253
|
-
export function Radio({
|
|
255
|
+
export const Radio = forwardRef<HTMLInputElement, RadioProps>(function Radio({
|
|
254
256
|
id: idProp,
|
|
255
257
|
label,
|
|
256
258
|
className,
|
|
257
259
|
wrapperClassName,
|
|
258
260
|
disabled,
|
|
259
261
|
...props
|
|
260
|
-
}
|
|
262
|
+
}, ref) {
|
|
261
263
|
const autoId = useId();
|
|
262
264
|
const id = idProp ?? autoId;
|
|
263
265
|
|
|
264
266
|
return (
|
|
265
267
|
<div className={cn("ph-control-row", wrapperClassName, disabled && "opacity-60")}>
|
|
266
268
|
<input
|
|
269
|
+
ref={ref}
|
|
267
270
|
id={id}
|
|
268
271
|
type="radio"
|
|
269
272
|
disabled={disabled}
|
|
@@ -275,21 +278,23 @@ export function Radio({
|
|
|
275
278
|
</label>
|
|
276
279
|
</div>
|
|
277
280
|
);
|
|
278
|
-
}
|
|
281
|
+
});
|
|
282
|
+
|
|
283
|
+
Radio.displayName = "Radio";
|
|
279
284
|
|
|
280
285
|
export interface ToggleProps extends Omit<ComponentPropsWithoutRef<"input">, "type"> {
|
|
281
286
|
label?: ReactNode;
|
|
282
287
|
wrapperClassName?: string;
|
|
283
288
|
}
|
|
284
289
|
|
|
285
|
-
export function Toggle({
|
|
290
|
+
export const Toggle = forwardRef<HTMLInputElement, ToggleProps>(function Toggle({
|
|
286
291
|
id: idProp,
|
|
287
292
|
label,
|
|
288
293
|
className,
|
|
289
294
|
wrapperClassName,
|
|
290
295
|
disabled,
|
|
291
296
|
...props
|
|
292
|
-
}
|
|
297
|
+
}, ref) {
|
|
293
298
|
const autoId = useId();
|
|
294
299
|
const id = idProp ?? autoId;
|
|
295
300
|
|
|
@@ -297,6 +302,7 @@ export function Toggle({
|
|
|
297
302
|
<label className={cn("ph-control-row-between", wrapperClassName, disabled && "opacity-60")}>
|
|
298
303
|
<span className={disabled ? "opacity-60" : undefined}>{label}</span>
|
|
299
304
|
<input
|
|
305
|
+
ref={ref}
|
|
300
306
|
id={id}
|
|
301
307
|
type="checkbox"
|
|
302
308
|
role="switch"
|
|
@@ -306,7 +312,9 @@ export function Toggle({
|
|
|
306
312
|
/>
|
|
307
313
|
</label>
|
|
308
314
|
);
|
|
309
|
-
}
|
|
315
|
+
});
|
|
316
|
+
|
|
317
|
+
Toggle.displayName = "Toggle";
|
|
310
318
|
|
|
311
319
|
export interface RangeProps extends Omit<ComponentPropsWithoutRef<"input">, "type"> {
|
|
312
320
|
label?: string;
|
|
@@ -316,7 +324,8 @@ export interface RangeProps extends Omit<ComponentPropsWithoutRef<"input">, "typ
|
|
|
316
324
|
wrapperClassName?: string;
|
|
317
325
|
}
|
|
318
326
|
|
|
319
|
-
export function Range({
|
|
327
|
+
export const Range = forwardRef<HTMLInputElement, RangeProps>(function Range({
|
|
328
|
+
id: idProp,
|
|
320
329
|
label,
|
|
321
330
|
minLabel,
|
|
322
331
|
maxLabel,
|
|
@@ -324,11 +333,14 @@ export function Range({
|
|
|
324
333
|
wrapperClassName,
|
|
325
334
|
className,
|
|
326
335
|
...props
|
|
327
|
-
}
|
|
336
|
+
}, ref) {
|
|
337
|
+
const autoId = useId();
|
|
338
|
+
const id = idProp ?? autoId;
|
|
339
|
+
|
|
328
340
|
return (
|
|
329
341
|
<div className={cn("ph-field", wrapperClassName)}>
|
|
330
|
-
{label && <label className="ph-label">{label}</label>}
|
|
331
|
-
<input type="range" className={cn("ph-range", className)} {...props} />
|
|
342
|
+
{label && <label htmlFor={id} className="ph-label">{label}</label>}
|
|
343
|
+
<input ref={ref} id={id} type="range" className={cn("ph-range", className)} {...props} />
|
|
332
344
|
{(minLabel || valueLabel || maxLabel) && (
|
|
333
345
|
<div className="flex justify-between text-xs text-ph-mutedtext">
|
|
334
346
|
<span>{minLabel}</span>
|
|
@@ -338,7 +350,9 @@ export function Range({
|
|
|
338
350
|
)}
|
|
339
351
|
</div>
|
|
340
352
|
);
|
|
341
|
-
}
|
|
353
|
+
});
|
|
354
|
+
|
|
355
|
+
Range.displayName = "Range";
|
|
342
356
|
|
|
343
357
|
export interface FileUploadProps extends Omit<ComponentPropsWithoutRef<"input">, "type"> {
|
|
344
358
|
label?: string;
|
|
@@ -346,14 +360,26 @@ export interface FileUploadProps extends Omit<ComponentPropsWithoutRef<"input">,
|
|
|
346
360
|
wrapperClassName?: string;
|
|
347
361
|
}
|
|
348
362
|
|
|
349
|
-
export
|
|
363
|
+
export const FileUpload = forwardRef<HTMLInputElement, FileUploadProps>(function FileUpload({
|
|
364
|
+
id: idProp,
|
|
365
|
+
label,
|
|
366
|
+
prompt = "Drop file or browse",
|
|
367
|
+
wrapperClassName,
|
|
368
|
+
className,
|
|
369
|
+
...props
|
|
370
|
+
}, ref) {
|
|
371
|
+
const autoId = useId();
|
|
372
|
+
const id = idProp ?? autoId;
|
|
373
|
+
|
|
350
374
|
return (
|
|
351
375
|
<div className={cn("ph-field", wrapperClassName)}>
|
|
352
|
-
{label && <label className="ph-label">{label}</label>}
|
|
353
|
-
<label className="
|
|
354
|
-
<input type="file" className={cn("sr-only", className)} {...props} />
|
|
376
|
+
{label && <label htmlFor={id} className="ph-label">{label}</label>}
|
|
377
|
+
<label htmlFor={id} className="ph-file-upload">
|
|
378
|
+
<input ref={ref} id={id} type="file" className={cn("sr-only", className)} {...props} />
|
|
355
379
|
{prompt}
|
|
356
380
|
</label>
|
|
357
381
|
</div>
|
|
358
382
|
);
|
|
359
|
-
}
|
|
383
|
+
});
|
|
384
|
+
|
|
385
|
+
FileUpload.displayName = "FileUpload";
|
|
@@ -2,37 +2,112 @@ import type { HTMLAttributes, ReactNode } from "react";
|
|
|
2
2
|
import { cn } from "@/lib/cn";
|
|
3
3
|
|
|
4
4
|
export type KbdVariant = "shortcut" | "key" | "token";
|
|
5
|
+
export type KbdPlatform = "text" | "mac" | "windows";
|
|
5
6
|
|
|
6
7
|
export interface KbdProps extends HTMLAttributes<HTMLElement> {
|
|
7
8
|
variant?: KbdVariant;
|
|
8
|
-
keys?: ReactNode[];
|
|
9
|
+
keys?: readonly ReactNode[];
|
|
10
|
+
platform?: KbdPlatform;
|
|
11
|
+
separator?: ReactNode;
|
|
9
12
|
children?: ReactNode;
|
|
10
13
|
}
|
|
11
14
|
|
|
12
|
-
|
|
15
|
+
const platformLabels: Record<KbdPlatform, Record<string, string>> = {
|
|
16
|
+
text: {
|
|
17
|
+
command: "Command",
|
|
18
|
+
cmd: "Command",
|
|
19
|
+
option: "Option",
|
|
20
|
+
alt: "Alt",
|
|
21
|
+
control: "Control",
|
|
22
|
+
ctrl: "Control",
|
|
23
|
+
shift: "Shift",
|
|
24
|
+
},
|
|
25
|
+
mac: {
|
|
26
|
+
command: "⌘",
|
|
27
|
+
cmd: "⌘",
|
|
28
|
+
option: "⌥",
|
|
29
|
+
alt: "⌥",
|
|
30
|
+
control: "⌃",
|
|
31
|
+
ctrl: "⌃",
|
|
32
|
+
shift: "⇧",
|
|
33
|
+
},
|
|
34
|
+
windows: {
|
|
35
|
+
command: "Win",
|
|
36
|
+
cmd: "Win",
|
|
37
|
+
option: "Alt",
|
|
38
|
+
alt: "Alt",
|
|
39
|
+
control: "Ctrl",
|
|
40
|
+
ctrl: "Ctrl",
|
|
41
|
+
shift: "Shift",
|
|
42
|
+
},
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
const spokenLabels: Record<string, string> = {
|
|
46
|
+
"⌘": "Command",
|
|
47
|
+
"⌥": "Option",
|
|
48
|
+
"⌃": "Control",
|
|
49
|
+
"⇧": "Shift",
|
|
50
|
+
"↵": "Enter",
|
|
51
|
+
"⏎": "Enter",
|
|
52
|
+
"⌫": "Backspace",
|
|
53
|
+
"⎋": "Escape",
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
export function Kbd({
|
|
57
|
+
variant = "shortcut",
|
|
58
|
+
keys,
|
|
59
|
+
platform = "text",
|
|
60
|
+
separator,
|
|
61
|
+
children,
|
|
62
|
+
className,
|
|
63
|
+
"aria-label": ariaLabel,
|
|
64
|
+
...props
|
|
65
|
+
}: KbdProps) {
|
|
13
66
|
if (variant === "token") {
|
|
14
67
|
return (
|
|
15
|
-
<code className={cn("ph-
|
|
68
|
+
<code className={cn("ph-code-token ph-kbd", className)} aria-label={ariaLabel} {...props}>
|
|
16
69
|
{children}
|
|
17
70
|
</code>
|
|
18
71
|
);
|
|
19
72
|
}
|
|
20
73
|
|
|
21
|
-
const
|
|
74
|
+
const sourceKeys = keys?.length ? keys : children != null ? [children] : [];
|
|
75
|
+
const displayKeys = sourceKeys.map((key) => formatKey(key, platform));
|
|
76
|
+
const accessibleLabel = ariaLabel ?? getShortcutLabel(displayKeys);
|
|
77
|
+
|
|
78
|
+
if (variant === "key") {
|
|
79
|
+
return (
|
|
80
|
+
<kbd className={cn("ph-keycap", className)} aria-label={accessibleLabel} {...props}>
|
|
81
|
+
{displayKeys[0]}
|
|
82
|
+
</kbd>
|
|
83
|
+
);
|
|
84
|
+
}
|
|
22
85
|
|
|
23
86
|
return (
|
|
24
|
-
<kbd className={cn("ph-shortcut", className)} {...props}>
|
|
25
|
-
{
|
|
87
|
+
<kbd className={cn("ph-shortcut-group ph-keycap", className)} aria-label={accessibleLabel} {...props}>
|
|
88
|
+
{displayKeys.map((key, index) => (
|
|
89
|
+
<span key={index} className="contents">
|
|
90
|
+
{index > 0 && separator != null ? <span className="ph-shortcut-separator" aria-hidden="true">{separator}</span> : null}
|
|
91
|
+
<span className="ph-shortcut-key" aria-hidden={accessibleLabel ? true : undefined}>{key}</span>
|
|
92
|
+
</span>
|
|
93
|
+
))}
|
|
26
94
|
</kbd>
|
|
27
95
|
);
|
|
28
96
|
}
|
|
29
97
|
|
|
30
|
-
function
|
|
31
|
-
if (typeof
|
|
98
|
+
function formatKey(key: ReactNode, platform: KbdPlatform) {
|
|
99
|
+
if (typeof key !== "string") return key;
|
|
100
|
+
return platformLabels[platform][key.toLowerCase()] ?? key;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function getShortcutLabel(keys: ReactNode[]) {
|
|
104
|
+
const labels = keys.map((key) => {
|
|
105
|
+
if (typeof key !== "string" && typeof key !== "number") return null;
|
|
106
|
+
const value = String(key);
|
|
107
|
+
return spokenLabels[value] ?? value;
|
|
108
|
+
});
|
|
32
109
|
|
|
33
|
-
return
|
|
34
|
-
.
|
|
35
|
-
|
|
36
|
-
.replace(/\bShift\b/g, "⇧")
|
|
37
|
-
.replace(/\bControl\b|\bCtrl\b/g, "⌃");
|
|
110
|
+
return labels.every((label): label is string => label != null)
|
|
111
|
+
? labels.join(" + ")
|
|
112
|
+
: undefined;
|
|
38
113
|
}
|