@xenide-io/the-old-ui-theme 0.2.6 → 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 (126) hide show
  1. package/README.md +1 -1
  2. package/dist/chunk-FDMD3IIN.mjs +3831 -0
  3. package/dist/index-D1RTT2Ap.d.mts +1044 -0
  4. package/dist/index-D1RTT2Ap.d.ts +1044 -0
  5. package/dist/index.d.mts +11 -758
  6. package/dist/index.d.ts +11 -758
  7. package/dist/index.js +1749 -768
  8. package/dist/index.mjs +312 -2846
  9. package/dist/tailwind-preset.js +7 -0
  10. package/dist/tailwind-preset.mjs +7 -0
  11. package/dist/ui.d.mts +4 -0
  12. package/dist/ui.d.ts +4 -0
  13. package/dist/ui.js +3864 -0
  14. package/dist/ui.mjs +169 -0
  15. package/package.json +28 -6
  16. package/src/app/globals.css +1005 -227
  17. package/src/components/charts/index.ts +8 -0
  18. package/src/components/charts/ph-insight-charts.tsx +162 -0
  19. package/src/components/dashboard/DashboardFiltersBar.tsx +19 -0
  20. package/src/components/dashboard/DashboardGrid.tsx +23 -0
  21. package/src/components/dashboard/DashboardInsightPlaceholder.tsx +37 -0
  22. package/src/components/dashboard/DashboardKpiCard.tsx +25 -0
  23. package/src/components/dashboard/DashboardToolbar.tsx +23 -0
  24. package/src/components/dashboard/DashboardWell.tsx +10 -0
  25. package/src/components/dashboard/InsightMiniCard.tsx +26 -0
  26. package/src/components/dashboard/InsightShell.tsx +37 -0
  27. package/src/components/dashboard/InsightShellHeader.tsx +22 -0
  28. package/src/components/dashboard/MiniTrendBars.tsx +51 -0
  29. package/src/components/dashboard/index.ts +31 -0
  30. package/src/components/dashboard/types.ts +9 -0
  31. package/src/components/icons/IconBase.tsx +47 -0
  32. package/src/components/icons/createIconBase.tsx +32 -0
  33. package/src/components/icons/icons.tsx +557 -0
  34. package/src/components/icons/index.ts +3 -0
  35. package/src/components/sections/AccordionShowcase.tsx +45 -0
  36. package/src/components/sections/AlertShowcase.tsx +40 -0
  37. package/src/components/sections/AvatarShowcase.tsx +80 -0
  38. package/src/components/sections/BadgeShowcase.tsx +65 -0
  39. package/src/components/sections/ButtonShowcase.tsx +308 -0
  40. package/src/components/sections/CalendarShowcase.tsx +35 -0
  41. package/src/components/sections/CardShowcase.tsx +159 -0
  42. package/src/components/sections/ChartShowcase.tsx +190 -0
  43. package/src/components/sections/CodeMockupShowcase.tsx +56 -0
  44. package/src/components/sections/ColorPalette.tsx +117 -0
  45. package/src/components/sections/CommandPaletteShowcase.tsx +48 -0
  46. package/src/components/sections/CountdownShowcase.tsx +43 -0
  47. package/src/components/sections/DashboardShowcase.tsx +191 -0
  48. package/src/components/sections/DiffShowcase.tsx +70 -0
  49. package/src/components/sections/DrawerShowcase.tsx +97 -0
  50. package/src/components/sections/DropdownShowcase.tsx +98 -0
  51. package/src/components/sections/EmptyStateShowcase.tsx +50 -0
  52. package/src/components/sections/FilterChipsShowcase.tsx +98 -0
  53. package/src/components/sections/FormShowcase.tsx +127 -0
  54. package/src/components/sections/HoverCardShowcase.tsx +50 -0
  55. package/src/components/sections/IconShowcase.tsx +127 -0
  56. package/src/components/sections/IndicatorShowcase.tsx +52 -0
  57. package/src/components/sections/KbdShowcase.tsx +57 -0
  58. package/src/components/sections/ModalShowcase.tsx +211 -0
  59. package/src/components/sections/NavigationShowcase.tsx +199 -0
  60. package/src/components/sections/ProductLayoutsShowcase.tsx +138 -0
  61. package/src/components/sections/ProgressShowcase.tsx +82 -0
  62. package/src/components/sections/SegmentedControlShowcase.tsx +49 -0
  63. package/src/components/sections/SetupThemeShowcase.tsx +262 -0
  64. package/src/components/sections/StackShowcase.tsx +33 -0
  65. package/src/components/sections/StatShowcase.tsx +129 -0
  66. package/src/components/sections/StepperShowcase.tsx +37 -0
  67. package/src/components/sections/SwapShowcase.tsx +99 -0
  68. package/src/components/sections/TabShowcase.tsx +84 -0
  69. package/src/components/sections/TableShowcase.tsx +140 -0
  70. package/src/components/sections/TimelineShowcase.tsx +83 -0
  71. package/src/components/sections/ToastShowcase.tsx +108 -0
  72. package/src/components/sections/TooltipShowcase.tsx +44 -0
  73. package/src/components/sections/UtilityShowcase.tsx +81 -0
  74. package/src/components/ui/Accordion.tsx +77 -0
  75. package/src/components/ui/Alert.tsx +77 -0
  76. package/src/components/ui/AuthLayout.tsx +58 -0
  77. package/src/components/ui/Avatar.tsx +97 -0
  78. package/src/components/ui/Badge.tsx +60 -0
  79. package/src/components/ui/Button.tsx +120 -0
  80. package/src/components/ui/ButtonChrome.tsx +20 -0
  81. package/src/components/ui/Calendar.tsx +115 -0
  82. package/src/components/ui/Card.tsx +70 -0
  83. package/src/components/ui/ChatBubble.tsx +71 -0
  84. package/src/components/ui/CodeBlock.tsx +48 -0
  85. package/src/components/ui/CollapsibleSection.tsx +49 -0
  86. package/src/components/ui/CommandPalette.tsx +137 -0
  87. package/src/components/ui/ComponentDocs.tsx +52 -0
  88. package/src/components/ui/Drawer.tsx +71 -0
  89. package/src/components/ui/DropdownMenu.tsx +262 -0
  90. package/src/components/ui/EmptyState.tsx +33 -0
  91. package/src/components/ui/FilterBar.tsx +165 -0
  92. package/src/components/ui/FilterChips.tsx +80 -0
  93. package/src/components/ui/FilterControls.tsx +30 -0
  94. package/src/components/ui/FormField.tsx +69 -0
  95. package/src/components/ui/HoverCard.tsx +36 -0
  96. package/src/components/ui/Indicator.tsx +61 -0
  97. package/src/components/ui/Input.tsx +385 -0
  98. package/src/components/ui/Kbd.tsx +113 -0
  99. package/src/components/ui/Loader.tsx +96 -0
  100. package/src/components/ui/Modal.tsx +105 -0
  101. package/src/components/ui/Navigation.tsx +94 -0
  102. package/src/components/ui/Panel.tsx +18 -0
  103. package/src/components/ui/Progress.tsx +59 -0
  104. package/src/components/ui/Rating.tsx +65 -0
  105. package/src/components/ui/SearchInput.tsx +106 -0
  106. package/src/components/ui/SegmentedControl.tsx +44 -0
  107. package/src/components/ui/SettingsLayout.tsx +94 -0
  108. package/src/components/ui/ShowcaseWrapper.tsx +124 -0
  109. package/src/components/ui/Stat.tsx +39 -0
  110. package/src/components/ui/Stepper.tsx +74 -0
  111. package/src/components/ui/Table.tsx +55 -0
  112. package/src/components/ui/Tabs.tsx +53 -0
  113. package/src/components/ui/Terminal.tsx +54 -0
  114. package/src/components/ui/ThemeDomSync.tsx +17 -0
  115. package/src/components/ui/ThemeManager.tsx +18 -0
  116. package/src/components/ui/ThemeSwitcher.tsx +46 -0
  117. package/src/components/ui/Timeline.tsx +60 -0
  118. package/src/components/ui/Toast.tsx +70 -0
  119. package/src/components/ui/Typography.tsx +129 -0
  120. package/src/components/ui/first-slice.test.tsx +98 -0
  121. package/src/components/ui/index.ts +151 -0
  122. package/src/components/ui/interaction-primitives.test.tsx +77 -0
  123. package/src/components/ui/kbd-icons.test.tsx +90 -0
  124. package/src/components/ui/product-patterns.test.tsx +77 -0
  125. package/src/styles/themes.css +528 -0
  126. package/tailwind-preset.ts +7 -0
@@ -0,0 +1,69 @@
1
+ import { useId, type HTMLAttributes, type ReactNode } from "react";
2
+ import { cn } from "@/lib/cn";
3
+
4
+ export interface FormFieldControlProps {
5
+ id: string;
6
+ "aria-describedby"?: string;
7
+ "aria-errormessage"?: string;
8
+ "aria-invalid"?: true;
9
+ }
10
+
11
+ export interface FormFieldProps extends Omit<HTMLAttributes<HTMLDivElement>, "children"> {
12
+ controlId?: string;
13
+ label?: ReactNode;
14
+ hideLabel?: boolean;
15
+ helperText?: ReactNode;
16
+ error?: ReactNode;
17
+ required?: boolean;
18
+ disabled?: boolean;
19
+ children: ReactNode | ((controlProps: FormFieldControlProps) => ReactNode);
20
+ }
21
+
22
+ export function FormField({
23
+ controlId,
24
+ label,
25
+ hideLabel = false,
26
+ helperText,
27
+ error,
28
+ required = false,
29
+ disabled = false,
30
+ children,
31
+ className,
32
+ ...props
33
+ }: FormFieldProps) {
34
+ const generatedId = useId();
35
+ const id = controlId ?? generatedId;
36
+ const descriptionId = helperText && !error ? `${id}-description` : undefined;
37
+ const errorId = error ? `${id}-error` : undefined;
38
+ const controlProps: FormFieldControlProps = {
39
+ id,
40
+ "aria-describedby": descriptionId,
41
+ "aria-errormessage": errorId,
42
+ "aria-invalid": error ? true : undefined,
43
+ };
44
+
45
+ return (
46
+ <div className={cn("ph-field", className)} {...props}>
47
+ {label ? (
48
+ <label
49
+ htmlFor={id}
50
+ className={cn("ph-label", hideLabel && "sr-only", disabled && "opacity-50")}
51
+ >
52
+ {label}
53
+ {required ? <span aria-hidden="true"> *</span> : null}
54
+ </label>
55
+ ) : null}
56
+ {typeof children === "function" ? children(controlProps) : children}
57
+ {descriptionId ? (
58
+ <p id={descriptionId} className="mt-1 text-xs text-ph-subtle">
59
+ {helperText}
60
+ </p>
61
+ ) : null}
62
+ {errorId ? (
63
+ <p id={errorId} className="mt-1 text-xs font-medium text-ph-danger">
64
+ {error}
65
+ </p>
66
+ ) : null}
67
+ </div>
68
+ );
69
+ }
@@ -0,0 +1,36 @@
1
+ "use client";
2
+
3
+ import { useState } from "react";
4
+ import { cn } from "@/lib/cn";
5
+
6
+ interface HoverCardProps {
7
+ trigger: React.ReactNode;
8
+ children: React.ReactNode;
9
+ className?: string;
10
+ }
11
+
12
+ export function HoverCard({ trigger, children, className }: HoverCardProps) {
13
+ const [isVisible, setIsVisible] = useState(false);
14
+
15
+ return (
16
+ <div
17
+ className="relative inline-block"
18
+ onMouseEnter={() => setIsVisible(true)}
19
+ onMouseLeave={() => setIsVisible(false)}
20
+ >
21
+ {trigger}
22
+ <div
23
+ className={cn(
24
+ "absolute left-1/2 top-full z-50 mt-2 w-64 -translate-x-1/2 rounded-lg border border-ph-border bg-ph-surface p-4 shadow-ph-md transition-all duration-150",
25
+ isVisible
26
+ ? "visible translate-y-0 opacity-100"
27
+ : "invisible -translate-y-1 opacity-0",
28
+ className
29
+ )}
30
+ >
31
+ <div className="absolute -top-1.5 left-1/2 h-3 w-3 -translate-x-1/2 rotate-45 border-l border-t border-ph-border bg-ph-surface" />
32
+ <div className="relative">{children}</div>
33
+ </div>
34
+ </div>
35
+ );
36
+ }
@@ -0,0 +1,61 @@
1
+ import type { ComponentPropsWithoutRef, ReactNode } from "react";
2
+ import { cn } from "@/lib/cn";
3
+
4
+ export type IndicatorPosition = "top-right" | "top-left" | "bottom-right" | "bottom-left";
5
+
6
+ export interface IndicatorProps extends ComponentPropsWithoutRef<"div"> {
7
+ /** The element to attach the indicator to */
8
+ children: ReactNode;
9
+ /** Badge content (number, text, or "!") */
10
+ badge?: ReactNode;
11
+ /** Indicator colour */
12
+ color?: string;
13
+ /** Position of the indicator dot */
14
+ position?: IndicatorPosition;
15
+ /** Show a simple dot instead of a badge */
16
+ dot?: boolean;
17
+ /** Offset from corner (default: 2px) */
18
+ offset?: number;
19
+ }
20
+
21
+ const positionMap: Record<IndicatorPosition, string> = {
22
+ "top-right": "-right-[2px] -top-[2px]",
23
+ "top-left": "-left-[2px] -top-[2px]",
24
+ "bottom-right": "-right-[2px] -bottom-[2px]",
25
+ "bottom-left": "-left-[2px] -bottom-[2px]",
26
+ };
27
+
28
+ export function Indicator({
29
+ children,
30
+ badge,
31
+ color = "bg-ph-brand",
32
+ position = "top-right",
33
+ dot = false,
34
+ className,
35
+ ...props
36
+ }: IndicatorProps) {
37
+ return (
38
+ <div className={cn("relative inline-flex items-center justify-center", className)} {...props}>
39
+ {dot ? (
40
+ <span
41
+ className={cn(
42
+ "absolute z-10 h-2.5 w-2.5 rounded-full border-2 border-white",
43
+ color,
44
+ positionMap[position],
45
+ )}
46
+ />
47
+ ) : badge != null ? (
48
+ <span
49
+ className={cn(
50
+ "absolute z-10 flex min-h-[22px] min-w-[22px] items-center justify-center rounded-full px-1 text-[11px] font-bold text-white",
51
+ color,
52
+ positionMap[position],
53
+ )}
54
+ >
55
+ {badge}
56
+ </span>
57
+ ) : null}
58
+ {children}
59
+ </div>
60
+ );
61
+ }
@@ -0,0 +1,385 @@
1
+ import { forwardRef, useId, type ComponentPropsWithoutRef, type ReactNode } from "react";
2
+ import { FormField } from "@/components/ui/FormField";
3
+ import { IconCheck } from "@/components/icons";
4
+ import { cn } from "@/lib/cn";
5
+
6
+ export type InputSize = "sm" | "md" | "lg";
7
+ export type InputVariant = "default" | "ghost";
8
+
9
+ export interface InputProps extends Omit<ComponentPropsWithoutRef<"input">, "size"> {
10
+ label?: string;
11
+ hideLabel?: boolean;
12
+ error?: string;
13
+ helperText?: string;
14
+ size?: InputSize;
15
+ variant?: InputVariant;
16
+ wrapperClassName?: string;
17
+ }
18
+
19
+ const sizeMap: Record<InputSize, string> = {
20
+ sm: "ph-input-sm",
21
+ md: "",
22
+ lg: "ph-input-lg",
23
+ };
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({
31
+ id: idProp,
32
+ label,
33
+ hideLabel = false,
34
+ error,
35
+ helperText,
36
+ size = "md",
37
+ variant = "default",
38
+ className,
39
+ wrapperClassName,
40
+ disabled,
41
+ required,
42
+ "aria-describedby": ariaDescribedBy,
43
+ ...props
44
+ }, ref) {
45
+ const autoId = useId();
46
+ const id = idProp ?? autoId;
47
+ const hasError = Boolean(error);
48
+
49
+ return (
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"])}
67
+ className={cn(
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
74
+ )}
75
+ {...props}
76
+ />
77
+ )}
78
+ </FormField>
79
+ );
80
+ });
81
+
82
+ Input.displayName = "Input";
83
+
84
+ export interface SelectProps extends Omit<ComponentPropsWithoutRef<"select">, "size"> {
85
+ label?: string;
86
+ hideLabel?: boolean;
87
+ error?: string;
88
+ helperText?: string;
89
+ size?: InputSize;
90
+ wrapperClassName?: string;
91
+ children: ReactNode;
92
+ }
93
+
94
+ export const Select = forwardRef<HTMLSelectElement, SelectProps>(function Select({
95
+ id: idProp,
96
+ label,
97
+ hideLabel = false,
98
+ error,
99
+ helperText,
100
+ size = "md",
101
+ className,
102
+ wrapperClassName,
103
+ disabled,
104
+ children,
105
+ required,
106
+ "aria-describedby": ariaDescribedBy,
107
+ ...props
108
+ }, ref) {
109
+ const autoId = useId();
110
+ const id = idProp ?? autoId;
111
+ const hasError = Boolean(error);
112
+
113
+ return (
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}
130
+ >
131
+ {children}
132
+ </select>
133
+ )}
134
+ </FormField>
135
+ );
136
+ });
137
+
138
+ Select.displayName = "Select";
139
+
140
+ export interface TextareaProps extends Omit<ComponentPropsWithoutRef<"textarea">, "size"> {
141
+ label?: string;
142
+ hideLabel?: boolean;
143
+ error?: string;
144
+ helperText?: string;
145
+ size?: InputSize;
146
+ wrapperClassName?: string;
147
+ }
148
+
149
+ export const Textarea = forwardRef<HTMLTextAreaElement, TextareaProps>(function Textarea({
150
+ id: idProp,
151
+ label,
152
+ hideLabel = false,
153
+ error,
154
+ helperText,
155
+ size = "md",
156
+ className,
157
+ wrapperClassName,
158
+ disabled,
159
+ required,
160
+ "aria-describedby": ariaDescribedBy,
161
+ ...props
162
+ }, ref) {
163
+ const autoId = useId();
164
+ const id = idProp ?? autoId;
165
+ const hasError = Boolean(error);
166
+
167
+ return (
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
+ />
185
+ )}
186
+ </FormField>
187
+ );
188
+ });
189
+
190
+ Textarea.displayName = "Textarea";
191
+
192
+ export interface CheckboxProps extends Omit<ComponentPropsWithoutRef<"input">, "type"> {
193
+ label?: ReactNode;
194
+ error?: string;
195
+ helperText?: string;
196
+ wrapperClassName?: string;
197
+ }
198
+
199
+ export const Checkbox = forwardRef<HTMLInputElement, CheckboxProps>(function Checkbox({
200
+ id: idProp,
201
+ label,
202
+ error,
203
+ helperText,
204
+ className,
205
+ wrapperClassName,
206
+ disabled,
207
+ "aria-describedby": ariaDescribedBy,
208
+ ...props
209
+ }, ref) {
210
+ const autoId = useId();
211
+ const id = idProp ?? autoId;
212
+ const hasError = Boolean(error);
213
+ const descriptionId = helperText && !hasError ? `${id}-description` : undefined;
214
+ const errorId = error ? `${id}-error` : undefined;
215
+
216
+ return (
217
+ <div className={cn("ph-control-row", wrapperClassName)}>
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
+ </div>
245
+ );
246
+ });
247
+
248
+ Checkbox.displayName = "Checkbox";
249
+
250
+ export interface RadioProps extends Omit<ComponentPropsWithoutRef<"input">, "type"> {
251
+ label?: ReactNode;
252
+ wrapperClassName?: string;
253
+ }
254
+
255
+ export const Radio = forwardRef<HTMLInputElement, RadioProps>(function Radio({
256
+ id: idProp,
257
+ label,
258
+ className,
259
+ wrapperClassName,
260
+ disabled,
261
+ ...props
262
+ }, ref) {
263
+ const autoId = useId();
264
+ const id = idProp ?? autoId;
265
+
266
+ return (
267
+ <div className={cn("ph-control-row", wrapperClassName, disabled && "opacity-60")}>
268
+ <input
269
+ ref={ref}
270
+ id={id}
271
+ type="radio"
272
+ disabled={disabled}
273
+ className={cn("ph-radio", disabled && "cursor-not-allowed", className)}
274
+ {...props}
275
+ />
276
+ <label htmlFor={id} className={cn("cursor-pointer", disabled && "opacity-60")}>
277
+ {label}
278
+ </label>
279
+ </div>
280
+ );
281
+ });
282
+
283
+ Radio.displayName = "Radio";
284
+
285
+ export interface ToggleProps extends Omit<ComponentPropsWithoutRef<"input">, "type"> {
286
+ label?: ReactNode;
287
+ wrapperClassName?: string;
288
+ }
289
+
290
+ export const Toggle = forwardRef<HTMLInputElement, ToggleProps>(function Toggle({
291
+ id: idProp,
292
+ label,
293
+ className,
294
+ wrapperClassName,
295
+ disabled,
296
+ ...props
297
+ }, ref) {
298
+ const autoId = useId();
299
+ const id = idProp ?? autoId;
300
+
301
+ return (
302
+ <label className={cn("ph-control-row-between", wrapperClassName, disabled && "opacity-60")}>
303
+ <span className={disabled ? "opacity-60" : undefined}>{label}</span>
304
+ <input
305
+ ref={ref}
306
+ id={id}
307
+ type="checkbox"
308
+ role="switch"
309
+ disabled={disabled}
310
+ className={cn("ph-toggle", disabled && "cursor-not-allowed", className)}
311
+ {...props}
312
+ />
313
+ </label>
314
+ );
315
+ });
316
+
317
+ Toggle.displayName = "Toggle";
318
+
319
+ export interface RangeProps extends Omit<ComponentPropsWithoutRef<"input">, "type"> {
320
+ label?: string;
321
+ minLabel?: string;
322
+ maxLabel?: string;
323
+ valueLabel?: string;
324
+ wrapperClassName?: string;
325
+ }
326
+
327
+ export const Range = forwardRef<HTMLInputElement, RangeProps>(function Range({
328
+ id: idProp,
329
+ label,
330
+ minLabel,
331
+ maxLabel,
332
+ valueLabel,
333
+ wrapperClassName,
334
+ className,
335
+ ...props
336
+ }, ref) {
337
+ const autoId = useId();
338
+ const id = idProp ?? autoId;
339
+
340
+ return (
341
+ <div className={cn("ph-field", wrapperClassName)}>
342
+ {label && <label htmlFor={id} className="ph-label">{label}</label>}
343
+ <input ref={ref} id={id} type="range" className={cn("ph-range", className)} {...props} />
344
+ {(minLabel || valueLabel || maxLabel) && (
345
+ <div className="flex justify-between text-xs text-ph-mutedtext">
346
+ <span>{minLabel}</span>
347
+ <span>{valueLabel}</span>
348
+ <span>{maxLabel}</span>
349
+ </div>
350
+ )}
351
+ </div>
352
+ );
353
+ });
354
+
355
+ Range.displayName = "Range";
356
+
357
+ export interface FileUploadProps extends Omit<ComponentPropsWithoutRef<"input">, "type"> {
358
+ label?: string;
359
+ prompt?: string;
360
+ wrapperClassName?: string;
361
+ }
362
+
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
+
374
+ return (
375
+ <div className={cn("ph-field", wrapperClassName)}>
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} />
379
+ {prompt}
380
+ </label>
381
+ </div>
382
+ );
383
+ });
384
+
385
+ FileUpload.displayName = "FileUpload";
@@ -0,0 +1,113 @@
1
+ import type { HTMLAttributes, ReactNode } from "react";
2
+ import { cn } from "@/lib/cn";
3
+
4
+ export type KbdVariant = "shortcut" | "key" | "token";
5
+ export type KbdPlatform = "text" | "mac" | "windows";
6
+
7
+ export interface KbdProps extends HTMLAttributes<HTMLElement> {
8
+ variant?: KbdVariant;
9
+ keys?: readonly ReactNode[];
10
+ platform?: KbdPlatform;
11
+ separator?: ReactNode;
12
+ children?: ReactNode;
13
+ }
14
+
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) {
66
+ if (variant === "token") {
67
+ return (
68
+ <code className={cn("ph-code-token ph-kbd", className)} aria-label={ariaLabel} {...props}>
69
+ {children}
70
+ </code>
71
+ );
72
+ }
73
+
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
+ }
85
+
86
+ return (
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
+ ))}
94
+ </kbd>
95
+ );
96
+ }
97
+
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
+ });
109
+
110
+ return labels.every((label): label is string => label != null)
111
+ ? labels.join(" + ")
112
+ : undefined;
113
+ }