@voila.dev/ui 1.1.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/package.json +78 -0
- package/src/components/ui/accordion.tsx +79 -0
- package/src/components/ui/alert-dialog.tsx +198 -0
- package/src/components/ui/alert.tsx +105 -0
- package/src/components/ui/aspect-ratio.tsx +30 -0
- package/src/components/ui/avatar.tsx +125 -0
- package/src/components/ui/badge-variants.ts +166 -0
- package/src/components/ui/badge.tsx +45 -0
- package/src/components/ui/banner.tsx +84 -0
- package/src/components/ui/breadcrumb.tsx +127 -0
- package/src/components/ui/button-group.tsx +99 -0
- package/src/components/ui/button-variants.ts +108 -0
- package/src/components/ui/button.tsx +54 -0
- package/src/components/ui/calendar.tsx +266 -0
- package/src/components/ui/card.tsx +155 -0
- package/src/components/ui/carousel.tsx +319 -0
- package/src/components/ui/chat.tsx +896 -0
- package/src/components/ui/checkbox-group.tsx +44 -0
- package/src/components/ui/checkbox.tsx +34 -0
- package/src/components/ui/chip.tsx +60 -0
- package/src/components/ui/collapsible.tsx +44 -0
- package/src/components/ui/color-picker.tsx +191 -0
- package/src/components/ui/combobox.tsx +317 -0
- package/src/components/ui/command.tsx +187 -0
- package/src/components/ui/confirm-dialog.tsx +125 -0
- package/src/components/ui/context-menu.tsx +254 -0
- package/src/components/ui/copyable-text.tsx +65 -0
- package/src/components/ui/date-picker.tsx +316 -0
- package/src/components/ui/date-time-picker.tsx +1071 -0
- package/src/components/ui/dialog.tsx +176 -0
- package/src/components/ui/direction.tsx +15 -0
- package/src/components/ui/drawer.tsx +178 -0
- package/src/components/ui/dropdown-menu.tsx +247 -0
- package/src/components/ui/empty.tsx +131 -0
- package/src/components/ui/field.tsx +257 -0
- package/src/components/ui/formatted-input.tsx +219 -0
- package/src/components/ui/gallery.tsx +161 -0
- package/src/components/ui/hover-card.tsx +77 -0
- package/src/components/ui/icon-picker.tsx +193 -0
- package/src/components/ui/image-cropper.tsx +787 -0
- package/src/components/ui/image-upload-field.tsx +344 -0
- package/src/components/ui/input-group.tsx +175 -0
- package/src/components/ui/input-otp.tsx +83 -0
- package/src/components/ui/input.tsx +21 -0
- package/src/components/ui/item.tsx +212 -0
- package/src/components/ui/kbd.tsx +62 -0
- package/src/components/ui/label.tsx +19 -0
- package/src/components/ui/list.tsx +46 -0
- package/src/components/ui/menu-variants.ts +71 -0
- package/src/components/ui/menubar.tsx +226 -0
- package/src/components/ui/money-input.tsx +96 -0
- package/src/components/ui/native-date-picker.tsx +120 -0
- package/src/components/ui/native-select.tsx +74 -0
- package/src/components/ui/navigation-menu.tsx +183 -0
- package/src/components/ui/pagination.tsx +150 -0
- package/src/components/ui/popover.tsx +104 -0
- package/src/components/ui/profile-header.tsx +196 -0
- package/src/components/ui/progress.tsx +71 -0
- package/src/components/ui/radio-group.tsx +115 -0
- package/src/components/ui/rating.tsx +200 -0
- package/src/components/ui/resizable.tsx +61 -0
- package/src/components/ui/responsive-dialog.tsx +278 -0
- package/src/components/ui/responsive-select.tsx +289 -0
- package/src/components/ui/responsive-sheet.tsx +225 -0
- package/src/components/ui/scroll-area.tsx +51 -0
- package/src/components/ui/section.tsx +106 -0
- package/src/components/ui/segmented-control.tsx +148 -0
- package/src/components/ui/select.tsx +216 -0
- package/src/components/ui/separator.tsx +69 -0
- package/src/components/ui/sheet.tsx +154 -0
- package/src/components/ui/shortcut.tsx +41 -0
- package/src/components/ui/sidebar.tsx +739 -0
- package/src/components/ui/skeleton.tsx +23 -0
- package/src/components/ui/slider.tsx +74 -0
- package/src/components/ui/sonner.tsx +64 -0
- package/src/components/ui/spinner.tsx +19 -0
- package/src/components/ui/stat-card.tsx +138 -0
- package/src/components/ui/stepper.tsx +181 -0
- package/src/components/ui/sticky-action-bar.tsx +42 -0
- package/src/components/ui/switch.tsx +30 -0
- package/src/components/ui/table.tsx +132 -0
- package/src/components/ui/tabs.tsx +77 -0
- package/src/components/ui/textarea.tsx +24 -0
- package/src/components/ui/time-picker.tsx +221 -0
- package/src/components/ui/toggle-group.tsx +86 -0
- package/src/components/ui/toggle.tsx +43 -0
- package/src/components/ui/tooltip.tsx +79 -0
- package/src/components/ui/translation-input.tsx +126 -0
- package/src/components/ui/user-avatar.tsx +74 -0
- package/src/css-modules.d.ts +4 -0
- package/src/hooks/use-command-palette.ts +28 -0
- package/src/hooks/use-mobile.ts +21 -0
- package/src/hooks/use-pagination.ts +81 -0
- package/src/hooks/use-picker-state.ts +44 -0
- package/src/lib/cva.ts +18 -0
- package/src/lib/time-math.ts +62 -0
- package/src/lib/utils.ts +14 -0
- package/src/styles/globals.css +36 -0
- package/src/styles.css +10 -0
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import type * as React from "react";
|
|
2
|
+
import {
|
|
3
|
+
InputGroup,
|
|
4
|
+
InputGroupAddon,
|
|
5
|
+
InputGroupInput,
|
|
6
|
+
} from "#/components/ui/input-group.tsx";
|
|
7
|
+
import { cn } from "#/lib/utils.ts";
|
|
8
|
+
|
|
9
|
+
type MoneyInputProps = Omit<
|
|
10
|
+
React.ComponentProps<typeof InputGroupInput>,
|
|
11
|
+
"type" | "value" | "onChange"
|
|
12
|
+
> & {
|
|
13
|
+
/** Raw amount as typed (e.g. `"120"` or `"120.50"`). */
|
|
14
|
+
value: string;
|
|
15
|
+
/** Receives the raw amount string on every change. */
|
|
16
|
+
onValueChange: (value: string) => void;
|
|
17
|
+
/** Selected currency code (e.g. `"EUR"`). */
|
|
18
|
+
currency: string;
|
|
19
|
+
/** Currency codes offered by the select. Defaults to the single `currency`. */
|
|
20
|
+
currencies?: ReadonlyArray<string>;
|
|
21
|
+
/** Receives the selected currency code. Omit when the select is locked. */
|
|
22
|
+
onCurrencyChange?: (currency: string) => void;
|
|
23
|
+
/**
|
|
24
|
+
* Locks the currency select. Defaults to `true` when there is a single
|
|
25
|
+
* currency to choose from, so the platform's single-currency case reads as a
|
|
26
|
+
* disabled selector rather than a bare suffix.
|
|
27
|
+
*/
|
|
28
|
+
currencyDisabled?: boolean;
|
|
29
|
+
/** Accessible name for the currency select. */
|
|
30
|
+
currencyLabel: string;
|
|
31
|
+
/** Wrapper class (width/layout), forwarded to the `InputGroup`. */
|
|
32
|
+
className?: string;
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* A money amount input: a numeric field with a trailing currency select. The
|
|
37
|
+
* select is disabled by default (the platform settles in a single currency), so
|
|
38
|
+
* it reads as a locked currency selector rather than a static suffix — pass
|
|
39
|
+
* `currencies` + `onCurrencyChange` to make it interactive. The amount is an
|
|
40
|
+
* uncontrolled-string like the underlying `<input type="number">`; the consumer
|
|
41
|
+
* owns parsing/formatting into cents.
|
|
42
|
+
*/
|
|
43
|
+
function MoneyInput({
|
|
44
|
+
value,
|
|
45
|
+
onValueChange,
|
|
46
|
+
currency,
|
|
47
|
+
currencies = [currency],
|
|
48
|
+
onCurrencyChange,
|
|
49
|
+
currencyDisabled = currencies.length <= 1,
|
|
50
|
+
currencyLabel,
|
|
51
|
+
className,
|
|
52
|
+
...props
|
|
53
|
+
}: MoneyInputProps) {
|
|
54
|
+
return (
|
|
55
|
+
<InputGroup
|
|
56
|
+
className={cn(
|
|
57
|
+
"has-disabled:bg-transparent has-disabled:opacity-100 dark:has-disabled:bg-input/30",
|
|
58
|
+
className,
|
|
59
|
+
)}
|
|
60
|
+
data-slot="money-input"
|
|
61
|
+
>
|
|
62
|
+
<InputGroupInput
|
|
63
|
+
type="number"
|
|
64
|
+
inputMode="decimal"
|
|
65
|
+
min="0"
|
|
66
|
+
step="0.5"
|
|
67
|
+
value={value}
|
|
68
|
+
onChange={(event) => onValueChange(event.target.value)}
|
|
69
|
+
{...props}
|
|
70
|
+
/>
|
|
71
|
+
<InputGroupAddon align="inline-end">
|
|
72
|
+
<select
|
|
73
|
+
data-slot="money-input-currency"
|
|
74
|
+
aria-label={currencyLabel}
|
|
75
|
+
disabled={currencyDisabled}
|
|
76
|
+
value={currency}
|
|
77
|
+
onChange={(event) => onCurrencyChange?.(event.target.value)}
|
|
78
|
+
className={cn(
|
|
79
|
+
"cursor-pointer appearance-none bg-transparent pr-1 text-sm font-medium text-muted-foreground outline-none",
|
|
80
|
+
// A disabled select must stay fully legible (it is the locked
|
|
81
|
+
// currency label), so keep full opacity and drop the cursor.
|
|
82
|
+
"disabled:cursor-default disabled:opacity-100",
|
|
83
|
+
)}
|
|
84
|
+
>
|
|
85
|
+
{currencies.map((code) => (
|
|
86
|
+
<option key={code} value={code}>
|
|
87
|
+
{code}
|
|
88
|
+
</option>
|
|
89
|
+
))}
|
|
90
|
+
</select>
|
|
91
|
+
</InputGroupAddon>
|
|
92
|
+
</InputGroup>
|
|
93
|
+
);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
export { MoneyInput, type MoneyInputProps };
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
import {
|
|
2
|
+
CalendarBlankIcon,
|
|
3
|
+
CalendarDotsIcon,
|
|
4
|
+
ClockIcon,
|
|
5
|
+
} from "@phosphor-icons/react";
|
|
6
|
+
import type * as React from "react";
|
|
7
|
+
import { cn } from "#/lib/utils.ts";
|
|
8
|
+
|
|
9
|
+
type NativeDateFieldProps = Omit<
|
|
10
|
+
React.ComponentProps<"input">,
|
|
11
|
+
"type" | "size"
|
|
12
|
+
> & {
|
|
13
|
+
size?: "sm" | "default";
|
|
14
|
+
/**
|
|
15
|
+
* Classes for the wrapper `<div>` that hosts the leading icon. Use this for
|
|
16
|
+
* layout and width (e.g. `w-full`, `w-48`); `className` styles the inner
|
|
17
|
+
* `<input>` itself, matching the rest of the kit's form controls.
|
|
18
|
+
*/
|
|
19
|
+
wrapperClassName?: string;
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Shared chrome for the native date/time/datetime-local inputs: the kit's
|
|
24
|
+
* Input field styling plus a leading icon (mirroring DatePicker's trigger)
|
|
25
|
+
* and click-anywhere-to-open via `showPicker()`. The WebKit indicator is
|
|
26
|
+
* hidden — the whole field is the affordance, like the composed DatePicker.
|
|
27
|
+
*/
|
|
28
|
+
function NativeDateField({
|
|
29
|
+
type,
|
|
30
|
+
slot,
|
|
31
|
+
icon,
|
|
32
|
+
className,
|
|
33
|
+
wrapperClassName,
|
|
34
|
+
size = "default",
|
|
35
|
+
onClick,
|
|
36
|
+
...props
|
|
37
|
+
}: NativeDateFieldProps & {
|
|
38
|
+
type: "date" | "time" | "datetime-local";
|
|
39
|
+
slot: string;
|
|
40
|
+
icon: React.ReactNode;
|
|
41
|
+
}) {
|
|
42
|
+
return (
|
|
43
|
+
<div
|
|
44
|
+
className={cn(
|
|
45
|
+
"group/native-date-picker relative w-fit has-[input:disabled]:opacity-50",
|
|
46
|
+
wrapperClassName,
|
|
47
|
+
)}
|
|
48
|
+
data-slot={`${slot}-wrapper`}
|
|
49
|
+
data-size={size}
|
|
50
|
+
>
|
|
51
|
+
<span
|
|
52
|
+
className="pointer-events-none absolute top-1/2 left-2.5 -translate-y-1/2 text-muted-foreground select-none [&_svg]:size-4"
|
|
53
|
+
aria-hidden="true"
|
|
54
|
+
data-slot={`${slot}-icon`}
|
|
55
|
+
>
|
|
56
|
+
{icon}
|
|
57
|
+
</span>
|
|
58
|
+
<input
|
|
59
|
+
type={type}
|
|
60
|
+
data-slot={slot}
|
|
61
|
+
data-size={size}
|
|
62
|
+
// `text-base` then `md:text-sm` is the same iOS zoom guard as Input.
|
|
63
|
+
className={cn(
|
|
64
|
+
"h-8 w-full min-w-0 rounded-lg border border-input bg-transparent py-1 pr-2.5 pl-8 text-base transition-[color,box-shadow] outline-none placeholder:text-muted-foreground focus-visible:border-ring focus-visible:ring-3 focus-visible:ring-ring/50 disabled:cursor-not-allowed disabled:bg-input/50 aria-invalid:border-destructive aria-invalid:ring-3 aria-invalid:ring-destructive/20 data-[size=sm]:h-7 data-[size=sm]:rounded-[min(var(--radius-md),12px)] data-[size=sm]:py-0.5 md:text-sm dark:scheme-dark dark:bg-input/30 dark:disabled:bg-input/80 dark:aria-invalid:border-destructive/50 dark:aria-invalid:ring-destructive/40 [&::-webkit-calendar-picker-indicator]:hidden",
|
|
65
|
+
className,
|
|
66
|
+
)}
|
|
67
|
+
onClick={(event) => {
|
|
68
|
+
onClick?.(event);
|
|
69
|
+
if (event.defaultPrevented) return;
|
|
70
|
+
const input = event.currentTarget;
|
|
71
|
+
// Chrome/Edge/Firefox/Safari 16+; throws outside a user gesture.
|
|
72
|
+
if (typeof input.showPicker === "function") {
|
|
73
|
+
try {
|
|
74
|
+
input.showPicker();
|
|
75
|
+
} catch {
|
|
76
|
+
// Unsupported context (e.g. cross-origin iframe) — the field
|
|
77
|
+
// still works through its native segment editing.
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
}}
|
|
81
|
+
{...props}
|
|
82
|
+
/>
|
|
83
|
+
</div>
|
|
84
|
+
);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function NativeDatePicker(props: NativeDateFieldProps) {
|
|
88
|
+
return (
|
|
89
|
+
<NativeDateField
|
|
90
|
+
type="date"
|
|
91
|
+
slot="native-date-picker"
|
|
92
|
+
icon={<CalendarBlankIcon />}
|
|
93
|
+
{...props}
|
|
94
|
+
/>
|
|
95
|
+
);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function NativeTimePicker(props: NativeDateFieldProps) {
|
|
99
|
+
return (
|
|
100
|
+
<NativeDateField
|
|
101
|
+
type="time"
|
|
102
|
+
slot="native-time-picker"
|
|
103
|
+
icon={<ClockIcon />}
|
|
104
|
+
{...props}
|
|
105
|
+
/>
|
|
106
|
+
);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function NativeDateTimePicker(props: NativeDateFieldProps) {
|
|
110
|
+
return (
|
|
111
|
+
<NativeDateField
|
|
112
|
+
type="datetime-local"
|
|
113
|
+
slot="native-date-time-picker"
|
|
114
|
+
icon={<CalendarDotsIcon />}
|
|
115
|
+
{...props}
|
|
116
|
+
/>
|
|
117
|
+
);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
export { NativeDatePicker, NativeDateTimePicker, NativeTimePicker };
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import { CaretDownIcon } from "@phosphor-icons/react";
|
|
2
|
+
import type * as React from "react";
|
|
3
|
+
import { cn } from "#/lib/utils.ts";
|
|
4
|
+
|
|
5
|
+
type NativeSelectProps = Omit<React.ComponentProps<"select">, "size"> & {
|
|
6
|
+
size?: "sm" | "default";
|
|
7
|
+
/**
|
|
8
|
+
* Classes for the wrapper `<div>` that hosts the caret overlay. Use this for
|
|
9
|
+
* layout and width (e.g. `w-full`, `w-48`); `className` styles the inner
|
|
10
|
+
* `<select>` itself, matching the rest of the kit's form controls.
|
|
11
|
+
*/
|
|
12
|
+
wrapperClassName?: string;
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
function NativeSelect({
|
|
16
|
+
className,
|
|
17
|
+
wrapperClassName,
|
|
18
|
+
size = "default",
|
|
19
|
+
...props
|
|
20
|
+
}: NativeSelectProps) {
|
|
21
|
+
return (
|
|
22
|
+
<div
|
|
23
|
+
className={cn(
|
|
24
|
+
"group/native-select relative w-full has-[select:disabled]:opacity-50",
|
|
25
|
+
wrapperClassName,
|
|
26
|
+
)}
|
|
27
|
+
data-slot="native-select-wrapper"
|
|
28
|
+
data-size={size}
|
|
29
|
+
>
|
|
30
|
+
<select
|
|
31
|
+
data-slot="native-select"
|
|
32
|
+
data-size={size}
|
|
33
|
+
className={cn(
|
|
34
|
+
"h-8 w-full min-w-0 appearance-none rounded-lg border border-input bg-transparent py-1 pr-8 pl-2.5 text-sm transition-colors outline-none select-none placeholder:text-muted-foreground focus-visible:border-ring focus-visible:ring-3 focus-visible:ring-ring/50 disabled:cursor-not-allowed disabled:bg-input/50 aria-invalid:border-destructive aria-invalid:ring-3 aria-invalid:ring-destructive/20 data-[size=sm]:h-7 data-[size=sm]:rounded-[min(var(--radius-md),10px)] data-[size=sm]:py-0.5 dark:scheme-dark dark:bg-input/30 dark:hover:bg-input/50 dark:disabled:bg-input/80 dark:aria-invalid:border-destructive/50 dark:aria-invalid:ring-destructive/40",
|
|
35
|
+
className,
|
|
36
|
+
)}
|
|
37
|
+
{...props}
|
|
38
|
+
/>
|
|
39
|
+
<CaretDownIcon
|
|
40
|
+
className="pointer-events-none absolute top-1/2 right-2.5 size-4 -translate-y-1/2 text-muted-foreground select-none"
|
|
41
|
+
aria-hidden="true"
|
|
42
|
+
data-slot="native-select-icon"
|
|
43
|
+
/>
|
|
44
|
+
</div>
|
|
45
|
+
);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function NativeSelectOption({
|
|
49
|
+
className,
|
|
50
|
+
...props
|
|
51
|
+
}: React.ComponentProps<"option">) {
|
|
52
|
+
return (
|
|
53
|
+
<option
|
|
54
|
+
data-slot="native-select-option"
|
|
55
|
+
className={cn("bg-[Canvas] text-[CanvasText]", className)}
|
|
56
|
+
{...props}
|
|
57
|
+
/>
|
|
58
|
+
);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function NativeSelectOptGroup({
|
|
62
|
+
className,
|
|
63
|
+
...props
|
|
64
|
+
}: React.ComponentProps<"optgroup">) {
|
|
65
|
+
return (
|
|
66
|
+
<optgroup
|
|
67
|
+
data-slot="native-select-optgroup"
|
|
68
|
+
className={cn("bg-[Canvas] text-[CanvasText]", className)}
|
|
69
|
+
{...props}
|
|
70
|
+
/>
|
|
71
|
+
);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export { NativeSelect, NativeSelectOptGroup, NativeSelectOption };
|
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
import { NavigationMenu as NavigationMenuPrimitive } from "@base-ui/react/navigation-menu";
|
|
2
|
+
import { CaretDownIcon } from "@phosphor-icons/react";
|
|
3
|
+
import { cva } from "#/lib/cva.ts";
|
|
4
|
+
import { cn } from "#/lib/utils.ts";
|
|
5
|
+
|
|
6
|
+
function NavigationMenu({
|
|
7
|
+
align = "start",
|
|
8
|
+
alignOffset = 0,
|
|
9
|
+
side = "bottom",
|
|
10
|
+
sideOffset = 8,
|
|
11
|
+
collisionPadding,
|
|
12
|
+
className,
|
|
13
|
+
children,
|
|
14
|
+
...props
|
|
15
|
+
}: NavigationMenuPrimitive.Root.Props &
|
|
16
|
+
Pick<
|
|
17
|
+
NavigationMenuPrimitive.Positioner.Props,
|
|
18
|
+
"align" | "alignOffset" | "side" | "sideOffset" | "collisionPadding"
|
|
19
|
+
>) {
|
|
20
|
+
return (
|
|
21
|
+
<NavigationMenuPrimitive.Root
|
|
22
|
+
data-slot="navigation-menu"
|
|
23
|
+
className={cn(
|
|
24
|
+
"group/navigation-menu relative flex max-w-max flex-1 items-center justify-center",
|
|
25
|
+
className,
|
|
26
|
+
)}
|
|
27
|
+
{...props}
|
|
28
|
+
>
|
|
29
|
+
{children}
|
|
30
|
+
<NavigationMenuPositioner
|
|
31
|
+
align={align}
|
|
32
|
+
alignOffset={alignOffset}
|
|
33
|
+
side={side}
|
|
34
|
+
sideOffset={sideOffset}
|
|
35
|
+
collisionPadding={collisionPadding}
|
|
36
|
+
/>
|
|
37
|
+
</NavigationMenuPrimitive.Root>
|
|
38
|
+
);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function NavigationMenuList({
|
|
42
|
+
className,
|
|
43
|
+
...props
|
|
44
|
+
}: NavigationMenuPrimitive.List.Props) {
|
|
45
|
+
return (
|
|
46
|
+
<NavigationMenuPrimitive.List
|
|
47
|
+
data-slot="navigation-menu-list"
|
|
48
|
+
className={cn(
|
|
49
|
+
"group flex flex-1 list-none items-center justify-center gap-0",
|
|
50
|
+
className,
|
|
51
|
+
)}
|
|
52
|
+
{...props}
|
|
53
|
+
/>
|
|
54
|
+
);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function NavigationMenuItem({
|
|
58
|
+
className,
|
|
59
|
+
...props
|
|
60
|
+
}: NavigationMenuPrimitive.Item.Props) {
|
|
61
|
+
return (
|
|
62
|
+
<NavigationMenuPrimitive.Item
|
|
63
|
+
data-slot="navigation-menu-item"
|
|
64
|
+
className={cn("relative", className)}
|
|
65
|
+
{...props}
|
|
66
|
+
/>
|
|
67
|
+
);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const navigationMenuTriggerStyle = cva({
|
|
71
|
+
base: "group/navigation-menu-trigger inline-flex h-9 w-max items-center justify-center rounded-lg px-2.5 py-1.5 text-sm font-medium transition-all outline-none hover:bg-muted focus-visible:bg-muted focus-visible:ring-3 focus-visible:ring-ring/50 disabled:pointer-events-none disabled:opacity-50 data-popup-open:bg-muted/50 data-popup-open:hover:bg-muted data-popup-open:focus-visible:bg-muted",
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
function NavigationMenuTrigger({
|
|
75
|
+
className,
|
|
76
|
+
children,
|
|
77
|
+
...props
|
|
78
|
+
}: NavigationMenuPrimitive.Trigger.Props) {
|
|
79
|
+
return (
|
|
80
|
+
<NavigationMenuPrimitive.Trigger
|
|
81
|
+
data-slot="navigation-menu-trigger"
|
|
82
|
+
className={cn(navigationMenuTriggerStyle(), "group", className)}
|
|
83
|
+
{...props}
|
|
84
|
+
>
|
|
85
|
+
{children}{" "}
|
|
86
|
+
<CaretDownIcon
|
|
87
|
+
className="relative top-px ml-1 size-3 transition duration-300 group-data-popup-open/navigation-menu-trigger:rotate-180"
|
|
88
|
+
aria-hidden="true"
|
|
89
|
+
/>
|
|
90
|
+
</NavigationMenuPrimitive.Trigger>
|
|
91
|
+
);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function NavigationMenuContent({
|
|
95
|
+
className,
|
|
96
|
+
...props
|
|
97
|
+
}: NavigationMenuPrimitive.Content.Props) {
|
|
98
|
+
return (
|
|
99
|
+
<NavigationMenuPrimitive.Content
|
|
100
|
+
data-slot="navigation-menu-content"
|
|
101
|
+
className={cn(
|
|
102
|
+
"h-full w-auto p-1 transition-[opacity,transform,translate] duration-350 ease-fluid data-starting-style:opacity-0 data-starting-style:data-[activation-direction=left]:translate-x-[-50%] data-starting-style:data-[activation-direction=right]:translate-x-[50%] data-ending-style:opacity-0 data-ending-style:data-[activation-direction=left]:translate-x-[50%] data-ending-style:data-[activation-direction=right]:translate-x-[-50%] motion-reduce:transition-none **:data-[slot=navigation-menu-link]:focus:ring-0 **:data-[slot=navigation-menu-link]:focus:outline-none",
|
|
103
|
+
className,
|
|
104
|
+
)}
|
|
105
|
+
{...props}
|
|
106
|
+
/>
|
|
107
|
+
);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function NavigationMenuPositioner({
|
|
111
|
+
className,
|
|
112
|
+
side = "bottom",
|
|
113
|
+
sideOffset = 8,
|
|
114
|
+
align = "start",
|
|
115
|
+
alignOffset = 0,
|
|
116
|
+
...props
|
|
117
|
+
}: NavigationMenuPrimitive.Positioner.Props) {
|
|
118
|
+
return (
|
|
119
|
+
<NavigationMenuPrimitive.Portal>
|
|
120
|
+
<NavigationMenuPrimitive.Positioner
|
|
121
|
+
side={side}
|
|
122
|
+
sideOffset={sideOffset}
|
|
123
|
+
align={align}
|
|
124
|
+
alignOffset={alignOffset}
|
|
125
|
+
className={cn(
|
|
126
|
+
"isolate z-50 h-(--positioner-height) w-(--positioner-width) max-w-(--available-width) transition-[top,left,right,bottom] duration-350 ease-fluid motion-reduce:transition-none data-instant:transition-none data-[side=bottom]:before:top-[-10px] data-[side=bottom]:before:right-0 data-[side=bottom]:before:left-0",
|
|
127
|
+
className,
|
|
128
|
+
)}
|
|
129
|
+
{...props}
|
|
130
|
+
>
|
|
131
|
+
<NavigationMenuPrimitive.Popup className="relative h-(--popup-height) w-(--popup-width) origin-(--transform-origin) rounded-lg bg-popover text-popover-foreground shadow ring-1 ring-foreground/10 transition-[opacity,transform,width,height,scale,translate] duration-350 ease-fluid outline-none motion-reduce:transition-none data-ending-style:scale-90 data-ending-style:opacity-0 data-ending-style:duration-150 data-ending-style:ease-[ease] data-starting-style:scale-90 data-starting-style:opacity-0">
|
|
132
|
+
<NavigationMenuPrimitive.Viewport className="relative size-full overflow-hidden" />
|
|
133
|
+
</NavigationMenuPrimitive.Popup>
|
|
134
|
+
</NavigationMenuPrimitive.Positioner>
|
|
135
|
+
</NavigationMenuPrimitive.Portal>
|
|
136
|
+
);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
function NavigationMenuLink({
|
|
140
|
+
className,
|
|
141
|
+
...props
|
|
142
|
+
}: NavigationMenuPrimitive.Link.Props) {
|
|
143
|
+
return (
|
|
144
|
+
<NavigationMenuPrimitive.Link
|
|
145
|
+
data-slot="navigation-menu-link"
|
|
146
|
+
className={cn(
|
|
147
|
+
"flex items-center gap-2 rounded-lg p-2 text-sm transition-all outline-none hover:bg-muted focus-visible:bg-muted focus-visible:ring-3 focus-visible:ring-ring/50 in-data-[slot=navigation-menu-content]:rounded-md data-active:bg-muted/50 data-active:hover:bg-muted data-active:focus-visible:bg-muted [&_svg:not([class*='size-'])]:size-4",
|
|
148
|
+
className,
|
|
149
|
+
)}
|
|
150
|
+
{...props}
|
|
151
|
+
/>
|
|
152
|
+
);
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
function NavigationMenuIndicator({
|
|
156
|
+
className,
|
|
157
|
+
...props
|
|
158
|
+
}: NavigationMenuPrimitive.Icon.Props) {
|
|
159
|
+
return (
|
|
160
|
+
<NavigationMenuPrimitive.Icon
|
|
161
|
+
data-slot="navigation-menu-indicator"
|
|
162
|
+
className={cn(
|
|
163
|
+
"top-full z-1 flex h-1.5 items-end justify-center overflow-hidden",
|
|
164
|
+
className,
|
|
165
|
+
)}
|
|
166
|
+
{...props}
|
|
167
|
+
>
|
|
168
|
+
<div className="relative top-[60%] h-2 w-2 rotate-45 rounded-tl-sm bg-border shadow-md" />
|
|
169
|
+
</NavigationMenuPrimitive.Icon>
|
|
170
|
+
);
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
export {
|
|
174
|
+
NavigationMenu,
|
|
175
|
+
NavigationMenuContent,
|
|
176
|
+
NavigationMenuIndicator,
|
|
177
|
+
NavigationMenuItem,
|
|
178
|
+
NavigationMenuLink,
|
|
179
|
+
NavigationMenuList,
|
|
180
|
+
NavigationMenuPositioner,
|
|
181
|
+
NavigationMenuTrigger,
|
|
182
|
+
navigationMenuTriggerStyle,
|
|
183
|
+
};
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
import {
|
|
2
|
+
CaretLeftIcon,
|
|
3
|
+
CaretRightIcon,
|
|
4
|
+
DotsThreeIcon,
|
|
5
|
+
} from "@phosphor-icons/react";
|
|
6
|
+
import type * as React from "react";
|
|
7
|
+
import { Button } from "#/components/ui/button.tsx";
|
|
8
|
+
import { cn } from "#/lib/utils.ts";
|
|
9
|
+
|
|
10
|
+
function Pagination({ className, ...props }: React.ComponentProps<"nav">) {
|
|
11
|
+
return (
|
|
12
|
+
<nav
|
|
13
|
+
// Sentence case to match the kit's other nav labels; spread-overridable
|
|
14
|
+
// (e.g. a French `aria-label="Pagination"`) since `{...props}` wins.
|
|
15
|
+
aria-label="Pagination"
|
|
16
|
+
data-slot="pagination"
|
|
17
|
+
className={cn("mx-auto flex w-full justify-center", className)}
|
|
18
|
+
{...props}
|
|
19
|
+
/>
|
|
20
|
+
);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function PaginationContent({
|
|
24
|
+
className,
|
|
25
|
+
...props
|
|
26
|
+
}: React.ComponentProps<"ul">) {
|
|
27
|
+
return (
|
|
28
|
+
<ul
|
|
29
|
+
data-slot="pagination-content"
|
|
30
|
+
className={cn("flex items-center gap-0.5", className)}
|
|
31
|
+
{...props}
|
|
32
|
+
/>
|
|
33
|
+
);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function PaginationItem({ ...props }: React.ComponentProps<"li">) {
|
|
37
|
+
return <li data-slot="pagination-item" {...props} />;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
type PaginationLinkProps = {
|
|
41
|
+
isActive?: boolean;
|
|
42
|
+
/**
|
|
43
|
+
* Render the link as non-interactive (e.g. Previous on the first page):
|
|
44
|
+
* `aria-disabled`, removed from the tab order, and dimmed. Maps to the kit's
|
|
45
|
+
* `aria-disabled:` styling idiom rather than a dead `href`.
|
|
46
|
+
*/
|
|
47
|
+
isDisabled?: boolean;
|
|
48
|
+
} & Pick<React.ComponentProps<typeof Button>, "size" | "variant"> &
|
|
49
|
+
React.ComponentProps<"a">;
|
|
50
|
+
|
|
51
|
+
function PaginationLink({
|
|
52
|
+
className,
|
|
53
|
+
isActive,
|
|
54
|
+
isDisabled,
|
|
55
|
+
size = "icon",
|
|
56
|
+
variant,
|
|
57
|
+
...props
|
|
58
|
+
}: PaginationLinkProps) {
|
|
59
|
+
return (
|
|
60
|
+
<Button
|
|
61
|
+
// `variant` overrides the active/inactive default so consumers can opt
|
|
62
|
+
// into e.g. a `secondary` active page without re-implementing the link.
|
|
63
|
+
variant={variant ?? (isActive ? "outline" : "ghost")}
|
|
64
|
+
size={size}
|
|
65
|
+
className={cn(
|
|
66
|
+
"aria-disabled:pointer-events-none aria-disabled:opacity-50",
|
|
67
|
+
className,
|
|
68
|
+
)}
|
|
69
|
+
nativeButton={false}
|
|
70
|
+
render={
|
|
71
|
+
<a
|
|
72
|
+
data-slot="pagination-link"
|
|
73
|
+
data-active={isActive}
|
|
74
|
+
aria-current={isActive ? "page" : undefined}
|
|
75
|
+
{...props}
|
|
76
|
+
aria-disabled={isDisabled || undefined}
|
|
77
|
+
tabIndex={isDisabled ? -1 : props.tabIndex}
|
|
78
|
+
/>
|
|
79
|
+
}
|
|
80
|
+
/>
|
|
81
|
+
);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function PaginationPrevious({
|
|
85
|
+
className,
|
|
86
|
+
text = "Previous",
|
|
87
|
+
...props
|
|
88
|
+
}: React.ComponentProps<typeof PaginationLink> & { text?: string }) {
|
|
89
|
+
return (
|
|
90
|
+
<PaginationLink
|
|
91
|
+
aria-label="Go to previous page"
|
|
92
|
+
size="default"
|
|
93
|
+
className={cn("pl-1.5!", className)}
|
|
94
|
+
{...props}
|
|
95
|
+
>
|
|
96
|
+
<CaretLeftIcon data-icon="inline-start" />
|
|
97
|
+
<span className="hidden sm:block">{text}</span>
|
|
98
|
+
</PaginationLink>
|
|
99
|
+
);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function PaginationNext({
|
|
103
|
+
className,
|
|
104
|
+
text = "Next",
|
|
105
|
+
...props
|
|
106
|
+
}: React.ComponentProps<typeof PaginationLink> & { text?: string }) {
|
|
107
|
+
return (
|
|
108
|
+
<PaginationLink
|
|
109
|
+
aria-label="Go to next page"
|
|
110
|
+
size="default"
|
|
111
|
+
className={cn("pr-1.5!", className)}
|
|
112
|
+
{...props}
|
|
113
|
+
>
|
|
114
|
+
<span className="hidden sm:block">{text}</span>
|
|
115
|
+
<CaretRightIcon data-icon="inline-end" />
|
|
116
|
+
</PaginationLink>
|
|
117
|
+
);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
function PaginationEllipsis({
|
|
121
|
+
className,
|
|
122
|
+
...props
|
|
123
|
+
}: React.ComponentProps<"span">) {
|
|
124
|
+
return (
|
|
125
|
+
// Decorative gap indicator: `aria-hidden` hides it from the a11y tree, so
|
|
126
|
+
// it carries no sr-only text (the old one was unreachable inside the hidden
|
|
127
|
+
// subtree). The surrounding page links convey the skipped range.
|
|
128
|
+
<span
|
|
129
|
+
aria-hidden
|
|
130
|
+
data-slot="pagination-ellipsis"
|
|
131
|
+
className={cn(
|
|
132
|
+
"flex size-8 items-center justify-center [&_svg:not([class*='size-'])]:size-4",
|
|
133
|
+
className,
|
|
134
|
+
)}
|
|
135
|
+
{...props}
|
|
136
|
+
>
|
|
137
|
+
<DotsThreeIcon />
|
|
138
|
+
</span>
|
|
139
|
+
);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
export {
|
|
143
|
+
Pagination,
|
|
144
|
+
PaginationContent,
|
|
145
|
+
PaginationEllipsis,
|
|
146
|
+
PaginationItem,
|
|
147
|
+
PaginationLink,
|
|
148
|
+
PaginationNext,
|
|
149
|
+
PaginationPrevious,
|
|
150
|
+
};
|