@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.
- package/README.md +1 -1
- package/dist/chunk-FDMD3IIN.mjs +3831 -0
- package/dist/index-D1RTT2Ap.d.mts +1044 -0
- package/dist/index-D1RTT2Ap.d.ts +1044 -0
- package/dist/index.d.mts +11 -758
- package/dist/index.d.ts +11 -758
- package/dist/index.js +1749 -768
- package/dist/index.mjs +312 -2846
- package/dist/tailwind-preset.js +7 -0
- package/dist/tailwind-preset.mjs +7 -0
- package/dist/ui.d.mts +4 -0
- package/dist/ui.d.ts +4 -0
- package/dist/ui.js +3864 -0
- package/dist/ui.mjs +169 -0
- package/package.json +28 -6
- package/src/app/globals.css +1005 -227
- 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 +47 -0
- package/src/components/icons/createIconBase.tsx +32 -0
- package/src/components/icons/icons.tsx +557 -0
- package/src/components/icons/index.ts +3 -0
- package/src/components/sections/AccordionShowcase.tsx +45 -0
- package/src/components/sections/AlertShowcase.tsx +40 -0
- package/src/components/sections/AvatarShowcase.tsx +80 -0
- package/src/components/sections/BadgeShowcase.tsx +65 -0
- package/src/components/sections/ButtonShowcase.tsx +308 -0
- package/src/components/sections/CalendarShowcase.tsx +35 -0
- package/src/components/sections/CardShowcase.tsx +159 -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 +98 -0
- package/src/components/sections/EmptyStateShowcase.tsx +50 -0
- package/src/components/sections/FilterChipsShowcase.tsx +98 -0
- package/src/components/sections/FormShowcase.tsx +127 -0
- package/src/components/sections/HoverCardShowcase.tsx +50 -0
- package/src/components/sections/IconShowcase.tsx +127 -0
- package/src/components/sections/IndicatorShowcase.tsx +52 -0
- package/src/components/sections/KbdShowcase.tsx +57 -0
- package/src/components/sections/ModalShowcase.tsx +211 -0
- package/src/components/sections/NavigationShowcase.tsx +199 -0
- package/src/components/sections/ProductLayoutsShowcase.tsx +138 -0
- package/src/components/sections/ProgressShowcase.tsx +82 -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 +99 -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 +77 -0
- package/src/components/ui/AuthLayout.tsx +58 -0
- package/src/components/ui/Avatar.tsx +97 -0
- package/src/components/ui/Badge.tsx +60 -0
- package/src/components/ui/Button.tsx +120 -0
- package/src/components/ui/ButtonChrome.tsx +20 -0
- package/src/components/ui/Calendar.tsx +115 -0
- package/src/components/ui/Card.tsx +70 -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 +262 -0
- package/src/components/ui/EmptyState.tsx +33 -0
- package/src/components/ui/FilterBar.tsx +165 -0
- package/src/components/ui/FilterChips.tsx +80 -0
- package/src/components/ui/FilterControls.tsx +30 -0
- package/src/components/ui/FormField.tsx +69 -0
- package/src/components/ui/HoverCard.tsx +36 -0
- package/src/components/ui/Indicator.tsx +61 -0
- package/src/components/ui/Input.tsx +385 -0
- package/src/components/ui/Kbd.tsx +113 -0
- package/src/components/ui/Loader.tsx +96 -0
- package/src/components/ui/Modal.tsx +105 -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/SettingsLayout.tsx +94 -0
- package/src/components/ui/ShowcaseWrapper.tsx +124 -0
- package/src/components/ui/Stat.tsx +39 -0
- package/src/components/ui/Stepper.tsx +74 -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/first-slice.test.tsx +98 -0
- package/src/components/ui/index.ts +151 -0
- package/src/components/ui/interaction-primitives.test.tsx +77 -0
- package/src/components/ui/kbd-icons.test.tsx +90 -0
- package/src/components/ui/product-patterns.test.tsx +77 -0
- package/src/styles/themes.css +528 -0
- 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
|
+
}
|