@ponti-studios/ui 0.7.1 → 0.8.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 +13 -0
- package/package.json +22 -20
- package/src/components/data-display/accordion.tsx +1 -1
- package/src/components/data-display/sort-controls.tsx +3 -3
- package/src/components/feedback/index.ts +0 -2
- package/src/components/forms/date-month-select.tsx +12 -3
- package/src/components/forms/date-picker.tsx +16 -12
- package/src/components/forms/drop-zone.tsx +1 -1
- package/src/components/forms/index.ts +4 -8
- package/src/components/forms/passkey-management.tsx +4 -10
- package/src/components/forms/radio-group.tsx +8 -4
- package/src/components/forms/select.tsx +1 -1
- package/src/components/forms/text-field.tsx +40 -5
- package/src/components/navigation/app-navigation.stories.tsx +0 -1
- package/src/components/navigation/app-navigation.tsx +67 -61
- package/src/components/navigation/index.ts +7 -4
- package/src/components/navigation/pagination-controls.stories.tsx +1 -1
- package/src/components/navigation/pagination-controls.tsx +10 -14
- package/src/components/overlays/dropdown-menu.stories.tsx +2 -2
- package/src/components/primitives/card.tsx +6 -10
- package/src/components/primitives/label.stories.tsx +0 -18
- package/src/components/primitives/label.tsx +12 -12
- package/src/constants/chart-colors.ts +16 -16
- package/src/styles/animations.css +0 -22
- package/src/styles/index.css +60 -6
- package/src/styles/tokens/color-systems.stories.tsx +2 -1
- package/src/styles/tokens/generated/light.colors.generated.ts +3 -3
- package/src/styles/tokens/generated/light.css +3 -3
- package/src/styles/tokens/source/light.tokens.json +3 -3
- package/src/styles/tokens/style-dictionary.light-storybook.json +16 -0
- package/src/components/feedback/file-upload-status-badge.stories.tsx +0 -28
- package/src/components/feedback/file-upload-status-badge.tsx +0 -33
- package/src/components/forms/field.stories.tsx +0 -62
- package/src/components/forms/field.tsx +0 -65
- package/src/components/forms/field.types.ts +0 -7
- package/src/components/forms/group-by-select.stories.tsx +0 -26
- package/src/components/forms/group-by-select.tsx +0 -47
- package/src/components/navigation/route-link.stories.tsx +0 -30
- package/src/components/navigation/route-link.tsx +0 -42
- package/src/hooks/use-derived-filter-state.stories.tsx +0 -136
- package/src/hooks/use-filter-state.stories.tsx +0 -102
package/README.md
CHANGED
|
@@ -11,6 +11,19 @@ The design system is documented in `docs/`:
|
|
|
11
11
|
- [Patterns](./docs/patterns.md) — screen composition rules
|
|
12
12
|
- [Review gates](./docs/review-gates.md) — pass/fail review criteria
|
|
13
13
|
|
|
14
|
+
## Development
|
|
15
|
+
|
|
16
|
+
Common development workflows are available through the `Makefile`:
|
|
17
|
+
|
|
18
|
+
```bash
|
|
19
|
+
make check # tokens, source contracts, and typecheck
|
|
20
|
+
make tokens-build # regenerate token artifacts
|
|
21
|
+
make storybook # build Storybook themes and start Storybook
|
|
22
|
+
make test-a11y # run light and dark accessibility tests
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
The existing `pnpm run` scripts delegate to these targets for compatibility.
|
|
26
|
+
|
|
14
27
|
## Release
|
|
15
28
|
|
|
16
29
|
Releases are owned by [release-please](https://github.com/googleapis/release-please), not by hand-edited version numbers or manually pushed tags. The `.github/workflows/publish.yml` workflow runs after a successful `validate` push to `main`:
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ponti-studios/ui",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.8.0",
|
|
4
4
|
"private": false,
|
|
5
5
|
"description": "Shared UI component library for Ponti Studios",
|
|
6
6
|
"license": "MIT",
|
|
@@ -82,18 +82,20 @@
|
|
|
82
82
|
"registry": "https://registry.npmjs.org"
|
|
83
83
|
},
|
|
84
84
|
"scripts": {
|
|
85
|
-
"build": "
|
|
85
|
+
"build": "make build",
|
|
86
86
|
"dev": "vite",
|
|
87
|
-
"storybook": "
|
|
88
|
-
"check:source": "
|
|
89
|
-
"test": "
|
|
90
|
-
"test:a11y": "
|
|
91
|
-
"
|
|
92
|
-
"
|
|
93
|
-
"
|
|
94
|
-
"
|
|
95
|
-
"
|
|
96
|
-
"
|
|
87
|
+
"storybook": "make storybook",
|
|
88
|
+
"check:source": "make check-source",
|
|
89
|
+
"test": "make test",
|
|
90
|
+
"test:a11y": "make test-a11y",
|
|
91
|
+
"test:a11y:light": "make test-a11y-light",
|
|
92
|
+
"test:a11y:dark": "make test-a11y-dark",
|
|
93
|
+
"tokens:build": "make tokens-build",
|
|
94
|
+
"tokens:check": "make tokens-check",
|
|
95
|
+
"typecheck": "make typecheck",
|
|
96
|
+
"lint": "make lint",
|
|
97
|
+
"format": "make format",
|
|
98
|
+
"prepack": "make prepack"
|
|
97
99
|
},
|
|
98
100
|
"dependencies": {
|
|
99
101
|
"@base-ui/react": "^1.6.0",
|
|
@@ -107,13 +109,13 @@
|
|
|
107
109
|
},
|
|
108
110
|
"devDependencies": {
|
|
109
111
|
"@chromatic-com/storybook": "^5.2.1",
|
|
110
|
-
"@storybook/addon-a11y": "10.5.
|
|
111
|
-
"@storybook/addon-docs": "
|
|
112
|
-
"@storybook/addon-links": "
|
|
113
|
-
"@storybook/addon-onboarding": "
|
|
114
|
-
"@storybook/addon-themes": "10.5.
|
|
115
|
-
"@storybook/addon-vitest": "10.5.
|
|
116
|
-
"@storybook/react-vite": "
|
|
112
|
+
"@storybook/addon-a11y": "10.5.6",
|
|
113
|
+
"@storybook/addon-docs": "10.5.6",
|
|
114
|
+
"@storybook/addon-links": "10.5.6",
|
|
115
|
+
"@storybook/addon-onboarding": "10.5.6",
|
|
116
|
+
"@storybook/addon-themes": "10.5.6",
|
|
117
|
+
"@storybook/addon-vitest": "10.5.6",
|
|
118
|
+
"@storybook/react-vite": "10.5.6",
|
|
117
119
|
"@tailwindcss/vite": "^4.3.3",
|
|
118
120
|
"@types/node": "^26.1.1",
|
|
119
121
|
"@types/react": "^19.2.17",
|
|
@@ -127,7 +129,7 @@
|
|
|
127
129
|
"react": "^19.2.7",
|
|
128
130
|
"react-dom": "^19.2.7",
|
|
129
131
|
"react-native": "0.86.0",
|
|
130
|
-
"storybook": "
|
|
132
|
+
"storybook": "10.5.6",
|
|
131
133
|
"style-dictionary": "5.5.0",
|
|
132
134
|
"tailwindcss": "^4.3.3",
|
|
133
135
|
"typescript": "6.0.3",
|
|
@@ -46,7 +46,7 @@ function AccordionTrigger({
|
|
|
46
46
|
<AccordionPrimitive.Trigger
|
|
47
47
|
data-slot="accordion-trigger"
|
|
48
48
|
className={cn(
|
|
49
|
-
"flex w-full flex-1 items-center justify-between gap-4 px-4 py-3 text-left text-sm font-semibold disabled:pointer-events-none disabled:opacity-50 [&[data-state=open]>svg]:rotate-180",
|
|
49
|
+
"text-muted-foreground flex w-full flex-1 items-center justify-between gap-4 px-4 py-3 text-left text-sm font-semibold disabled:pointer-events-none disabled:opacity-50 [&[data-state=open]>svg]:rotate-180",
|
|
50
50
|
className,
|
|
51
51
|
)}
|
|
52
52
|
{...props}
|
|
@@ -48,10 +48,10 @@ export function SortControls({
|
|
|
48
48
|
</Button>
|
|
49
49
|
</DropdownMenuTrigger>
|
|
50
50
|
<DropdownMenuContent className="w-80 space-y-2 p-2">
|
|
51
|
-
<DropdownMenuLabel>Define Sort Order</DropdownMenuLabel>
|
|
52
|
-
<DropdownMenuSeparator />
|
|
53
|
-
|
|
54
51
|
<DropdownMenuGroup className="space-y-1">
|
|
52
|
+
<DropdownMenuLabel>Define Sort Order</DropdownMenuLabel>
|
|
53
|
+
<DropdownMenuSeparator />
|
|
54
|
+
|
|
55
55
|
{sortOptions.map((sort, index) => {
|
|
56
56
|
const usedFields = sortOptions
|
|
57
57
|
.filter((_, i) => i !== index)
|
|
@@ -3,8 +3,6 @@ export { EmptyState } from "./empty-state";
|
|
|
3
3
|
export type { EmptyStateProps } from "./empty-state";
|
|
4
4
|
export { FileUploadStatus } from "./file-upload-status";
|
|
5
5
|
export type { FileUploadState, FileUploadStats } from "./file-upload-status";
|
|
6
|
-
export { FileUploadStatusBadge } from "./file-upload-status-badge";
|
|
7
|
-
export type { FileUploadStatusValue } from "./file-upload-status-badge";
|
|
8
6
|
export { Progress } from "./progress";
|
|
9
7
|
export { ProgressBar } from "./progress-bar";
|
|
10
8
|
export type { ProgressBarProps } from "./progress-bar";
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { Calendar } from "lucide-react";
|
|
2
2
|
import { useMemo } from "react";
|
|
3
3
|
|
|
4
|
+
import { cn } from "../../lib/utils";
|
|
4
5
|
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "./select";
|
|
5
6
|
|
|
6
7
|
interface MonthOption {
|
|
@@ -40,13 +41,21 @@ export function DateMonthSelect({
|
|
|
40
41
|
}, [monthsBack]);
|
|
41
42
|
|
|
42
43
|
return (
|
|
43
|
-
<div className="flex items-center
|
|
44
|
-
<
|
|
44
|
+
<div className="flex items-center">
|
|
45
|
+
<div className="flex items-center justify-center size-10 border rounded-l-xl bg-surface-2 text-text-1">
|
|
46
|
+
<Calendar className="size-4" />
|
|
47
|
+
</div>
|
|
45
48
|
<Select
|
|
46
49
|
value={selectedMonthYear}
|
|
47
50
|
onValueChange={(v: string | null) => v != null && onMonthChange(v)}
|
|
48
51
|
>
|
|
49
|
-
<SelectTrigger
|
|
52
|
+
<SelectTrigger
|
|
53
|
+
className={cn(
|
|
54
|
+
"rounded-r-xl rounded-l-none min-h-10 max-h-10 bg-surface-2 text-text-1 border-l-0",
|
|
55
|
+
className,
|
|
56
|
+
)}
|
|
57
|
+
aria-label={placeholder}
|
|
58
|
+
>
|
|
50
59
|
<SelectValue placeholder={placeholder} />
|
|
51
60
|
</SelectTrigger>
|
|
52
61
|
<SelectContent>
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import { format } from "date-fns";
|
|
2
2
|
import { CalendarIcon } from "lucide-react";
|
|
3
3
|
|
|
4
|
-
import { Button } from "../primitives/button";
|
|
5
4
|
import { Calendar } from "../forms/calendar";
|
|
6
5
|
import { Popover, PopoverContent, PopoverTrigger } from "../overlays/popover";
|
|
6
|
+
import { Button } from "../primitives/button";
|
|
7
7
|
|
|
8
8
|
export interface DatePickerProps {
|
|
9
9
|
value: Date | undefined;
|
|
@@ -40,17 +40,21 @@ export function DatePicker({
|
|
|
40
40
|
</label>
|
|
41
41
|
) : null}
|
|
42
42
|
<Popover>
|
|
43
|
-
<
|
|
44
|
-
<
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
43
|
+
<div className="flex w-full items-center">
|
|
44
|
+
<div className="border-border bg-surface-2 text-text-1 flex size-10 shrink-0 items-center justify-center rounded-l-xl border">
|
|
45
|
+
<CalendarIcon className="size-4" aria-hidden />
|
|
46
|
+
</div>
|
|
47
|
+
<PopoverTrigger asChild>
|
|
48
|
+
<Button
|
|
49
|
+
id={id}
|
|
50
|
+
variant={variant}
|
|
51
|
+
disabled={disabled}
|
|
52
|
+
className="border-border bg-surface-2 text-text-1 placeholder:text-muted-foreground min-h-10 max-h-10 w-full justify-start rounded-l-none rounded-r-xl border-l-0 text-left font-normal"
|
|
53
|
+
>
|
|
54
|
+
{value ? format(value, dateFormat) : <span>{placeholder}</span>}
|
|
55
|
+
</Button>
|
|
56
|
+
</PopoverTrigger>
|
|
57
|
+
</div>
|
|
54
58
|
<PopoverContent className="bg-popover w-auto p-0" align={popoverAlign}>
|
|
55
59
|
<Calendar mode="single" selected={value} onSelect={onSelect} />
|
|
56
60
|
</PopoverContent>
|
|
@@ -126,7 +126,7 @@ export function DropZone({
|
|
|
126
126
|
>
|
|
127
127
|
<div className="space-y-2 text-center">
|
|
128
128
|
<p className="text-muted-foreground">
|
|
129
|
-
{label} <span className="text-
|
|
129
|
+
{label} <span className="text-accent font-medium">click to browse</span>
|
|
130
130
|
</p>
|
|
131
131
|
{helpText ? <p className="text-muted-foreground text-sm">{helpText}</p> : null}
|
|
132
132
|
</div>
|
|
@@ -1,17 +1,13 @@
|
|
|
1
|
+
export { FilterSelect } from "../filters/filter-select";
|
|
2
|
+
export type { FilterSelectOption, FilterSelectProps } from "../filters/filter-select";
|
|
1
3
|
export { Calendar } from "./calendar";
|
|
2
4
|
export type { CalendarProps } from "./calendar";
|
|
3
|
-
export { DatePicker } from "./date-picker";
|
|
4
|
-
export type { DatePickerProps } from "./date-picker";
|
|
5
5
|
export { DateMonthSelect, getCurrentMonthYear } from "./date-month-select";
|
|
6
6
|
export type { DateMonthSelectProps } from "./date-month-select";
|
|
7
|
+
export { DatePicker } from "./date-picker";
|
|
8
|
+
export type { DatePickerProps } from "./date-picker";
|
|
7
9
|
export { DropZone } from "./drop-zone";
|
|
8
10
|
export type { DropZoneProps } from "./drop-zone";
|
|
9
|
-
export { Field } from "./field";
|
|
10
|
-
export type { FieldProps } from "./field";
|
|
11
|
-
export { FilterSelect } from "../filters/filter-select";
|
|
12
|
-
export type { FilterSelectOption, FilterSelectProps } from "../filters/filter-select";
|
|
13
|
-
export { GroupBySelect } from "./group-by-select";
|
|
14
|
-
export type { GroupBySelectProps } from "./group-by-select";
|
|
15
11
|
export { Input } from "./input";
|
|
16
12
|
export { PasskeyManagement } from "./passkey-management";
|
|
17
13
|
export type { PasskeyManagementProps, PasskeyRecord } from "./passkey-management";
|
|
@@ -68,17 +68,11 @@ export function PasskeyManagement({
|
|
|
68
68
|
return (
|
|
69
69
|
<section aria-labelledby="passkey-heading" className="space-y-4">
|
|
70
70
|
<div className="flex items-center justify-between">
|
|
71
|
-
<
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
</h2>
|
|
75
|
-
<p className="text-muted-foreground text-sm">
|
|
76
|
-
Sign in without a password using biometrics or a security key.
|
|
77
|
-
</p>
|
|
78
|
-
</div>
|
|
71
|
+
<h2 id="passkey-heading" className="text-foreground text-sm font-medium">
|
|
72
|
+
Passkeys
|
|
73
|
+
</h2>
|
|
79
74
|
<Button
|
|
80
75
|
type="button"
|
|
81
|
-
variant="outline"
|
|
82
76
|
size="sm"
|
|
83
77
|
onClick={() => void handleAdd()}
|
|
84
78
|
disabled={adding}
|
|
@@ -107,7 +101,7 @@ export function PasskeyManagement({
|
|
|
107
101
|
{passkeys.map((pk) => (
|
|
108
102
|
<li
|
|
109
103
|
key={pk.id}
|
|
110
|
-
className="border-border flex items-center justify-between border px-4 py-3 text-sm"
|
|
104
|
+
className="border-border rounded flex items-center justify-between border px-4 py-3 text-sm"
|
|
111
105
|
>
|
|
112
106
|
<div className="flex items-center gap-3">
|
|
113
107
|
<KeyRound className="text-muted-foreground size-4 shrink-0" aria-hidden />
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
|
|
3
|
-
import { RadioGroup as RadioGroupPrimitive } from "@base-ui/react/radio-group";
|
|
4
3
|
import { Radio as RadioPrimitive } from "@base-ui/react/radio";
|
|
4
|
+
import { RadioGroup as RadioGroupPrimitive } from "@base-ui/react/radio-group";
|
|
5
5
|
import { Circle } from "lucide-react";
|
|
6
6
|
import * as React from "react";
|
|
7
7
|
|
|
@@ -23,13 +23,17 @@ const RadioGroupItem = React.forwardRef<
|
|
|
23
23
|
<RadioPrimitive.Root
|
|
24
24
|
ref={ref}
|
|
25
25
|
className={cn(
|
|
26
|
-
"border-primary bg-background
|
|
26
|
+
"border-primary bg-background focus-visible:border-ring focus-visible:bg-muted/25 focus-visible:ring-ring aspect-square size-6 rounded-full border focus-visible:ring-2 focus-visible:ring-offset-2 focus-visible:outline-none disabled:cursor-not-allowed disabled:opacity-50",
|
|
27
|
+
"flex items-center justify-center",
|
|
27
28
|
className,
|
|
28
29
|
)}
|
|
29
30
|
{...props}
|
|
30
31
|
>
|
|
31
|
-
<RadioPrimitive.Indicator
|
|
32
|
-
|
|
32
|
+
<RadioPrimitive.Indicator
|
|
33
|
+
keepMounted
|
|
34
|
+
className="data-unchecked:opacity-0 data-checked:opacity-100 data-starting-style:animate-fade-in data-ending-style:animate-fade-out"
|
|
35
|
+
>
|
|
36
|
+
<Circle className="fill-primary size-5" />
|
|
33
37
|
</RadioPrimitive.Indicator>
|
|
34
38
|
</RadioPrimitive.Root>
|
|
35
39
|
);
|
|
@@ -71,7 +71,7 @@ function SelectContent({
|
|
|
71
71
|
<SelectPrimitive.Popup
|
|
72
72
|
data-slot="select-content"
|
|
73
73
|
className={cn(
|
|
74
|
-
"bg-
|
|
74
|
+
"bg-background text-popover-foreground data-open:animate-in data-closed:animate-out data-closed:zoom-out-95 data-open:zoom-in-95 relative z-50 max-h-(--available-height) min-w-32 origin-(--transform-origin) overflow-x-hidden overflow-y-auto rounded-md border p-1 outline-none",
|
|
75
75
|
className,
|
|
76
76
|
)}
|
|
77
77
|
{...props}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import * as React from "react";
|
|
2
2
|
|
|
3
|
-
import {
|
|
3
|
+
import { cn } from "../../lib/utils";
|
|
4
4
|
import { Input } from "./input";
|
|
5
5
|
import type { TextFieldBaseProps, TextFieldType } from "./text-field.types";
|
|
6
6
|
|
|
@@ -26,14 +26,49 @@ function TextField({
|
|
|
26
26
|
type = "text",
|
|
27
27
|
...inputProps
|
|
28
28
|
}: TextFieldProps) {
|
|
29
|
+
const generatedId = React.useId();
|
|
30
|
+
const inputId = id ?? generatedId;
|
|
31
|
+
const descriptionId = `${inputId}-description`;
|
|
32
|
+
const errorId = `${inputId}-error`;
|
|
33
|
+
|
|
29
34
|
if (!label && !helpText && !error) {
|
|
30
|
-
return
|
|
35
|
+
return (
|
|
36
|
+
<Input id={inputId} className={className} disabled={disabled} type={type} {...inputProps} />
|
|
37
|
+
);
|
|
31
38
|
}
|
|
32
39
|
|
|
33
40
|
return (
|
|
34
|
-
<
|
|
35
|
-
|
|
36
|
-
|
|
41
|
+
<div className="flex flex-col gap-1.5">
|
|
42
|
+
{label && (
|
|
43
|
+
<label
|
|
44
|
+
htmlFor={inputId}
|
|
45
|
+
className={cn(
|
|
46
|
+
"text-foreground text-sm font-medium",
|
|
47
|
+
inputProps.required && "after:text-destructive-text after:ml-0.5 after:content-['*']",
|
|
48
|
+
)}
|
|
49
|
+
>
|
|
50
|
+
{label}
|
|
51
|
+
</label>
|
|
52
|
+
)}
|
|
53
|
+
<Input
|
|
54
|
+
id={inputId}
|
|
55
|
+
className={className}
|
|
56
|
+
disabled={disabled}
|
|
57
|
+
type={type}
|
|
58
|
+
aria-describedby={error ? errorId : helpText ? descriptionId : undefined}
|
|
59
|
+
aria-invalid={error ? true : undefined}
|
|
60
|
+
{...inputProps}
|
|
61
|
+
/>
|
|
62
|
+
{error ? (
|
|
63
|
+
<p id={errorId} role="alert" className="text-destructive-text text-sm">
|
|
64
|
+
{error}
|
|
65
|
+
</p>
|
|
66
|
+
) : helpText ? (
|
|
67
|
+
<p id={descriptionId} className="text-muted-foreground text-sm">
|
|
68
|
+
{helpText}
|
|
69
|
+
</p>
|
|
70
|
+
) : null}
|
|
71
|
+
</div>
|
|
37
72
|
);
|
|
38
73
|
}
|
|
39
74
|
|
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
import { Menu } from "lucide-react";
|
|
2
|
+
import type { ComponentPropsWithoutRef } from "react";
|
|
2
3
|
import * as React from "react";
|
|
3
4
|
|
|
4
|
-
import {
|
|
5
|
+
import { cn } from "../../lib/utils";
|
|
5
6
|
import { Sheet, SheetClose, SheetContent, SheetTitle, SheetTrigger } from "../overlays/sheet";
|
|
7
|
+
import { Button } from "../primitives/button";
|
|
6
8
|
|
|
7
9
|
export interface AppNavigationLink {
|
|
8
10
|
href: string;
|
|
@@ -24,15 +26,47 @@ export interface AppNavigationProps {
|
|
|
24
26
|
ariaLabel?: string;
|
|
25
27
|
/** Current pathname used to highlight the active link. */
|
|
26
28
|
activeHref?: string;
|
|
27
|
-
/** A router link component. Native anchors are used when omitted. */
|
|
28
|
-
linkComponent?: React.ElementType;
|
|
29
|
-
/** The prop used by the link component for its destination. */
|
|
30
|
-
linkProp?: "href" | "to";
|
|
31
|
-
/** Props shared by every link, such as a router's prefetch hint. */
|
|
32
|
-
linkProps?: Record<string, unknown>;
|
|
33
29
|
}
|
|
34
30
|
|
|
35
|
-
|
|
31
|
+
interface NavigationLinkOwnProps {
|
|
32
|
+
active?: boolean;
|
|
33
|
+
brand?: boolean;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export type NavigationLinkProps = NavigationLinkOwnProps &
|
|
37
|
+
Omit<ComponentPropsWithoutRef<"a">, keyof NavigationLinkOwnProps>;
|
|
38
|
+
|
|
39
|
+
function NavigationLink({
|
|
40
|
+
children,
|
|
41
|
+
active = false,
|
|
42
|
+
brand = false,
|
|
43
|
+
className,
|
|
44
|
+
...props
|
|
45
|
+
}: NavigationLinkProps) {
|
|
46
|
+
return (
|
|
47
|
+
<a
|
|
48
|
+
{...props}
|
|
49
|
+
className={cn(
|
|
50
|
+
"inline-flex h-9 max-h-9 shrink-0 items-center px-3 py-2 text-sm font-medium",
|
|
51
|
+
!brand && "hover:bg-muted hover:text-muted-foreground",
|
|
52
|
+
className,
|
|
53
|
+
)}
|
|
54
|
+
aria-current={active ? "page" : undefined}
|
|
55
|
+
>
|
|
56
|
+
{children}
|
|
57
|
+
</a>
|
|
58
|
+
);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function NavigationCta({ href, label }: AppNavigationCta) {
|
|
62
|
+
return (
|
|
63
|
+
<Button asChild size="md">
|
|
64
|
+
<a href={href}>{label}</a>
|
|
65
|
+
</Button>
|
|
66
|
+
);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function NavigationRoot({
|
|
36
70
|
brand,
|
|
37
71
|
brandHref = "/",
|
|
38
72
|
endContent,
|
|
@@ -40,9 +74,6 @@ export function AppNavigation({
|
|
|
40
74
|
cta,
|
|
41
75
|
ariaLabel = "Primary navigation",
|
|
42
76
|
activeHref,
|
|
43
|
-
linkComponent: LinkComponent = "a",
|
|
44
|
-
linkProp = "href",
|
|
45
|
-
linkProps,
|
|
46
77
|
}: AppNavigationProps) {
|
|
47
78
|
const [mobileOpen, setMobileOpen] = React.useState(false);
|
|
48
79
|
|
|
@@ -53,55 +84,25 @@ export function AppNavigation({
|
|
|
53
84
|
return href !== "/" && activeHref.startsWith(`${href}/`);
|
|
54
85
|
};
|
|
55
86
|
|
|
56
|
-
const createLink = ({
|
|
57
|
-
href,
|
|
58
|
-
children,
|
|
59
|
-
active = false,
|
|
60
|
-
variant,
|
|
61
|
-
brand = false,
|
|
62
|
-
}: {
|
|
63
|
-
href: string;
|
|
64
|
-
children: React.ReactNode;
|
|
65
|
-
active?: boolean;
|
|
66
|
-
variant?: AppNavigationCta["variant"];
|
|
67
|
-
brand?: boolean;
|
|
68
|
-
}) =>
|
|
69
|
-
React.createElement(
|
|
70
|
-
LinkComponent,
|
|
71
|
-
{
|
|
72
|
-
...linkProps,
|
|
73
|
-
...(linkProp === "to" ? { to: href } : { href }),
|
|
74
|
-
className: brand
|
|
75
|
-
? "inline-flex min-h-9 shrink-0 items-center rounded-md px-3 py-2 text-sm font-medium"
|
|
76
|
-
: variant
|
|
77
|
-
? "inline-flex min-h-9 shrink-0 items-center justify-center rounded-md px-3 py-2 text-sm font-medium"
|
|
78
|
-
: "inline-flex min-h-9 shrink-0 items-center rounded-md px-3 py-2 text-sm font-medium hover:bg-muted hover:text-muted-foreground",
|
|
79
|
-
"aria-current": active ? "page" : undefined,
|
|
80
|
-
"data-variant": variant,
|
|
81
|
-
},
|
|
82
|
-
children,
|
|
83
|
-
);
|
|
84
|
-
|
|
85
87
|
return (
|
|
86
|
-
<header className="bg-background/95 supports-
|
|
88
|
+
<header className="bg-background/95 supports-backdrop-filter:bg-background/60 sticky top-0 z-50 flex w-full justify-center border-b px-4 backdrop-blur">
|
|
87
89
|
<nav className="w-full max-w-7xl" aria-label={ariaLabel}>
|
|
88
90
|
<div className="flex min-h-14 items-center justify-between gap-6">
|
|
89
91
|
<div className="flex min-w-0 items-center">
|
|
90
|
-
{brand &&
|
|
92
|
+
{brand && (
|
|
93
|
+
<NavigationLink href={brandHref} brand>
|
|
94
|
+
{brand}
|
|
95
|
+
</NavigationLink>
|
|
96
|
+
)}
|
|
91
97
|
</div>
|
|
92
98
|
|
|
93
99
|
<div className="ml-auto hidden min-w-0 items-center gap-1 sm:flex">
|
|
94
100
|
{links?.map((link) => (
|
|
95
|
-
<
|
|
96
|
-
{
|
|
97
|
-
</
|
|
101
|
+
<NavigationLink key={link.href} href={link.href} active={isActive(link.href)}>
|
|
102
|
+
{link.label}
|
|
103
|
+
</NavigationLink>
|
|
98
104
|
))}
|
|
99
|
-
{cta &&
|
|
100
|
-
createLink({
|
|
101
|
-
href: cta.href,
|
|
102
|
-
children: cta.label,
|
|
103
|
-
variant: cta.variant ?? "default",
|
|
104
|
-
})}
|
|
105
|
+
{cta && <NavigationCta {...cta} />}
|
|
105
106
|
{endContent}
|
|
106
107
|
</div>
|
|
107
108
|
|
|
@@ -122,11 +123,9 @@ export function AppNavigation({
|
|
|
122
123
|
<div className="mt-8 flex flex-1 flex-col gap-1">
|
|
123
124
|
{links?.map((link) => (
|
|
124
125
|
<SheetClose asChild key={link.href}>
|
|
125
|
-
{
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
active: isActive(link.href),
|
|
129
|
-
})}
|
|
126
|
+
<NavigationLink href={link.href} active={isActive(link.href)}>
|
|
127
|
+
{link.label}
|
|
128
|
+
</NavigationLink>
|
|
130
129
|
</SheetClose>
|
|
131
130
|
))}
|
|
132
131
|
</div>
|
|
@@ -135,11 +134,7 @@ export function AppNavigation({
|
|
|
135
134
|
<div className="flex flex-col gap-4 border-t pt-4">
|
|
136
135
|
{cta && (
|
|
137
136
|
<SheetClose asChild>
|
|
138
|
-
{
|
|
139
|
-
href: cta.href,
|
|
140
|
-
children: cta.label,
|
|
141
|
-
variant: cta.variant ?? "default",
|
|
142
|
-
})}
|
|
137
|
+
<NavigationCta {...cta} />
|
|
143
138
|
</SheetClose>
|
|
144
139
|
)}
|
|
145
140
|
{endContent && <div className="flex items-center gap-3">{endContent}</div>}
|
|
@@ -153,3 +148,14 @@ export function AppNavigation({
|
|
|
153
148
|
</header>
|
|
154
149
|
);
|
|
155
150
|
}
|
|
151
|
+
|
|
152
|
+
type NavigationComponent = typeof NavigationRoot & {
|
|
153
|
+
Link: typeof NavigationLink;
|
|
154
|
+
};
|
|
155
|
+
|
|
156
|
+
export const Navigation: NavigationComponent = Object.assign(NavigationRoot, {
|
|
157
|
+
Link: NavigationLink,
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
/** @deprecated Use `Navigation` instead. */
|
|
161
|
+
export const AppNavigation = Navigation;
|
|
@@ -1,7 +1,10 @@
|
|
|
1
|
-
export { AppNavigation } from "./app-navigation";
|
|
2
|
-
export type {
|
|
1
|
+
export { AppNavigation, Navigation } from "./app-navigation";
|
|
2
|
+
export type {
|
|
3
|
+
AppNavigationCta,
|
|
4
|
+
AppNavigationLink,
|
|
5
|
+
AppNavigationProps,
|
|
6
|
+
NavigationLinkProps,
|
|
7
|
+
} from "./app-navigation";
|
|
3
8
|
export { PaginationControls } from "./pagination-controls";
|
|
4
9
|
export type { PaginationControlsProps } from "./pagination-controls";
|
|
5
|
-
export { RouteLink } from "./route-link";
|
|
6
|
-
export type { RouteLinkProps } from "./route-link";
|
|
7
10
|
export { Tabs, TabsContent, TabsList, TabsTrigger } from "./tabs";
|
|
@@ -35,7 +35,7 @@ export const Default: Story = {
|
|
|
35
35
|
};
|
|
36
36
|
|
|
37
37
|
export const SinglePage: Story = {
|
|
38
|
-
name: "Single page (
|
|
38
|
+
name: "Single page (disabled controls)",
|
|
39
39
|
args: { ...baseArgs, totalPages: 1 },
|
|
40
40
|
render: () => <PaginationPreview totalPages={1} />,
|
|
41
41
|
};
|
|
@@ -8,41 +8,37 @@ export interface PaginationControlsProps {
|
|
|
8
8
|
onPageChange: (page: number) => void;
|
|
9
9
|
}
|
|
10
10
|
|
|
11
|
-
/** Zero-indexed prev/next pagination control.
|
|
11
|
+
/** Zero-indexed prev/next pagination control. */
|
|
12
12
|
export function PaginationControls({
|
|
13
13
|
currentPage,
|
|
14
14
|
totalPages,
|
|
15
15
|
onPageChange,
|
|
16
16
|
}: PaginationControlsProps) {
|
|
17
|
-
if (totalPages <= 1) {
|
|
18
|
-
return null;
|
|
19
|
-
}
|
|
20
|
-
|
|
21
17
|
return (
|
|
22
|
-
<div className="text-
|
|
18
|
+
<div className="border-border bg-background text-foreground inline-flex items-center overflow-hidden rounded-full border text-sm">
|
|
23
19
|
<Button
|
|
24
20
|
type="button"
|
|
25
21
|
variant="outline"
|
|
26
|
-
size="
|
|
27
|
-
className="border-
|
|
22
|
+
size="icon"
|
|
23
|
+
className="rounded-none border-0 border-r bg-transparent shadow-none"
|
|
28
24
|
onClick={() => onPageChange(currentPage - 1)}
|
|
29
25
|
disabled={currentPage === 0}
|
|
26
|
+
aria-label="Previous page"
|
|
30
27
|
>
|
|
31
28
|
<ChevronLeft className="size-4" aria-hidden />
|
|
32
|
-
Previous
|
|
33
29
|
</Button>
|
|
34
|
-
<span className="text-
|
|
35
|
-
|
|
30
|
+
<span className="min-w-20 px-3 text-center text-xs tabular-nums">
|
|
31
|
+
{currentPage + 1} of {totalPages}
|
|
36
32
|
</span>
|
|
37
33
|
<Button
|
|
38
34
|
type="button"
|
|
39
35
|
variant="outline"
|
|
40
|
-
size="
|
|
41
|
-
className="border-
|
|
36
|
+
size="icon"
|
|
37
|
+
className="rounded-none border-0 border-l bg-transparent shadow-none"
|
|
42
38
|
onClick={() => onPageChange(currentPage + 1)}
|
|
43
39
|
disabled={currentPage >= totalPages - 1}
|
|
40
|
+
aria-label="Next page"
|
|
44
41
|
>
|
|
45
|
-
Next
|
|
46
42
|
<ChevronRight className="size-4" aria-hidden />
|
|
47
43
|
</Button>
|
|
48
44
|
</div>
|
|
@@ -44,9 +44,9 @@ export const Default: Story = {
|
|
|
44
44
|
<Button variant="outline">Open Menu</Button>
|
|
45
45
|
</DropdownMenuTrigger>
|
|
46
46
|
<DropdownMenuContent className="w-56">
|
|
47
|
-
<DropdownMenuLabel>My Account</DropdownMenuLabel>
|
|
48
|
-
<DropdownMenuSeparator />
|
|
49
47
|
<DropdownMenuGroup>
|
|
48
|
+
<DropdownMenuLabel>My Account</DropdownMenuLabel>
|
|
49
|
+
<DropdownMenuSeparator />
|
|
50
50
|
<DropdownMenuItem>
|
|
51
51
|
<User className="mr-2 size-4" />
|
|
52
52
|
<span>Profile</span>
|