@xenide-io/the-old-ui-theme 0.2.9 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/dist/{chunk-SHF57J7U.mjs → chunk-FDMD3IIN.mjs} +1573 -652
- package/dist/index-D1RTT2Ap.d.mts +1044 -0
- package/dist/index-D1RTT2Ap.d.ts +1044 -0
- package/dist/index.d.mts +10 -7
- package/dist/index.d.ts +10 -7
- package/dist/index.js +1705 -744
- package/dist/index.mjs +61 -1
- package/dist/tailwind-preset.js +7 -0
- package/dist/tailwind-preset.mjs +7 -0
- package/dist/ui.d.mts +3 -2
- package/dist/ui.d.ts +3 -2
- package/dist/ui.js +1584 -666
- package/dist/ui.mjs +31 -1
- package/package.json +18 -4
- package/src/app/globals.css +1005 -227
- package/src/components/icons/IconBase.tsx +20 -12
- package/src/components/icons/createIconBase.tsx +12 -7
- package/src/components/icons/icons.tsx +208 -18
- package/src/components/sections/AlertShowcase.tsx +2 -1
- package/src/components/sections/BadgeShowcase.tsx +1 -1
- package/src/components/sections/ButtonShowcase.tsx +6 -10
- package/src/components/sections/CardShowcase.tsx +22 -6
- package/src/components/sections/DropdownShowcase.tsx +4 -6
- package/src/components/sections/FilterChipsShowcase.tsx +74 -26
- package/src/components/sections/FormShowcase.tsx +2 -1
- package/src/components/sections/IconShowcase.tsx +86 -206
- package/src/components/sections/KbdShowcase.tsx +47 -21
- package/src/components/sections/ModalShowcase.tsx +7 -6
- package/src/components/sections/ProductLayoutsShowcase.tsx +138 -0
- package/src/components/sections/ProgressShowcase.tsx +50 -53
- package/src/components/sections/SwapShowcase.tsx +13 -5
- package/src/components/ui/Alert.tsx +28 -19
- package/src/components/ui/AuthLayout.tsx +58 -0
- package/src/components/ui/Badge.tsx +10 -5
- package/src/components/ui/Button.tsx +22 -12
- package/src/components/ui/ButtonChrome.tsx +1 -1
- package/src/components/ui/Calendar.tsx +6 -7
- package/src/components/ui/Card.tsx +21 -11
- package/src/components/ui/CollapsibleSection.tsx +11 -11
- package/src/components/ui/DropdownMenu.tsx +189 -65
- package/src/components/ui/FilterBar.tsx +165 -0
- package/src/components/ui/FilterChips.tsx +65 -35
- package/src/components/ui/FilterControls.tsx +30 -0
- package/src/components/ui/FormField.tsx +69 -0
- package/src/components/ui/Input.tsx +166 -140
- package/src/components/ui/Kbd.tsx +88 -13
- package/src/components/ui/Loader.tsx +96 -0
- package/src/components/ui/Modal.tsx +66 -36
- package/src/components/ui/SettingsLayout.tsx +94 -0
- package/src/components/ui/ShowcaseWrapper.tsx +4 -16
- package/src/components/ui/Stepper.tsx +5 -6
- package/src/components/ui/first-slice.test.tsx +98 -0
- package/src/components/ui/index.ts +62 -3
- 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 +411 -1
- package/tailwind-preset.ts +7 -0
- package/dist/index-CmS6hcUk.d.mts +0 -753
- package/dist/index-CmS6hcUk.d.ts +0 -753
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
|
|
3
3
|
import { useState } from "react";
|
|
4
|
+
import { IconChevronLeft, IconChevronRight } from "@/components/icons";
|
|
4
5
|
import { cn } from "@/lib/cn";
|
|
5
6
|
|
|
6
7
|
interface CalendarProps {
|
|
@@ -64,11 +65,10 @@ export function Calendar({ value, onChange, className }: CalendarProps) {
|
|
|
64
65
|
<button
|
|
65
66
|
type="button"
|
|
66
67
|
onClick={prevMonth}
|
|
68
|
+
aria-label="Previous month"
|
|
67
69
|
className="rounded-lg p-1 text-ph-subtle transition hover:bg-ph-muted hover:text-ph-ink"
|
|
68
70
|
>
|
|
69
|
-
<
|
|
70
|
-
<path d="M15 18l-6-6 6-6" />
|
|
71
|
-
</svg>
|
|
71
|
+
<IconChevronLeft className="h-5 w-5" aria-hidden />
|
|
72
72
|
</button>
|
|
73
73
|
<span className="text-sm font-semibold text-ph-ink">
|
|
74
74
|
{monthNames[month]} {year}
|
|
@@ -76,11 +76,10 @@ export function Calendar({ value, onChange, className }: CalendarProps) {
|
|
|
76
76
|
<button
|
|
77
77
|
type="button"
|
|
78
78
|
onClick={nextMonth}
|
|
79
|
+
aria-label="Next month"
|
|
79
80
|
className="rounded-lg p-1 text-ph-subtle transition hover:bg-ph-muted hover:text-ph-ink"
|
|
80
81
|
>
|
|
81
|
-
<
|
|
82
|
-
<path d="M9 18l6-6-6-6" />
|
|
83
|
-
</svg>
|
|
82
|
+
<IconChevronRight className="h-5 w-5" aria-hidden />
|
|
84
83
|
</button>
|
|
85
84
|
</div>
|
|
86
85
|
|
|
@@ -99,7 +98,7 @@ export function Calendar({ value, onChange, className }: CalendarProps) {
|
|
|
99
98
|
className={cn(
|
|
100
99
|
"flex h-full w-full items-center justify-center rounded-lg text-sm transition-colors",
|
|
101
100
|
isSelected(day)
|
|
102
|
-
? "bg-ph-brand font-semibold text-
|
|
101
|
+
? "bg-ph-brand font-semibold text-[var(--ph-on-accent)] shadow-ph"
|
|
103
102
|
: isToday(day)
|
|
104
103
|
? "bg-ph-muted font-semibold text-ph-brand ring-1 ring-ph-brand/30"
|
|
105
104
|
: "text-ph-ink hover:bg-ph-muted"
|
|
@@ -1,9 +1,9 @@
|
|
|
1
|
-
import type
|
|
1
|
+
import { forwardRef, type HTMLAttributes, type ReactNode } from "react";
|
|
2
2
|
import { cn } from "@/lib/cn";
|
|
3
3
|
|
|
4
4
|
export type CardVariant = "default" | "outlined" | "elevated" | "highlighted";
|
|
5
5
|
|
|
6
|
-
export interface CardProps {
|
|
6
|
+
export interface CardProps extends Omit<HTMLAttributes<HTMLDivElement>, "title"> {
|
|
7
7
|
children?: ReactNode;
|
|
8
8
|
title?: ReactNode;
|
|
9
9
|
description?: ReactNode;
|
|
@@ -13,16 +13,18 @@ export interface CardProps {
|
|
|
13
13
|
variant?: CardVariant;
|
|
14
14
|
className?: string;
|
|
15
15
|
bodyClassName?: string;
|
|
16
|
+
interactive?: boolean;
|
|
17
|
+
titleAs?: "h2" | "h3" | "h4";
|
|
16
18
|
}
|
|
17
19
|
|
|
18
20
|
const variantMap: Record<CardVariant, string> = {
|
|
19
|
-
default: "
|
|
20
|
-
outlined: "border-2 border-ph-border
|
|
21
|
-
elevated: "
|
|
22
|
-
highlighted: "
|
|
21
|
+
default: "",
|
|
22
|
+
outlined: "border-2 border-ph-border",
|
|
23
|
+
elevated: "ph-card-elevated",
|
|
24
|
+
highlighted: "ph-card-highlighted",
|
|
23
25
|
};
|
|
24
26
|
|
|
25
|
-
export function Card({
|
|
27
|
+
export const Card = forwardRef<HTMLDivElement, CardProps>(function Card({
|
|
26
28
|
children,
|
|
27
29
|
title,
|
|
28
30
|
description,
|
|
@@ -32,19 +34,25 @@ export function Card({
|
|
|
32
34
|
variant = "default",
|
|
33
35
|
className,
|
|
34
36
|
bodyClassName,
|
|
35
|
-
|
|
37
|
+
interactive = false,
|
|
38
|
+
titleAs: Title = "h3",
|
|
39
|
+
...props
|
|
40
|
+
}, ref) {
|
|
36
41
|
return (
|
|
37
42
|
<div
|
|
43
|
+
ref={ref}
|
|
38
44
|
className={cn(
|
|
39
45
|
"ph-card",
|
|
40
46
|
variantMap[variant],
|
|
47
|
+
interactive && "ph-card-interactive",
|
|
41
48
|
className
|
|
42
49
|
)}
|
|
50
|
+
{...props}
|
|
43
51
|
>
|
|
44
|
-
{media && <div className="-
|
|
52
|
+
{media && <div className="ph-card-media">{media}</div>}
|
|
45
53
|
<div className={cn("ph-card-body", bodyClassName)}>
|
|
46
54
|
{title && (
|
|
47
|
-
<
|
|
55
|
+
<Title className="ph-card-title">{title}</Title>
|
|
48
56
|
)}
|
|
49
57
|
{description && (
|
|
50
58
|
<p className="text-sm text-ph-subtle">{description}</p>
|
|
@@ -57,4 +65,6 @@ export function Card({
|
|
|
57
65
|
)}
|
|
58
66
|
</div>
|
|
59
67
|
);
|
|
60
|
-
}
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
Card.displayName = "Card";
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
|
|
3
|
-
import { useState } from "react";
|
|
3
|
+
import { useId, useState } from "react";
|
|
4
|
+
import { IconChevronDown } from "@/components/icons";
|
|
4
5
|
import { cn } from "@/lib/cn";
|
|
5
6
|
|
|
6
7
|
interface CollapsibleSectionProps {
|
|
@@ -12,31 +13,30 @@ interface CollapsibleSectionProps {
|
|
|
12
13
|
|
|
13
14
|
export function CollapsibleSection({ title, children, defaultOpen = true, id }: CollapsibleSectionProps) {
|
|
14
15
|
const [isOpen, setIsOpen] = useState(defaultOpen);
|
|
16
|
+
const generatedId = useId();
|
|
17
|
+
const contentId = `${id ?? generatedId}-content`;
|
|
15
18
|
|
|
16
19
|
return (
|
|
17
20
|
<section id={id} className="scroll-mt-20">
|
|
18
21
|
<button
|
|
19
22
|
type="button"
|
|
20
23
|
onClick={() => setIsOpen(!isOpen)}
|
|
24
|
+
aria-expanded={isOpen}
|
|
25
|
+
aria-controls={contentId}
|
|
21
26
|
className="mb-6 flex w-full items-center justify-between rounded-lg border border-ph-border bg-ph-muted px-5 py-4 text-left transition hover:bg-ph-muted/80"
|
|
22
27
|
>
|
|
23
28
|
<span className="text-lg font-bold text-ph-ink">{title}</span>
|
|
24
|
-
<
|
|
29
|
+
<IconChevronDown
|
|
25
30
|
className={cn(
|
|
26
31
|
"h-5 w-5 text-ph-subtle transition-transform duration-200",
|
|
27
32
|
isOpen && "rotate-180"
|
|
28
33
|
)}
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
stroke="currentColor"
|
|
32
|
-
strokeWidth="2"
|
|
33
|
-
strokeLinecap="round"
|
|
34
|
-
strokeLinejoin="round"
|
|
35
|
-
>
|
|
36
|
-
<path d="M6 9l6 6 6-6" />
|
|
37
|
-
</svg>
|
|
34
|
+
aria-hidden
|
|
35
|
+
/>
|
|
38
36
|
</button>
|
|
39
37
|
<div
|
|
38
|
+
id={contentId}
|
|
39
|
+
aria-hidden={!isOpen}
|
|
40
40
|
className={cn(
|
|
41
41
|
"overflow-hidden transition-all duration-300",
|
|
42
42
|
isOpen ? "max-h-[50000px] opacity-100" : "max-h-0 opacity-0"
|
|
@@ -1,39 +1,75 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import * as DropdownPrimitive from "@radix-ui/react-dropdown-menu";
|
|
4
|
+
import type {
|
|
5
|
+
ButtonHTMLAttributes,
|
|
6
|
+
ComponentPropsWithoutRef,
|
|
7
|
+
HTMLAttributes,
|
|
8
|
+
ReactNode,
|
|
9
|
+
} from "react";
|
|
10
|
+
import { IconCheck, IconChevronDown } from "@/components/icons";
|
|
3
11
|
import { cn } from "@/lib/cn";
|
|
4
12
|
import { ButtonChrome } from "@/components/ui/ButtonChrome";
|
|
5
13
|
import type { ButtonSize } from "@/components/ui/Button";
|
|
6
14
|
|
|
7
|
-
|
|
15
|
+
type ContentProps = ComponentPropsWithoutRef<typeof DropdownPrimitive.Content>;
|
|
16
|
+
|
|
17
|
+
export type DropdownAlign = NonNullable<ContentProps["align"]>;
|
|
18
|
+
export type DropdownSide = NonNullable<ContentProps["side"]>;
|
|
19
|
+
|
|
20
|
+
interface DropdownPlacementProps {
|
|
21
|
+
align?: DropdownAlign;
|
|
22
|
+
side?: DropdownSide;
|
|
23
|
+
sideOffset?: number;
|
|
24
|
+
alignOffset?: number;
|
|
25
|
+
collisionPadding?: ContentProps["collisionPadding"];
|
|
26
|
+
avoidCollisions?: boolean;
|
|
27
|
+
panelClassName?: string;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
interface DropdownOpenProps {
|
|
31
|
+
open?: boolean;
|
|
32
|
+
defaultOpen?: boolean;
|
|
33
|
+
onOpenChange?: (open: boolean) => void;
|
|
34
|
+
modal?: boolean;
|
|
35
|
+
}
|
|
8
36
|
|
|
9
|
-
export interface DropdownMenuProps
|
|
10
|
-
|
|
37
|
+
export interface DropdownMenuProps
|
|
38
|
+
extends Omit<HTMLAttributes<HTMLDivElement>, "children">,
|
|
39
|
+
DropdownPlacementProps,
|
|
40
|
+
DropdownOpenProps {
|
|
41
|
+
/** Custom non-interactive trigger node, such as an avatar or icon chip. */
|
|
11
42
|
trigger: ReactNode;
|
|
12
|
-
/** Accessible name when trigger has no visible text */
|
|
43
|
+
/** Accessible name when the trigger has no visible text. */
|
|
13
44
|
"aria-label": string;
|
|
14
45
|
children: ReactNode;
|
|
15
|
-
|
|
16
|
-
panelClassName?: string;
|
|
46
|
+
disabled?: boolean;
|
|
17
47
|
}
|
|
18
48
|
|
|
19
|
-
export interface DropdownButtonProps
|
|
20
|
-
|
|
49
|
+
export interface DropdownButtonProps
|
|
50
|
+
extends Omit<HTMLAttributes<HTMLDivElement>, "children">,
|
|
51
|
+
DropdownPlacementProps,
|
|
52
|
+
DropdownOpenProps {
|
|
53
|
+
/** Visible button label. */
|
|
21
54
|
label: string;
|
|
22
55
|
variant?: "primary" | "secondary" | "accent";
|
|
23
56
|
outline?: boolean;
|
|
24
57
|
size?: ButtonSize;
|
|
25
|
-
/** Split control
|
|
58
|
+
/** Split control styling with a divider before the trailing chevron. */
|
|
26
59
|
split?: boolean;
|
|
27
60
|
icon?: ReactNode;
|
|
28
|
-
/** Defaults to chevron when omitted */
|
|
61
|
+
/** Defaults to a chevron when omitted. */
|
|
29
62
|
sideIcon?: ReactNode;
|
|
30
63
|
children: ReactNode;
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
/** Overrides accessible name; defaults to `label` */
|
|
64
|
+
disabled?: boolean;
|
|
65
|
+
/** Overrides the accessible name; defaults to `label`. */
|
|
34
66
|
"aria-label"?: string;
|
|
35
67
|
}
|
|
36
68
|
|
|
69
|
+
export type DropdownItemProps = ButtonHTMLAttributes<HTMLButtonElement>;
|
|
70
|
+
export type DropdownRadioGroupProps = ComponentPropsWithoutRef<typeof DropdownPrimitive.RadioGroup>;
|
|
71
|
+
export type DropdownRadioItemProps = ComponentPropsWithoutRef<typeof DropdownPrimitive.RadioItem>;
|
|
72
|
+
|
|
37
73
|
const sizeClass: Record<ButtonSize, string> = {
|
|
38
74
|
xs: "ph-btn-xs",
|
|
39
75
|
sm: "ph-btn-sm",
|
|
@@ -47,38 +83,85 @@ const variantClass = {
|
|
|
47
83
|
accent: "ph-btn-accent",
|
|
48
84
|
} as const;
|
|
49
85
|
|
|
50
|
-
function
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
86
|
+
function DropdownContent({
|
|
87
|
+
children,
|
|
88
|
+
align = "start",
|
|
89
|
+
side = "bottom",
|
|
90
|
+
sideOffset = 7,
|
|
91
|
+
alignOffset = 0,
|
|
92
|
+
collisionPadding = 8,
|
|
93
|
+
avoidCollisions = true,
|
|
94
|
+
panelClassName,
|
|
95
|
+
}: DropdownPlacementProps & { children: ReactNode }) {
|
|
96
|
+
return (
|
|
97
|
+
<DropdownPrimitive.Portal>
|
|
98
|
+
<DropdownPrimitive.Content
|
|
99
|
+
className={cn("ph-dropdown-panel", panelClassName)}
|
|
100
|
+
align={align}
|
|
101
|
+
side={side}
|
|
102
|
+
sideOffset={sideOffset}
|
|
103
|
+
alignOffset={alignOffset}
|
|
104
|
+
collisionPadding={collisionPadding}
|
|
105
|
+
avoidCollisions={avoidCollisions}
|
|
106
|
+
sticky="partial"
|
|
107
|
+
>
|
|
108
|
+
{children}
|
|
109
|
+
<DropdownPrimitive.Arrow className="ph-dropdown-arrow" width={12} height={6} />
|
|
110
|
+
</DropdownPrimitive.Content>
|
|
111
|
+
</DropdownPrimitive.Portal>
|
|
55
112
|
);
|
|
56
113
|
}
|
|
57
114
|
|
|
58
|
-
/**
|
|
115
|
+
/** Collision-aware menu that flips sides and shifts its alignment to remain visible. */
|
|
59
116
|
export function DropdownMenu({
|
|
60
117
|
trigger,
|
|
61
118
|
children,
|
|
62
|
-
align
|
|
119
|
+
align,
|
|
120
|
+
side,
|
|
121
|
+
sideOffset,
|
|
122
|
+
alignOffset,
|
|
123
|
+
collisionPadding,
|
|
124
|
+
avoidCollisions,
|
|
63
125
|
className,
|
|
64
126
|
panelClassName,
|
|
65
127
|
"aria-label": ariaLabel,
|
|
128
|
+
open,
|
|
129
|
+
defaultOpen,
|
|
130
|
+
onOpenChange,
|
|
131
|
+
modal = false,
|
|
132
|
+
disabled,
|
|
66
133
|
...props
|
|
67
134
|
}: DropdownMenuProps) {
|
|
68
135
|
return (
|
|
69
|
-
<
|
|
70
|
-
<
|
|
71
|
-
|
|
72
|
-
|
|
136
|
+
<div className={cn("ph-dropdown", className)} {...props}>
|
|
137
|
+
<DropdownPrimitive.Root
|
|
138
|
+
open={open}
|
|
139
|
+
defaultOpen={defaultOpen}
|
|
140
|
+
onOpenChange={onOpenChange}
|
|
141
|
+
modal={modal}
|
|
73
142
|
>
|
|
74
|
-
{
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
143
|
+
<DropdownPrimitive.Trigger asChild disabled={disabled}>
|
|
144
|
+
<button type="button" className="ph-dropdown-trigger" aria-label={ariaLabel}>
|
|
145
|
+
{trigger}
|
|
146
|
+
</button>
|
|
147
|
+
</DropdownPrimitive.Trigger>
|
|
148
|
+
<DropdownContent
|
|
149
|
+
align={align}
|
|
150
|
+
side={side}
|
|
151
|
+
sideOffset={sideOffset}
|
|
152
|
+
alignOffset={alignOffset}
|
|
153
|
+
collisionPadding={collisionPadding}
|
|
154
|
+
avoidCollisions={avoidCollisions}
|
|
155
|
+
panelClassName={panelClassName}
|
|
156
|
+
>
|
|
157
|
+
{children}
|
|
158
|
+
</DropdownContent>
|
|
159
|
+
</DropdownPrimitive.Root>
|
|
160
|
+
</div>
|
|
78
161
|
);
|
|
79
162
|
}
|
|
80
163
|
|
|
81
|
-
/**
|
|
164
|
+
/** Tactile dropdown trigger backed by the same adaptive menu positioning. */
|
|
82
165
|
export function DropdownButton({
|
|
83
166
|
label,
|
|
84
167
|
variant = "secondary",
|
|
@@ -88,51 +171,92 @@ export function DropdownButton({
|
|
|
88
171
|
icon,
|
|
89
172
|
sideIcon,
|
|
90
173
|
children,
|
|
91
|
-
align
|
|
174
|
+
align,
|
|
175
|
+
side,
|
|
176
|
+
sideOffset,
|
|
177
|
+
alignOffset,
|
|
178
|
+
collisionPadding,
|
|
179
|
+
avoidCollisions,
|
|
92
180
|
className,
|
|
93
181
|
panelClassName,
|
|
94
182
|
"aria-label": ariaLabel,
|
|
183
|
+
open,
|
|
184
|
+
defaultOpen,
|
|
185
|
+
onOpenChange,
|
|
186
|
+
modal = false,
|
|
187
|
+
disabled,
|
|
95
188
|
...props
|
|
96
189
|
}: DropdownButtonProps) {
|
|
97
|
-
const
|
|
98
|
-
const chevron = <IconChevronDown className="h-4 w-4" aria-hidden />;
|
|
99
|
-
const trailing = sideIcon ?? chevron;
|
|
190
|
+
const trailing = sideIcon ?? <IconChevronDown className="h-4 w-4" aria-hidden />;
|
|
100
191
|
|
|
101
192
|
return (
|
|
102
|
-
<
|
|
103
|
-
<
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
sizeClass[size],
|
|
109
|
-
split && "ph-btn-split",
|
|
110
|
-
)}
|
|
111
|
-
aria-label={ariaLabel ?? label}
|
|
193
|
+
<div className={cn("ph-dropdown", className)} {...props}>
|
|
194
|
+
<DropdownPrimitive.Root
|
|
195
|
+
open={open}
|
|
196
|
+
defaultOpen={defaultOpen}
|
|
197
|
+
onOpenChange={onOpenChange}
|
|
198
|
+
modal={modal}
|
|
112
199
|
>
|
|
113
|
-
|
|
114
|
-
<
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
200
|
+
<DropdownPrimitive.Trigger asChild disabled={disabled}>
|
|
201
|
+
<button
|
|
202
|
+
type="button"
|
|
203
|
+
disabled={disabled}
|
|
204
|
+
className={cn(
|
|
205
|
+
"ph-btn ph-btn-raised ph-dropdown-button",
|
|
206
|
+
variantClass[variant],
|
|
207
|
+
outline && "ph-btn-outline",
|
|
208
|
+
sizeClass[size],
|
|
209
|
+
split && "ph-btn-split",
|
|
210
|
+
)}
|
|
211
|
+
aria-label={ariaLabel}
|
|
212
|
+
>
|
|
213
|
+
<ButtonChrome label={label} icon={icon} sideIcon={trailing} split={split} />
|
|
214
|
+
</button>
|
|
215
|
+
</DropdownPrimitive.Trigger>
|
|
216
|
+
<DropdownContent
|
|
217
|
+
align={align}
|
|
218
|
+
side={side}
|
|
219
|
+
sideOffset={sideOffset}
|
|
220
|
+
alignOffset={alignOffset}
|
|
221
|
+
collisionPadding={collisionPadding}
|
|
222
|
+
avoidCollisions={avoidCollisions}
|
|
223
|
+
panelClassName={panelClassName}
|
|
224
|
+
>
|
|
225
|
+
{children}
|
|
226
|
+
</DropdownContent>
|
|
227
|
+
</DropdownPrimitive.Root>
|
|
228
|
+
</div>
|
|
125
229
|
);
|
|
126
230
|
}
|
|
127
231
|
|
|
128
|
-
export function DropdownItem({
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
232
|
+
export function DropdownItem({ className, children, disabled, ...props }: DropdownItemProps) {
|
|
233
|
+
return (
|
|
234
|
+
<DropdownPrimitive.Item asChild disabled={disabled}>
|
|
235
|
+
<button
|
|
236
|
+
type="button"
|
|
237
|
+
disabled={disabled}
|
|
238
|
+
className={cn("ph-dropdown-item w-full text-left", className)}
|
|
239
|
+
{...props}
|
|
240
|
+
>
|
|
241
|
+
{children}
|
|
242
|
+
</button>
|
|
243
|
+
</DropdownPrimitive.Item>
|
|
244
|
+
);
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
export function DropdownRadioGroup(props: DropdownRadioGroupProps) {
|
|
248
|
+
return <DropdownPrimitive.RadioGroup {...props} />;
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
export function DropdownRadioItem({ className, children, ...props }: DropdownRadioItemProps) {
|
|
133
252
|
return (
|
|
134
|
-
<
|
|
135
|
-
|
|
136
|
-
|
|
253
|
+
<DropdownPrimitive.RadioItem className={cn("ph-dropdown-item ph-dropdown-radio-item", className)} {...props}>
|
|
254
|
+
<span className="ph-dropdown-item-indicator" aria-hidden>
|
|
255
|
+
<DropdownPrimitive.ItemIndicator>
|
|
256
|
+
<IconCheck className="h-3.5 w-3.5" />
|
|
257
|
+
</DropdownPrimitive.ItemIndicator>
|
|
258
|
+
</span>
|
|
259
|
+
<span>{children}</span>
|
|
260
|
+
</DropdownPrimitive.RadioItem>
|
|
137
261
|
);
|
|
138
262
|
}
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import type { HTMLAttributes, ReactNode } from "react";
|
|
4
|
+
import { IconArrowDown, IconTuning } from "@/components/icons";
|
|
5
|
+
import { cn } from "@/lib/cn";
|
|
6
|
+
import {
|
|
7
|
+
DropdownMenu,
|
|
8
|
+
DropdownRadioGroup,
|
|
9
|
+
DropdownRadioItem,
|
|
10
|
+
type DropdownAlign,
|
|
11
|
+
} from "@/components/ui/DropdownMenu";
|
|
12
|
+
|
|
13
|
+
export interface FilterMenuOption {
|
|
14
|
+
value: string;
|
|
15
|
+
label: ReactNode;
|
|
16
|
+
/** Plain-text label used for typeahead and accessible trigger naming. */
|
|
17
|
+
textValue?: string;
|
|
18
|
+
disabled?: boolean;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export interface FilterMenuProps {
|
|
22
|
+
label: string;
|
|
23
|
+
value?: string;
|
|
24
|
+
options: readonly FilterMenuOption[];
|
|
25
|
+
onValueChange: (value: string) => void;
|
|
26
|
+
placeholder?: ReactNode;
|
|
27
|
+
align?: DropdownAlign;
|
|
28
|
+
disabled?: boolean;
|
|
29
|
+
className?: string;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export interface SortMenuProps extends Omit<FilterMenuProps, "placeholder"> {
|
|
33
|
+
direction?: "ascending" | "descending";
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export interface FilterBarProps extends Omit<HTMLAttributes<HTMLDivElement>, "children"> {
|
|
37
|
+
children: ReactNode;
|
|
38
|
+
label?: string;
|
|
39
|
+
onClear?: () => void;
|
|
40
|
+
clearLabel?: string;
|
|
41
|
+
resultCount?: ReactNode;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export function FilterBar({
|
|
45
|
+
children,
|
|
46
|
+
label = "Filters and sorting",
|
|
47
|
+
onClear,
|
|
48
|
+
clearLabel = "Clear all",
|
|
49
|
+
resultCount,
|
|
50
|
+
className,
|
|
51
|
+
...props
|
|
52
|
+
}: FilterBarProps) {
|
|
53
|
+
return (
|
|
54
|
+
<div className={cn("ph-filter-bar", className)} {...props}>
|
|
55
|
+
<div className="ph-filter-bar__controls" role="group" aria-label={label}>
|
|
56
|
+
{children}
|
|
57
|
+
{onClear ? (
|
|
58
|
+
<button type="button" className="ph-filter-bar__clear" onClick={onClear}>
|
|
59
|
+
{clearLabel}
|
|
60
|
+
</button>
|
|
61
|
+
) : null}
|
|
62
|
+
</div>
|
|
63
|
+
{resultCount != null ? (
|
|
64
|
+
<output className="ph-filter-bar__results" aria-live="polite">
|
|
65
|
+
{resultCount}
|
|
66
|
+
</output>
|
|
67
|
+
) : null}
|
|
68
|
+
</div>
|
|
69
|
+
);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export function FilterMenu({
|
|
73
|
+
label,
|
|
74
|
+
value,
|
|
75
|
+
options,
|
|
76
|
+
onValueChange,
|
|
77
|
+
placeholder = "Any",
|
|
78
|
+
align = "start",
|
|
79
|
+
disabled,
|
|
80
|
+
className,
|
|
81
|
+
}: FilterMenuProps) {
|
|
82
|
+
const selected = options.find((option) => option.value === value);
|
|
83
|
+
const displayValue = selected?.label ?? placeholder;
|
|
84
|
+
const accessibleValue = selected ? getOptionText(selected) : String(placeholder);
|
|
85
|
+
|
|
86
|
+
return (
|
|
87
|
+
<DropdownMenu
|
|
88
|
+
aria-label={`${label}: ${accessibleValue}`}
|
|
89
|
+
align={align}
|
|
90
|
+
disabled={disabled}
|
|
91
|
+
className={className}
|
|
92
|
+
trigger={
|
|
93
|
+
<span className={cn("ph-filter-menu-trigger", selected && "ph-filter-menu-trigger--active")}>
|
|
94
|
+
<IconTuning className="h-3.5 w-3.5" aria-hidden />
|
|
95
|
+
<span className="ph-filter-menu-trigger__label">{label}</span>
|
|
96
|
+
<span className="ph-filter-menu-trigger__value">{displayValue}</span>
|
|
97
|
+
</span>
|
|
98
|
+
}
|
|
99
|
+
>
|
|
100
|
+
<DropdownRadioGroup value={value} onValueChange={onValueChange}>
|
|
101
|
+
{options.map((option) => (
|
|
102
|
+
<DropdownRadioItem
|
|
103
|
+
key={option.value}
|
|
104
|
+
value={option.value}
|
|
105
|
+
textValue={getOptionText(option)}
|
|
106
|
+
disabled={option.disabled}
|
|
107
|
+
>
|
|
108
|
+
{option.label}
|
|
109
|
+
</DropdownRadioItem>
|
|
110
|
+
))}
|
|
111
|
+
</DropdownRadioGroup>
|
|
112
|
+
</DropdownMenu>
|
|
113
|
+
);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
export function SortMenu({
|
|
117
|
+
label,
|
|
118
|
+
value,
|
|
119
|
+
options,
|
|
120
|
+
onValueChange,
|
|
121
|
+
direction = "descending",
|
|
122
|
+
align = "end",
|
|
123
|
+
disabled,
|
|
124
|
+
className,
|
|
125
|
+
}: SortMenuProps) {
|
|
126
|
+
const selected = options.find((option) => option.value === value);
|
|
127
|
+
|
|
128
|
+
return (
|
|
129
|
+
<DropdownMenu
|
|
130
|
+
aria-label={`${label}: ${selected ? getOptionText(selected) : "Choose sort order"}`}
|
|
131
|
+
align={align}
|
|
132
|
+
disabled={disabled}
|
|
133
|
+
className={className}
|
|
134
|
+
trigger={
|
|
135
|
+
<span className="ph-filter-menu-trigger ph-sort-menu-trigger">
|
|
136
|
+
<IconArrowDown
|
|
137
|
+
className={cn("h-3.5 w-3.5", direction === "ascending" && "rotate-180")}
|
|
138
|
+
aria-hidden
|
|
139
|
+
/>
|
|
140
|
+
<span className="ph-filter-menu-trigger__label">{label}</span>
|
|
141
|
+
{selected ? <span className="ph-filter-menu-trigger__value">{selected.label}</span> : null}
|
|
142
|
+
</span>
|
|
143
|
+
}
|
|
144
|
+
>
|
|
145
|
+
<DropdownRadioGroup value={value} onValueChange={onValueChange}>
|
|
146
|
+
{options.map((option) => (
|
|
147
|
+
<DropdownRadioItem
|
|
148
|
+
key={option.value}
|
|
149
|
+
value={option.value}
|
|
150
|
+
textValue={getOptionText(option)}
|
|
151
|
+
disabled={option.disabled}
|
|
152
|
+
>
|
|
153
|
+
{option.label}
|
|
154
|
+
</DropdownRadioItem>
|
|
155
|
+
))}
|
|
156
|
+
</DropdownRadioGroup>
|
|
157
|
+
</DropdownMenu>
|
|
158
|
+
);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
function getOptionText(option: FilterMenuOption) {
|
|
162
|
+
if (option.textValue) return option.textValue;
|
|
163
|
+
if (typeof option.label === "string" || typeof option.label === "number") return String(option.label);
|
|
164
|
+
return option.value;
|
|
165
|
+
}
|