@xenide-io/the-old-ui-theme 0.2.5 → 0.2.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.
Files changed (112) hide show
  1. package/dist/chunk-FWCSY2DS.mjs +37 -0
  2. package/dist/chunk-SHF57J7U.mjs +2910 -0
  3. package/dist/index-CmS6hcUk.d.mts +753 -0
  4. package/dist/index-CmS6hcUk.d.ts +753 -0
  5. package/dist/index.d.mts +5 -755
  6. package/dist/index.d.ts +5 -755
  7. package/dist/index.js +655 -378
  8. package/dist/index.mjs +348 -2695
  9. package/dist/tailwind-preset.mjs +2 -0
  10. package/dist/ui.d.mts +3 -0
  11. package/dist/ui.d.ts +3 -0
  12. package/dist/ui.js +2946 -0
  13. package/dist/ui.mjs +139 -0
  14. package/package.json +13 -5
  15. package/src/components/charts/index.ts +8 -0
  16. package/src/components/charts/ph-insight-charts.tsx +162 -0
  17. package/src/components/dashboard/DashboardFiltersBar.tsx +19 -0
  18. package/src/components/dashboard/DashboardGrid.tsx +23 -0
  19. package/src/components/dashboard/DashboardInsightPlaceholder.tsx +37 -0
  20. package/src/components/dashboard/DashboardKpiCard.tsx +25 -0
  21. package/src/components/dashboard/DashboardToolbar.tsx +23 -0
  22. package/src/components/dashboard/DashboardWell.tsx +10 -0
  23. package/src/components/dashboard/InsightMiniCard.tsx +26 -0
  24. package/src/components/dashboard/InsightShell.tsx +37 -0
  25. package/src/components/dashboard/InsightShellHeader.tsx +22 -0
  26. package/src/components/dashboard/MiniTrendBars.tsx +51 -0
  27. package/src/components/dashboard/index.ts +31 -0
  28. package/src/components/dashboard/types.ts +9 -0
  29. package/src/components/icons/IconBase.tsx +39 -0
  30. package/src/components/icons/createIconBase.tsx +27 -0
  31. package/src/components/icons/icons.tsx +367 -0
  32. package/src/components/icons/index.ts +3 -0
  33. package/src/components/sections/AccordionShowcase.tsx +45 -0
  34. package/src/components/sections/AlertShowcase.tsx +39 -0
  35. package/src/components/sections/AvatarShowcase.tsx +80 -0
  36. package/src/components/sections/BadgeShowcase.tsx +65 -0
  37. package/src/components/sections/ButtonShowcase.tsx +312 -0
  38. package/src/components/sections/CalendarShowcase.tsx +35 -0
  39. package/src/components/sections/CardShowcase.tsx +143 -0
  40. package/src/components/sections/ChartShowcase.tsx +190 -0
  41. package/src/components/sections/CodeMockupShowcase.tsx +56 -0
  42. package/src/components/sections/ColorPalette.tsx +117 -0
  43. package/src/components/sections/CommandPaletteShowcase.tsx +48 -0
  44. package/src/components/sections/CountdownShowcase.tsx +43 -0
  45. package/src/components/sections/DashboardShowcase.tsx +191 -0
  46. package/src/components/sections/DiffShowcase.tsx +70 -0
  47. package/src/components/sections/DrawerShowcase.tsx +97 -0
  48. package/src/components/sections/DropdownShowcase.tsx +100 -0
  49. package/src/components/sections/EmptyStateShowcase.tsx +50 -0
  50. package/src/components/sections/FilterChipsShowcase.tsx +50 -0
  51. package/src/components/sections/FormShowcase.tsx +126 -0
  52. package/src/components/sections/HoverCardShowcase.tsx +50 -0
  53. package/src/components/sections/IconShowcase.tsx +247 -0
  54. package/src/components/sections/IndicatorShowcase.tsx +52 -0
  55. package/src/components/sections/KbdShowcase.tsx +31 -0
  56. package/src/components/sections/ModalShowcase.tsx +210 -0
  57. package/src/components/sections/NavigationShowcase.tsx +199 -0
  58. package/src/components/sections/ProgressShowcase.tsx +85 -0
  59. package/src/components/sections/SegmentedControlShowcase.tsx +49 -0
  60. package/src/components/sections/SetupThemeShowcase.tsx +262 -0
  61. package/src/components/sections/StackShowcase.tsx +33 -0
  62. package/src/components/sections/StatShowcase.tsx +129 -0
  63. package/src/components/sections/StepperShowcase.tsx +37 -0
  64. package/src/components/sections/SwapShowcase.tsx +91 -0
  65. package/src/components/sections/TabShowcase.tsx +84 -0
  66. package/src/components/sections/TableShowcase.tsx +140 -0
  67. package/src/components/sections/TimelineShowcase.tsx +83 -0
  68. package/src/components/sections/ToastShowcase.tsx +108 -0
  69. package/src/components/sections/TooltipShowcase.tsx +44 -0
  70. package/src/components/sections/UtilityShowcase.tsx +81 -0
  71. package/src/components/ui/Accordion.tsx +77 -0
  72. package/src/components/ui/Alert.tsx +68 -0
  73. package/src/components/ui/Avatar.tsx +97 -0
  74. package/src/components/ui/Badge.tsx +55 -0
  75. package/src/components/ui/Button.tsx +110 -0
  76. package/src/components/ui/ButtonChrome.tsx +20 -0
  77. package/src/components/ui/Calendar.tsx +116 -0
  78. package/src/components/ui/Card.tsx +60 -0
  79. package/src/components/ui/ChatBubble.tsx +71 -0
  80. package/src/components/ui/CodeBlock.tsx +48 -0
  81. package/src/components/ui/CollapsibleSection.tsx +49 -0
  82. package/src/components/ui/CommandPalette.tsx +137 -0
  83. package/src/components/ui/ComponentDocs.tsx +52 -0
  84. package/src/components/ui/Drawer.tsx +71 -0
  85. package/src/components/ui/DropdownMenu.tsx +138 -0
  86. package/src/components/ui/EmptyState.tsx +33 -0
  87. package/src/components/ui/FilterChips.tsx +50 -0
  88. package/src/components/ui/HoverCard.tsx +36 -0
  89. package/src/components/ui/Indicator.tsx +61 -0
  90. package/src/components/ui/Input.tsx +359 -0
  91. package/src/components/ui/Kbd.tsx +38 -0
  92. package/src/components/ui/Modal.tsx +75 -0
  93. package/src/components/ui/Navigation.tsx +94 -0
  94. package/src/components/ui/Panel.tsx +18 -0
  95. package/src/components/ui/Progress.tsx +59 -0
  96. package/src/components/ui/Rating.tsx +65 -0
  97. package/src/components/ui/SearchInput.tsx +106 -0
  98. package/src/components/ui/SegmentedControl.tsx +44 -0
  99. package/src/components/ui/ShowcaseWrapper.tsx +136 -0
  100. package/src/components/ui/Stat.tsx +39 -0
  101. package/src/components/ui/Stepper.tsx +75 -0
  102. package/src/components/ui/Table.tsx +55 -0
  103. package/src/components/ui/Tabs.tsx +53 -0
  104. package/src/components/ui/Terminal.tsx +54 -0
  105. package/src/components/ui/ThemeDomSync.tsx +17 -0
  106. package/src/components/ui/ThemeManager.tsx +18 -0
  107. package/src/components/ui/ThemeSwitcher.tsx +46 -0
  108. package/src/components/ui/Timeline.tsx +60 -0
  109. package/src/components/ui/Toast.tsx +70 -0
  110. package/src/components/ui/Typography.tsx +129 -0
  111. package/src/components/ui/index.ts +92 -0
  112. package/src/styles/themes.css +118 -0
@@ -0,0 +1,50 @@
1
+ "use client";
2
+
3
+ import { cn } from "@/lib/cn";
4
+ import { IconClose } from "@/components/icons";
5
+
6
+ interface FilterChip {
7
+ id: string;
8
+ label: string;
9
+ active?: boolean;
10
+ }
11
+
12
+ interface FilterChipsProps {
13
+ chips: FilterChip[];
14
+ onToggle: (id: string) => void;
15
+ onRemove: (id: string) => void;
16
+ className?: string;
17
+ }
18
+
19
+ export function FilterChips({ chips, onToggle, onRemove, className }: FilterChipsProps) {
20
+ return (
21
+ <div className={cn("flex flex-wrap gap-2", className)}>
22
+ {chips.map((chip) => (
23
+ <button
24
+ key={chip.id}
25
+ type="button"
26
+ onClick={() => onToggle(chip.id)}
27
+ className={cn(
28
+ "inline-flex items-center gap-1.5 rounded-full px-3 py-1.5 text-sm font-medium transition-colors",
29
+ chip.active
30
+ ? "bg-ph-brand text-white"
31
+ : "bg-ph-muted text-ph-subtle hover:bg-ph-border hover:text-ph-ink"
32
+ )}
33
+ >
34
+ {chip.label}
35
+ {chip.active && (
36
+ <span
37
+ onClick={(e) => {
38
+ e.stopPropagation();
39
+ onRemove(chip.id);
40
+ }}
41
+ className="ml-0.5 flex h-4 w-4 cursor-pointer items-center justify-center rounded-full hover:bg-white/20"
42
+ >
43
+ <IconClose className="h-3 w-3" />
44
+ </span>
45
+ )}
46
+ </button>
47
+ ))}
48
+ </div>
49
+ );
50
+ }
@@ -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,359 @@
1
+ import type { ComponentPropsWithoutRef, ReactNode } from "react";
2
+ import { useId } from "react";
3
+ import { cn } from "@/lib/cn";
4
+
5
+ export type InputSize = "sm" | "md" | "lg";
6
+ export type InputVariant = "default" | "ghost";
7
+
8
+ export interface InputProps extends Omit<ComponentPropsWithoutRef<"input">, "size"> {
9
+ label?: string;
10
+ hideLabel?: boolean;
11
+ error?: string;
12
+ helperText?: string;
13
+ size?: InputSize;
14
+ variant?: InputVariant;
15
+ wrapperClassName?: string;
16
+ }
17
+
18
+ const sizeMap: Record<InputSize, string> = {
19
+ sm: "ph-input-sm",
20
+ md: "",
21
+ lg: "ph-input-lg",
22
+ };
23
+
24
+ export function Input({
25
+ id: idProp,
26
+ label,
27
+ hideLabel = false,
28
+ error,
29
+ helperText,
30
+ size = "md",
31
+ variant = "default",
32
+ className,
33
+ wrapperClassName,
34
+ disabled,
35
+ ...props
36
+ }: InputProps) {
37
+ const autoId = useId();
38
+ const id = idProp ?? autoId;
39
+ const hasError = Boolean(error);
40
+
41
+ return (
42
+ <div className={cn("ph-field", wrapperClassName)}>
43
+ {label && (
44
+ <label
45
+ htmlFor={id}
46
+ className={cn(
47
+ "ph-label",
48
+ hideLabel && "sr-only",
49
+ disabled && "opacity-50"
50
+ )}
51
+ >
52
+ {label}
53
+ </label>
54
+ )}
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>
76
+ );
77
+ }
78
+
79
+ export interface SelectProps extends Omit<ComponentPropsWithoutRef<"select">, "size"> {
80
+ label?: string;
81
+ hideLabel?: boolean;
82
+ error?: string;
83
+ helperText?: string;
84
+ size?: InputSize;
85
+ wrapperClassName?: string;
86
+ children: ReactNode;
87
+ }
88
+
89
+ export function Select({
90
+ id: idProp,
91
+ label,
92
+ hideLabel = false,
93
+ error,
94
+ helperText,
95
+ size = "md",
96
+ className,
97
+ wrapperClassName,
98
+ disabled,
99
+ children,
100
+ ...props
101
+ }: SelectProps) {
102
+ const autoId = useId();
103
+ const id = idProp ?? autoId;
104
+ const hasError = Boolean(error);
105
+
106
+ 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")}
112
+ >
113
+ {label}
114
+ </label>
115
+ )}
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>
138
+ );
139
+ }
140
+
141
+ export interface TextareaProps extends Omit<ComponentPropsWithoutRef<"textarea">, "size"> {
142
+ label?: string;
143
+ hideLabel?: boolean;
144
+ error?: string;
145
+ helperText?: string;
146
+ size?: InputSize;
147
+ wrapperClassName?: string;
148
+ }
149
+
150
+ export function Textarea({
151
+ id: idProp,
152
+ label,
153
+ hideLabel = false,
154
+ error,
155
+ helperText,
156
+ size = "md",
157
+ className,
158
+ wrapperClassName,
159
+ disabled,
160
+ ...props
161
+ }: TextareaProps) {
162
+ const autoId = useId();
163
+ const id = idProp ?? autoId;
164
+ const hasError = Boolean(error);
165
+
166
+ 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>
194
+ )}
195
+ </div>
196
+ );
197
+ }
198
+
199
+ export interface CheckboxProps extends Omit<ComponentPropsWithoutRef<"input">, "type"> {
200
+ label?: ReactNode;
201
+ error?: string;
202
+ helperText?: string;
203
+ wrapperClassName?: string;
204
+ }
205
+
206
+ export function Checkbox({
207
+ id: idProp,
208
+ label,
209
+ error,
210
+ helperText,
211
+ className,
212
+ wrapperClassName,
213
+ disabled,
214
+ ...props
215
+ }: CheckboxProps) {
216
+ const autoId = useId();
217
+ const id = idProp ?? autoId;
218
+ const hasError = Boolean(error);
219
+
220
+ return (
221
+ <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
+ )}
244
+ </div>
245
+ );
246
+ }
247
+
248
+ export interface RadioProps extends Omit<ComponentPropsWithoutRef<"input">, "type"> {
249
+ label?: ReactNode;
250
+ wrapperClassName?: string;
251
+ }
252
+
253
+ export function Radio({
254
+ id: idProp,
255
+ label,
256
+ className,
257
+ wrapperClassName,
258
+ disabled,
259
+ ...props
260
+ }: RadioProps) {
261
+ const autoId = useId();
262
+ const id = idProp ?? autoId;
263
+
264
+ return (
265
+ <div className={cn("ph-control-row", wrapperClassName, disabled && "opacity-60")}>
266
+ <input
267
+ id={id}
268
+ type="radio"
269
+ disabled={disabled}
270
+ className={cn("ph-radio", disabled && "cursor-not-allowed", className)}
271
+ {...props}
272
+ />
273
+ <label htmlFor={id} className={cn("cursor-pointer", disabled && "opacity-60")}>
274
+ {label}
275
+ </label>
276
+ </div>
277
+ );
278
+ }
279
+
280
+ export interface ToggleProps extends Omit<ComponentPropsWithoutRef<"input">, "type"> {
281
+ label?: ReactNode;
282
+ wrapperClassName?: string;
283
+ }
284
+
285
+ export function Toggle({
286
+ id: idProp,
287
+ label,
288
+ className,
289
+ wrapperClassName,
290
+ disabled,
291
+ ...props
292
+ }: ToggleProps) {
293
+ const autoId = useId();
294
+ const id = idProp ?? autoId;
295
+
296
+ return (
297
+ <label className={cn("ph-control-row-between", wrapperClassName, disabled && "opacity-60")}>
298
+ <span className={disabled ? "opacity-60" : undefined}>{label}</span>
299
+ <input
300
+ id={id}
301
+ type="checkbox"
302
+ role="switch"
303
+ disabled={disabled}
304
+ className={cn("ph-toggle", disabled && "cursor-not-allowed", className)}
305
+ {...props}
306
+ />
307
+ </label>
308
+ );
309
+ }
310
+
311
+ export interface RangeProps extends Omit<ComponentPropsWithoutRef<"input">, "type"> {
312
+ label?: string;
313
+ minLabel?: string;
314
+ maxLabel?: string;
315
+ valueLabel?: string;
316
+ wrapperClassName?: string;
317
+ }
318
+
319
+ export function Range({
320
+ label,
321
+ minLabel,
322
+ maxLabel,
323
+ valueLabel,
324
+ wrapperClassName,
325
+ className,
326
+ ...props
327
+ }: RangeProps) {
328
+ return (
329
+ <div className={cn("ph-field", wrapperClassName)}>
330
+ {label && <label className="ph-label">{label}</label>}
331
+ <input type="range" className={cn("ph-range", className)} {...props} />
332
+ {(minLabel || valueLabel || maxLabel) && (
333
+ <div className="flex justify-between text-xs text-ph-mutedtext">
334
+ <span>{minLabel}</span>
335
+ <span>{valueLabel}</span>
336
+ <span>{maxLabel}</span>
337
+ </div>
338
+ )}
339
+ </div>
340
+ );
341
+ }
342
+
343
+ export interface FileUploadProps extends Omit<ComponentPropsWithoutRef<"input">, "type"> {
344
+ label?: string;
345
+ prompt?: string;
346
+ wrapperClassName?: string;
347
+ }
348
+
349
+ export function FileUpload({ label, prompt = "Drop file or browse", wrapperClassName, className, ...props }: FileUploadProps) {
350
+ return (
351
+ <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} />
355
+ {prompt}
356
+ </label>
357
+ </div>
358
+ );
359
+ }
@@ -0,0 +1,38 @@
1
+ import type { HTMLAttributes, ReactNode } from "react";
2
+ import { cn } from "@/lib/cn";
3
+
4
+ export type KbdVariant = "shortcut" | "key" | "token";
5
+
6
+ export interface KbdProps extends HTMLAttributes<HTMLElement> {
7
+ variant?: KbdVariant;
8
+ keys?: ReactNode[];
9
+ children?: ReactNode;
10
+ }
11
+
12
+ export function Kbd({ variant = "shortcut", keys, children, className, ...props }: KbdProps) {
13
+ if (variant === "token") {
14
+ return (
15
+ <code className={cn("ph-kbd text-ph-ink", className)} {...props}>
16
+ {children}
17
+ </code>
18
+ );
19
+ }
20
+
21
+ const content = keys?.length ? keys.join(" ") : normalizeShortcut(children);
22
+
23
+ return (
24
+ <kbd className={cn("ph-shortcut", className)} {...props}>
25
+ {content}
26
+ </kbd>
27
+ );
28
+ }
29
+
30
+ function normalizeShortcut(children: ReactNode) {
31
+ if (typeof children !== "string") return children;
32
+
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, "⌃");
38
+ }
@@ -0,0 +1,75 @@
1
+ import type { ReactNode } from "react";
2
+ import { IconClose } from "@/components/icons";
3
+ import { cn } from "@/lib/cn";
4
+
5
+ export type ModalSize = "sm" | "md" | "lg" | "xl" | "full";
6
+
7
+ export interface ModalProps {
8
+ open?: boolean;
9
+ onClose?: () => void;
10
+ title?: string;
11
+ description?: string;
12
+ children?: ReactNode;
13
+ footer?: ReactNode;
14
+ size?: ModalSize;
15
+ className?: string;
16
+ showCloseButton?: boolean;
17
+ }
18
+
19
+ const sizeMap: Record<ModalSize, string> = {
20
+ sm: "max-w-sm",
21
+ md: "max-w-lg",
22
+ lg: "max-w-2xl",
23
+ xl: "max-w-4xl",
24
+ full: "max-w-[calc(100%-2rem)]",
25
+ };
26
+
27
+ export function Modal({
28
+ open = false,
29
+ onClose,
30
+ title,
31
+ description,
32
+ children,
33
+ footer,
34
+ size = "md",
35
+ className,
36
+ showCloseButton = true,
37
+ }: ModalProps) {
38
+ if (!open) return null;
39
+
40
+ return (
41
+ <div className="ph-modal-overlay" role="presentation" onClick={onClose}>
42
+ <div
43
+ role="dialog"
44
+ aria-modal
45
+ className={cn(
46
+ "ph-modal-panel",
47
+ sizeMap[size],
48
+ className
49
+ )}
50
+ onClick={(e) => e.stopPropagation()}
51
+ >
52
+ {(title || showCloseButton) && (
53
+ <header className="flex items-start justify-between gap-4">
54
+ <div>
55
+ {title && <h3 className="text-lg font-bold text-ph-ink">{title}</h3>}
56
+ {description && <p className="mt-1 text-sm text-ph-subtle">{description}</p>}
57
+ </div>
58
+ {showCloseButton && onClose && (
59
+ <button
60
+ type="button"
61
+ onClick={onClose}
62
+ className="shrink-0 rounded-md p-1 text-ph-subtle hover:bg-black/5 hover:text-ph-ink"
63
+ aria-label="Close"
64
+ >
65
+ <IconClose className="h-5 w-5" />
66
+ </button>
67
+ )}
68
+ </header>
69
+ )}
70
+ <div className="mt-4">{children}</div>
71
+ {footer && <div className="mt-6 flex justify-end gap-2 border-t border-ph-border pt-5">{footer}</div>}
72
+ </div>
73
+ </div>
74
+ );
75
+ }