@xenide-io/the-old-ui-theme 0.2.6 → 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.
- package/dist/chunk-SHF57J7U.mjs +2910 -0
- package/dist/index-CmS6hcUk.d.mts +753 -0
- package/dist/index-CmS6hcUk.d.ts +753 -0
- package/dist/index.d.mts +5 -755
- package/dist/index.d.ts +5 -755
- package/dist/index.js +43 -23
- package/dist/index.mjs +252 -2846
- package/dist/ui.d.mts +3 -0
- package/dist/ui.d.ts +3 -0
- package/dist/ui.js +2946 -0
- package/dist/ui.mjs +139 -0
- package/package.json +11 -3
- package/src/components/charts/index.ts +8 -0
- package/src/components/charts/ph-insight-charts.tsx +162 -0
- package/src/components/dashboard/DashboardFiltersBar.tsx +19 -0
- package/src/components/dashboard/DashboardGrid.tsx +23 -0
- package/src/components/dashboard/DashboardInsightPlaceholder.tsx +37 -0
- package/src/components/dashboard/DashboardKpiCard.tsx +25 -0
- package/src/components/dashboard/DashboardToolbar.tsx +23 -0
- package/src/components/dashboard/DashboardWell.tsx +10 -0
- package/src/components/dashboard/InsightMiniCard.tsx +26 -0
- package/src/components/dashboard/InsightShell.tsx +37 -0
- package/src/components/dashboard/InsightShellHeader.tsx +22 -0
- package/src/components/dashboard/MiniTrendBars.tsx +51 -0
- package/src/components/dashboard/index.ts +31 -0
- package/src/components/dashboard/types.ts +9 -0
- package/src/components/icons/IconBase.tsx +39 -0
- package/src/components/icons/createIconBase.tsx +27 -0
- package/src/components/icons/icons.tsx +367 -0
- package/src/components/icons/index.ts +3 -0
- package/src/components/sections/AccordionShowcase.tsx +45 -0
- package/src/components/sections/AlertShowcase.tsx +39 -0
- package/src/components/sections/AvatarShowcase.tsx +80 -0
- package/src/components/sections/BadgeShowcase.tsx +65 -0
- package/src/components/sections/ButtonShowcase.tsx +312 -0
- package/src/components/sections/CalendarShowcase.tsx +35 -0
- package/src/components/sections/CardShowcase.tsx +143 -0
- package/src/components/sections/ChartShowcase.tsx +190 -0
- package/src/components/sections/CodeMockupShowcase.tsx +56 -0
- package/src/components/sections/ColorPalette.tsx +117 -0
- package/src/components/sections/CommandPaletteShowcase.tsx +48 -0
- package/src/components/sections/CountdownShowcase.tsx +43 -0
- package/src/components/sections/DashboardShowcase.tsx +191 -0
- package/src/components/sections/DiffShowcase.tsx +70 -0
- package/src/components/sections/DrawerShowcase.tsx +97 -0
- package/src/components/sections/DropdownShowcase.tsx +100 -0
- package/src/components/sections/EmptyStateShowcase.tsx +50 -0
- package/src/components/sections/FilterChipsShowcase.tsx +50 -0
- package/src/components/sections/FormShowcase.tsx +126 -0
- package/src/components/sections/HoverCardShowcase.tsx +50 -0
- package/src/components/sections/IconShowcase.tsx +247 -0
- package/src/components/sections/IndicatorShowcase.tsx +52 -0
- package/src/components/sections/KbdShowcase.tsx +31 -0
- package/src/components/sections/ModalShowcase.tsx +210 -0
- package/src/components/sections/NavigationShowcase.tsx +199 -0
- package/src/components/sections/ProgressShowcase.tsx +85 -0
- package/src/components/sections/SegmentedControlShowcase.tsx +49 -0
- package/src/components/sections/SetupThemeShowcase.tsx +262 -0
- package/src/components/sections/StackShowcase.tsx +33 -0
- package/src/components/sections/StatShowcase.tsx +129 -0
- package/src/components/sections/StepperShowcase.tsx +37 -0
- package/src/components/sections/SwapShowcase.tsx +91 -0
- package/src/components/sections/TabShowcase.tsx +84 -0
- package/src/components/sections/TableShowcase.tsx +140 -0
- package/src/components/sections/TimelineShowcase.tsx +83 -0
- package/src/components/sections/ToastShowcase.tsx +108 -0
- package/src/components/sections/TooltipShowcase.tsx +44 -0
- package/src/components/sections/UtilityShowcase.tsx +81 -0
- package/src/components/ui/Accordion.tsx +77 -0
- package/src/components/ui/Alert.tsx +68 -0
- package/src/components/ui/Avatar.tsx +97 -0
- package/src/components/ui/Badge.tsx +55 -0
- package/src/components/ui/Button.tsx +110 -0
- package/src/components/ui/ButtonChrome.tsx +20 -0
- package/src/components/ui/Calendar.tsx +116 -0
- package/src/components/ui/Card.tsx +60 -0
- package/src/components/ui/ChatBubble.tsx +71 -0
- package/src/components/ui/CodeBlock.tsx +48 -0
- package/src/components/ui/CollapsibleSection.tsx +49 -0
- package/src/components/ui/CommandPalette.tsx +137 -0
- package/src/components/ui/ComponentDocs.tsx +52 -0
- package/src/components/ui/Drawer.tsx +71 -0
- package/src/components/ui/DropdownMenu.tsx +138 -0
- package/src/components/ui/EmptyState.tsx +33 -0
- package/src/components/ui/FilterChips.tsx +50 -0
- package/src/components/ui/HoverCard.tsx +36 -0
- package/src/components/ui/Indicator.tsx +61 -0
- package/src/components/ui/Input.tsx +359 -0
- package/src/components/ui/Kbd.tsx +38 -0
- package/src/components/ui/Modal.tsx +75 -0
- package/src/components/ui/Navigation.tsx +94 -0
- package/src/components/ui/Panel.tsx +18 -0
- package/src/components/ui/Progress.tsx +59 -0
- package/src/components/ui/Rating.tsx +65 -0
- package/src/components/ui/SearchInput.tsx +106 -0
- package/src/components/ui/SegmentedControl.tsx +44 -0
- package/src/components/ui/ShowcaseWrapper.tsx +136 -0
- package/src/components/ui/Stat.tsx +39 -0
- package/src/components/ui/Stepper.tsx +75 -0
- package/src/components/ui/Table.tsx +55 -0
- package/src/components/ui/Tabs.tsx +53 -0
- package/src/components/ui/Terminal.tsx +54 -0
- package/src/components/ui/ThemeDomSync.tsx +17 -0
- package/src/components/ui/ThemeManager.tsx +18 -0
- package/src/components/ui/ThemeSwitcher.tsx +46 -0
- package/src/components/ui/Timeline.tsx +60 -0
- package/src/components/ui/Toast.tsx +70 -0
- package/src/components/ui/Typography.tsx +129 -0
- package/src/components/ui/index.ts +92 -0
- 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
|
+
}
|