@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.
Files changed (61) hide show
  1. package/README.md +1 -1
  2. package/dist/{chunk-SHF57J7U.mjs → chunk-FDMD3IIN.mjs} +1573 -652
  3. package/dist/index-D1RTT2Ap.d.mts +1044 -0
  4. package/dist/index-D1RTT2Ap.d.ts +1044 -0
  5. package/dist/index.d.mts +10 -7
  6. package/dist/index.d.ts +10 -7
  7. package/dist/index.js +1705 -744
  8. package/dist/index.mjs +61 -1
  9. package/dist/tailwind-preset.js +7 -0
  10. package/dist/tailwind-preset.mjs +7 -0
  11. package/dist/ui.d.mts +3 -2
  12. package/dist/ui.d.ts +3 -2
  13. package/dist/ui.js +1584 -666
  14. package/dist/ui.mjs +31 -1
  15. package/package.json +18 -4
  16. package/src/app/globals.css +1005 -227
  17. package/src/components/icons/IconBase.tsx +20 -12
  18. package/src/components/icons/createIconBase.tsx +12 -7
  19. package/src/components/icons/icons.tsx +208 -18
  20. package/src/components/sections/AlertShowcase.tsx +2 -1
  21. package/src/components/sections/BadgeShowcase.tsx +1 -1
  22. package/src/components/sections/ButtonShowcase.tsx +6 -10
  23. package/src/components/sections/CardShowcase.tsx +22 -6
  24. package/src/components/sections/DropdownShowcase.tsx +4 -6
  25. package/src/components/sections/FilterChipsShowcase.tsx +74 -26
  26. package/src/components/sections/FormShowcase.tsx +2 -1
  27. package/src/components/sections/IconShowcase.tsx +86 -206
  28. package/src/components/sections/KbdShowcase.tsx +47 -21
  29. package/src/components/sections/ModalShowcase.tsx +7 -6
  30. package/src/components/sections/ProductLayoutsShowcase.tsx +138 -0
  31. package/src/components/sections/ProgressShowcase.tsx +50 -53
  32. package/src/components/sections/SwapShowcase.tsx +13 -5
  33. package/src/components/ui/Alert.tsx +28 -19
  34. package/src/components/ui/AuthLayout.tsx +58 -0
  35. package/src/components/ui/Badge.tsx +10 -5
  36. package/src/components/ui/Button.tsx +22 -12
  37. package/src/components/ui/ButtonChrome.tsx +1 -1
  38. package/src/components/ui/Calendar.tsx +6 -7
  39. package/src/components/ui/Card.tsx +21 -11
  40. package/src/components/ui/CollapsibleSection.tsx +11 -11
  41. package/src/components/ui/DropdownMenu.tsx +189 -65
  42. package/src/components/ui/FilterBar.tsx +165 -0
  43. package/src/components/ui/FilterChips.tsx +65 -35
  44. package/src/components/ui/FilterControls.tsx +30 -0
  45. package/src/components/ui/FormField.tsx +69 -0
  46. package/src/components/ui/Input.tsx +166 -140
  47. package/src/components/ui/Kbd.tsx +88 -13
  48. package/src/components/ui/Loader.tsx +96 -0
  49. package/src/components/ui/Modal.tsx +66 -36
  50. package/src/components/ui/SettingsLayout.tsx +94 -0
  51. package/src/components/ui/ShowcaseWrapper.tsx +4 -16
  52. package/src/components/ui/Stepper.tsx +5 -6
  53. package/src/components/ui/first-slice.test.tsx +98 -0
  54. package/src/components/ui/index.ts +62 -3
  55. package/src/components/ui/interaction-primitives.test.tsx +77 -0
  56. package/src/components/ui/kbd-icons.test.tsx +90 -0
  57. package/src/components/ui/product-patterns.test.tsx +77 -0
  58. package/src/styles/themes.css +411 -1
  59. package/tailwind-preset.ts +7 -0
  60. package/dist/index-CmS6hcUk.d.mts +0 -753
  61. package/dist/index-CmS6hcUk.d.ts +0 -753
@@ -1,5 +1,6 @@
1
- import type { ComponentPropsWithoutRef, ReactNode } from "react";
2
- import { useId } from "react";
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
- export function Input({
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
- }: InputProps) {
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
- <div className={cn("ph-field", wrapperClassName)}>
43
- {label && (
44
- <label
45
- htmlFor={id}
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-label",
48
- hideLabel && "sr-only",
49
- disabled && "opacity-50"
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
- {label}
53
- </label>
75
+ {...props}
76
+ />
54
77
  )}
55
- <input
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
- }: SelectProps) {
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
- <div className={cn("ph-field", wrapperClassName)}>
108
- {label && (
109
- <label
110
- htmlFor={id}
111
- className={cn("ph-label", hideLabel && "sr-only", disabled && "opacity-50")}
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
- {label}
114
- </label>
131
+ {children}
132
+ </select>
115
133
  )}
116
- <select
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
- }: TextareaProps) {
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
- <div className={cn("ph-field", wrapperClassName)}>
168
- {label && (
169
- <label
170
- htmlFor={id}
171
- className={cn("ph-label", hideLabel && "sr-only", disabled && "opacity-50")}
172
- >
173
- {label}
174
- </label>
175
- )}
176
- <textarea
177
- id={id}
178
- disabled={disabled}
179
- aria-invalid={hasError || undefined}
180
- className={cn(
181
- "ph-textarea w-full",
182
- sizeMap[size],
183
- hasError && "border-ph-danger focus:border-ph-danger focus:ring-ph-danger/20",
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
- </div>
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
- }: CheckboxProps) {
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
- <input
223
- id={id}
224
- type="checkbox"
225
- disabled={disabled}
226
- aria-invalid={hasError || undefined}
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
- <label htmlFor={id} className={cn("cursor-pointer", disabled && "opacity-60")}>
236
- {label}
237
- </label>
238
- {helperText && !hasError && (
239
- <p className="mt-0.5 text-xs text-ph-subtle">{helperText}</p>
240
- )}
241
- {error && (
242
- <p className="mt-0.5 text-xs font-medium text-ph-danger">{error}</p>
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
- }: RadioProps) {
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
- }: ToggleProps) {
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
- }: RangeProps) {
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 function FileUpload({ label, prompt = "Drop file or browse", wrapperClassName, className, ...props }: FileUploadProps) {
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="flex cursor-pointer items-center gap-3 rounded-lg border border-dashed border-ph-border bg-ph-surface px-4 py-6 text-sm text-ph-subtle transition hover:bg-ph-muted/60">
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
- export function Kbd({ variant = "shortcut", keys, children, className, ...props }: KbdProps) {
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-kbd text-ph-ink", className)} {...props}>
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 content = keys?.length ? keys.join(" ") : normalizeShortcut(children);
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
- {content}
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 normalizeShortcut(children: ReactNode) {
31
- if (typeof children !== "string") return children;
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 children
34
- .replace(/\bCommand\b|\bCmd\b/g, "⌘")
35
- .replace(/\bOption\b|\bAlt\b/g, "⌥")
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
  }