@ponti-studios/ui 0.7.0 → 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/feedback/update-guard.tsx +5 -1
- package/src/components/filters/filter-select.tsx +1 -1
- 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/hooks/use-media-query.ts +5 -1
- 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
|
@@ -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>
|
|
@@ -6,7 +6,7 @@ function Card({ className, ...props }: React.ComponentProps<"div">) {
|
|
|
6
6
|
return (
|
|
7
7
|
<div
|
|
8
8
|
data-slot="card"
|
|
9
|
-
className={cn("bg-card text-card-foreground flex flex-col
|
|
9
|
+
className={cn("bg-card text-card-foreground flex flex-col gap-4 rounded border", className)}
|
|
10
10
|
{...props}
|
|
11
11
|
/>
|
|
12
12
|
);
|
|
@@ -17,7 +17,7 @@ function CardHeader({ className, ...props }: React.ComponentProps<"div">) {
|
|
|
17
17
|
<div
|
|
18
18
|
data-slot="card-header"
|
|
19
19
|
className={cn(
|
|
20
|
-
"flex items-
|
|
20
|
+
"flex flex-col items-start justify-between gap-1 p-4 has-data-[slot=card-action]:grid has-data-[slot=card-action]:grid-cols-[1fr_auto]",
|
|
21
21
|
className,
|
|
22
22
|
)}
|
|
23
23
|
{...props}
|
|
@@ -26,14 +26,14 @@ function CardHeader({ className, ...props }: React.ComponentProps<"div">) {
|
|
|
26
26
|
}
|
|
27
27
|
|
|
28
28
|
function CardTitle({ className, ...props }: React.ComponentProps<"div">) {
|
|
29
|
-
return <div data-slot="card-title" className={cn(className)} {...props} />;
|
|
29
|
+
return <div data-slot="card-title" className={cn("text-xl", className)} {...props} />;
|
|
30
30
|
}
|
|
31
31
|
|
|
32
32
|
function CardDescription({ className, ...props }: React.ComponentProps<"div">) {
|
|
33
33
|
return (
|
|
34
34
|
<div
|
|
35
35
|
data-slot="card-description"
|
|
36
|
-
className={cn("text-muted-foreground text-
|
|
36
|
+
className={cn("text-muted-foreground text-xs", className)}
|
|
37
37
|
{...props}
|
|
38
38
|
/>
|
|
39
39
|
);
|
|
@@ -55,12 +55,8 @@ function CardContent({ className, ...props }: React.ComponentProps<"div">) {
|
|
|
55
55
|
|
|
56
56
|
function CardFooter({ className, ...props }: React.ComponentProps<"div">) {
|
|
57
57
|
return (
|
|
58
|
-
<div
|
|
59
|
-
data-slot="card-footer"
|
|
60
|
-
className={cn("flex items-center border-t px-4 py-3", className)}
|
|
61
|
-
{...props}
|
|
62
|
-
/>
|
|
58
|
+
<div data-slot="card-footer" className={cn("flex items-center p-4", className)} {...props} />
|
|
63
59
|
);
|
|
64
60
|
}
|
|
65
61
|
|
|
66
|
-
export { Card,
|
|
62
|
+
export { Card, CardAction, CardContent, CardDescription, CardFooter, CardHeader, CardTitle };
|
|
@@ -27,21 +27,3 @@ export const WithInput: Story = {
|
|
|
27
27
|
</div>
|
|
28
28
|
),
|
|
29
29
|
};
|
|
30
|
-
|
|
31
|
-
export const Disabled: Story = {
|
|
32
|
-
render: () => (
|
|
33
|
-
<div className="flex w-full max-w-sm flex-col gap-2">
|
|
34
|
-
<Label htmlFor="disabled-field">Subscription plan</Label>
|
|
35
|
-
<div className="flex items-center gap-3">
|
|
36
|
-
<Input id="disabled-field" className="peer" disabled placeholder="Enterprise" />
|
|
37
|
-
<Label htmlFor="disabled-field" className="shrink-0">
|
|
38
|
-
(locked)
|
|
39
|
-
</Label>
|
|
40
|
-
</div>
|
|
41
|
-
<p className="text-muted-foreground text-xs">
|
|
42
|
-
The <code className="font-mono">peer-disabled</code> variant mutes the label when its paired
|
|
43
|
-
input is disabled.
|
|
44
|
-
</p>
|
|
45
|
-
</div>
|
|
46
|
-
),
|
|
47
|
-
};
|
|
@@ -1,20 +1,20 @@
|
|
|
1
|
-
"use client";
|
|
2
|
-
|
|
3
|
-
import { cva, type VariantProps } from "class-variance-authority";
|
|
4
1
|
import * as React from "react";
|
|
5
2
|
|
|
6
3
|
import { cn } from "../../lib/utils";
|
|
7
4
|
|
|
8
|
-
const
|
|
9
|
-
|
|
5
|
+
const Label = React.forwardRef<HTMLLabelElement, React.LabelHTMLAttributes<HTMLLabelElement>>(
|
|
6
|
+
({ className, ...props }, ref) => (
|
|
7
|
+
<label
|
|
8
|
+
htmlFor={props.htmlFor}
|
|
9
|
+
ref={ref}
|
|
10
|
+
className={cn(
|
|
11
|
+
"text-sm font-medium leading-none text-foreground peer-disabled:cursor-not-allowed peer-disabled:opacity-70",
|
|
12
|
+
className,
|
|
13
|
+
)}
|
|
14
|
+
{...props}
|
|
15
|
+
/>
|
|
16
|
+
),
|
|
10
17
|
);
|
|
11
|
-
|
|
12
|
-
const Label = React.forwardRef<
|
|
13
|
-
HTMLLabelElement,
|
|
14
|
-
React.LabelHTMLAttributes<HTMLLabelElement> & VariantProps<typeof labelVariants>
|
|
15
|
-
>(({ className, ...props }, ref) => (
|
|
16
|
-
<label ref={ref} className={cn(labelVariants(), className)} {...props} />
|
|
17
|
-
));
|
|
18
18
|
Label.displayName = "Label";
|
|
19
19
|
|
|
20
20
|
export { Label };
|
|
@@ -4,15 +4,15 @@
|
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
6
|
export const CHART_COLORS = {
|
|
7
|
-
chart1: "var(--
|
|
8
|
-
chart2: "var(--
|
|
9
|
-
chart3: "var(--
|
|
10
|
-
chart4: "var(--
|
|
11
|
-
chart5: "var(--
|
|
7
|
+
chart1: "var(--chart-1)",
|
|
8
|
+
chart2: "var(--chart-2)",
|
|
9
|
+
chart3: "var(--chart-3)",
|
|
10
|
+
chart4: "var(--chart-4)",
|
|
11
|
+
chart5: "var(--chart-5)",
|
|
12
12
|
|
|
13
|
-
positive: "var(--
|
|
14
|
-
negative: "var(--
|
|
15
|
-
neutral: "var(--
|
|
13
|
+
positive: "var(--chart-positive)",
|
|
14
|
+
negative: "var(--chart-negative)",
|
|
15
|
+
neutral: "var(--chart-neutral)",
|
|
16
16
|
|
|
17
17
|
background: "var(--color-background)",
|
|
18
18
|
grid: "var(--color-border)",
|
|
@@ -28,12 +28,12 @@ export const CHART_COLORS = {
|
|
|
28
28
|
} as const;
|
|
29
29
|
|
|
30
30
|
export const CHART_CSS_VARS = {
|
|
31
|
-
positive: "var(--
|
|
32
|
-
negative: "var(--
|
|
33
|
-
neutral: "var(--
|
|
34
|
-
chart1: "var(--
|
|
35
|
-
chart2: "var(--
|
|
36
|
-
chart3: "var(--
|
|
37
|
-
chart4: "var(--
|
|
38
|
-
chart5: "var(--
|
|
31
|
+
positive: "var(--chart-positive)",
|
|
32
|
+
negative: "var(--chart-negative)",
|
|
33
|
+
neutral: "var(--chart-neutral)",
|
|
34
|
+
chart1: "var(--chart-1)",
|
|
35
|
+
chart2: "var(--chart-2)",
|
|
36
|
+
chart3: "var(--chart-3)",
|
|
37
|
+
chart4: "var(--chart-4)",
|
|
38
|
+
chart5: "var(--chart-5)",
|
|
39
39
|
} as const;
|
|
@@ -29,6 +29,10 @@ function mediaQuery(query: string, options?: MediaQueryOptions) {
|
|
|
29
29
|
return mql.matches;
|
|
30
30
|
}
|
|
31
31
|
|
|
32
|
+
function getServerSnapshot() {
|
|
33
|
+
return false;
|
|
34
|
+
}
|
|
35
|
+
|
|
32
36
|
/**
|
|
33
37
|
* Listens for changes to a media query and returns a boolean indicating whether the query matches.
|
|
34
38
|
* @param query - The media query string to listen for.
|
|
@@ -61,5 +65,5 @@ export function useMediaQuery(query: string) {
|
|
|
61
65
|
return mediaQuery(query);
|
|
62
66
|
}, [query]);
|
|
63
67
|
|
|
64
|
-
return useSyncExternalStore(subscribe, getSnapshot,
|
|
68
|
+
return useSyncExternalStore(subscribe, getSnapshot, getServerSnapshot);
|
|
65
69
|
}
|
|
@@ -51,28 +51,6 @@
|
|
|
51
51
|
}
|
|
52
52
|
}
|
|
53
53
|
|
|
54
|
-
.animate-in {
|
|
55
|
-
animation:
|
|
56
|
-
fade-in 150ms ease-out,
|
|
57
|
-
zoom-in 150ms ease-out;
|
|
58
|
-
}
|
|
59
|
-
.animate-out {
|
|
60
|
-
animation:
|
|
61
|
-
fade-out 120ms ease-in,
|
|
62
|
-
zoom-out 120ms ease-in;
|
|
63
|
-
}
|
|
64
|
-
.fade-in-0 {
|
|
65
|
-
animation-name: fade-in;
|
|
66
|
-
}
|
|
67
|
-
.fade-out-0 {
|
|
68
|
-
animation-name: fade-out;
|
|
69
|
-
}
|
|
70
|
-
.zoom-in-95 {
|
|
71
|
-
animation-name: zoom-in;
|
|
72
|
-
}
|
|
73
|
-
.zoom-out-95 {
|
|
74
|
-
animation-name: zoom-out;
|
|
75
|
-
}
|
|
76
54
|
.data-open\:animate-in[data-open],
|
|
77
55
|
.data-open\:animate-in[data-state="open"] {
|
|
78
56
|
animation:
|
package/src/styles/index.css
CHANGED
|
@@ -19,6 +19,30 @@
|
|
|
19
19
|
@import "./tokens/generated/dark.css" (prefers-color-scheme: dark);
|
|
20
20
|
@import "./animations.css";
|
|
21
21
|
|
|
22
|
+
@theme {
|
|
23
|
+
--animate-accordion-down: accordion-down 200ms ease-out;
|
|
24
|
+
--animate-accordion-up: accordion-up 200ms ease-out;
|
|
25
|
+
--animate-in: fade-in 150ms ease-out, zoom-in 150ms ease-out;
|
|
26
|
+
--animate-out: fade-out 120ms ease-in, zoom-out 120ms ease-in;
|
|
27
|
+
--animate-fade-in: fade-in 150ms ease-out;
|
|
28
|
+
--animate-fade-out: fade-out 120ms ease-in;
|
|
29
|
+
--animate-zoom-in: zoom-in 150ms ease-out;
|
|
30
|
+
--animate-zoom-out: zoom-out 120ms ease-in;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
@utility fade-in-0 {
|
|
34
|
+
animation-name: fade-in;
|
|
35
|
+
}
|
|
36
|
+
@utility fade-out-0 {
|
|
37
|
+
animation-name: fade-out;
|
|
38
|
+
}
|
|
39
|
+
@utility zoom-in-95 {
|
|
40
|
+
animation-name: zoom-in;
|
|
41
|
+
}
|
|
42
|
+
@utility zoom-out-95 {
|
|
43
|
+
animation-name: zoom-out;
|
|
44
|
+
}
|
|
45
|
+
|
|
22
46
|
/* shadcn New York semantic contract. Values are generated from the DTCG
|
|
23
47
|
* source; this file only names the roles consumed by web components. */
|
|
24
48
|
@theme inline {
|
|
@@ -101,22 +125,52 @@
|
|
|
101
125
|
* Default fade size: 16 px (scoped to the element via inline var).
|
|
102
126
|
* Disable with scroll-fade-none (sets --scroll-fade-mask: none). */
|
|
103
127
|
@utility scroll-fade-x {
|
|
104
|
-
mask-image: var(
|
|
128
|
+
mask-image: var(
|
|
129
|
+
--scroll-fade-mask,
|
|
130
|
+
linear-gradient(
|
|
131
|
+
to right,
|
|
132
|
+
transparent,
|
|
133
|
+
black var(--scroll-fade-size, 16px),
|
|
134
|
+
black calc(100% - var(--scroll-fade-size, 16px)),
|
|
135
|
+
transparent
|
|
136
|
+
)
|
|
137
|
+
);
|
|
105
138
|
}
|
|
106
139
|
@utility scroll-fade-y {
|
|
107
|
-
mask-image: var(
|
|
140
|
+
mask-image: var(
|
|
141
|
+
--scroll-fade-mask,
|
|
142
|
+
linear-gradient(
|
|
143
|
+
to bottom,
|
|
144
|
+
transparent,
|
|
145
|
+
black var(--scroll-fade-size, 16px),
|
|
146
|
+
black calc(100% - var(--scroll-fade-size, 16px)),
|
|
147
|
+
transparent
|
|
148
|
+
)
|
|
149
|
+
);
|
|
108
150
|
}
|
|
109
151
|
@utility scroll-fade-l {
|
|
110
|
-
mask-image: var(
|
|
152
|
+
mask-image: var(
|
|
153
|
+
--scroll-fade-mask,
|
|
154
|
+
linear-gradient(to right, transparent, black var(--scroll-fade-size, 16px))
|
|
155
|
+
);
|
|
111
156
|
}
|
|
112
157
|
@utility scroll-fade-r {
|
|
113
|
-
mask-image: var(
|
|
158
|
+
mask-image: var(
|
|
159
|
+
--scroll-fade-mask,
|
|
160
|
+
linear-gradient(to left, transparent, black var(--scroll-fade-size, 16px))
|
|
161
|
+
);
|
|
114
162
|
}
|
|
115
163
|
@utility scroll-fade-t {
|
|
116
|
-
mask-image: var(
|
|
164
|
+
mask-image: var(
|
|
165
|
+
--scroll-fade-mask,
|
|
166
|
+
linear-gradient(to bottom, transparent, black var(--scroll-fade-size, 16px))
|
|
167
|
+
);
|
|
117
168
|
}
|
|
118
169
|
@utility scroll-fade-b {
|
|
119
|
-
mask-image: var(
|
|
170
|
+
mask-image: var(
|
|
171
|
+
--scroll-fade-mask,
|
|
172
|
+
linear-gradient(to top, transparent, black var(--scroll-fade-size, 16px))
|
|
173
|
+
);
|
|
120
174
|
}
|
|
121
175
|
@utility scroll-fade-none {
|
|
122
176
|
--scroll-fade-mask: none;
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { Meta, StoryObj } from "@storybook/react-vite";
|
|
2
2
|
|
|
3
3
|
import { Button } from "../../components/primitives/button";
|
|
4
|
+
import { colors } from "./css-vars";
|
|
4
5
|
import { colorTokenNames } from "./index";
|
|
5
6
|
|
|
6
7
|
const meta: Meta = { title: "Foundations/Colors", parameters: { layout: "fullscreen" } };
|
|
@@ -28,7 +29,7 @@ export const SystemAppearance: Story = {
|
|
|
28
29
|
<div
|
|
29
30
|
key={token}
|
|
30
31
|
className="border-border grid min-h-20 content-end rounded-md border p-3"
|
|
31
|
-
style={{ backgroundColor:
|
|
32
|
+
style={{ backgroundColor: colors[token] }}
|
|
32
33
|
>
|
|
33
34
|
<span className="bg-background text-foreground w-fit rounded px-1 font-mono text-xs">
|
|
34
35
|
{token}
|
|
@@ -24,8 +24,8 @@ export default {
|
|
|
24
24
|
"chart-3": "#12a594",
|
|
25
25
|
"chart-4": "#f76b15",
|
|
26
26
|
"chart-5": "#8b8d98",
|
|
27
|
-
"chart-positive": "#
|
|
28
|
-
"chart-negative": "#
|
|
29
|
-
"chart-neutral": "#
|
|
27
|
+
"chart-positive": "#1f7a50",
|
|
28
|
+
"chart-negative": "#be2e33",
|
|
29
|
+
"chart-neutral": "#626a73",
|
|
30
30
|
"overlay-scrim": "#000000",
|
|
31
31
|
};
|
|
@@ -24,8 +24,8 @@
|
|
|
24
24
|
--chart-3: #12a594;
|
|
25
25
|
--chart-4: #f76b15;
|
|
26
26
|
--chart-5: #8b8d98;
|
|
27
|
-
--chart-positive: #
|
|
28
|
-
--chart-negative: #
|
|
29
|
-
--chart-neutral: #
|
|
27
|
+
--chart-positive: #1f7a50;
|
|
28
|
+
--chart-negative: #be2e33;
|
|
29
|
+
--chart-neutral: #626a73;
|
|
30
30
|
--overlay-scrim: #000000;
|
|
31
31
|
}
|
|
@@ -88,15 +88,15 @@
|
|
|
88
88
|
},
|
|
89
89
|
"chart-positive": {
|
|
90
90
|
"$type": "color",
|
|
91
|
-
"$value": "rgb(
|
|
91
|
+
"$value": "rgb(31, 122, 80)"
|
|
92
92
|
},
|
|
93
93
|
"chart-negative": {
|
|
94
94
|
"$type": "color",
|
|
95
|
-
"$value": "rgb(
|
|
95
|
+
"$value": "rgb(190, 46, 51)"
|
|
96
96
|
},
|
|
97
97
|
"chart-neutral": {
|
|
98
98
|
"$type": "color",
|
|
99
|
-
"$value": "rgb(
|
|
99
|
+
"$value": "rgb(98, 106, 115)"
|
|
100
100
|
},
|
|
101
101
|
"overlay-scrim": {
|
|
102
102
|
"$type": "color",
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
{
|
|
2
|
+
"source": ["src/styles/tokens/source/light.tokens.json"],
|
|
3
|
+
"platforms": {
|
|
4
|
+
"css": {
|
|
5
|
+
"transformGroup": "css",
|
|
6
|
+
"buildPath": ".storybook/generated/",
|
|
7
|
+
"files": [
|
|
8
|
+
{
|
|
9
|
+
"destination": "light-theme-class.css",
|
|
10
|
+
"format": "css/variables",
|
|
11
|
+
"options": { "selector": ":root.storybook-theme-light" }
|
|
12
|
+
}
|
|
13
|
+
]
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
}
|
|
@@ -1,28 +0,0 @@
|
|
|
1
|
-
import type { Meta, StoryObj } from "@storybook/react-vite";
|
|
2
|
-
|
|
3
|
-
import { FileUploadStatusBadge } from "./file-upload-status-badge";
|
|
4
|
-
|
|
5
|
-
const meta = {
|
|
6
|
-
title: "Feedback/FileUploadStatusBadge",
|
|
7
|
-
component: FileUploadStatusBadge,
|
|
8
|
-
tags: ["autodocs"],
|
|
9
|
-
} satisfies Meta<typeof FileUploadStatusBadge>;
|
|
10
|
-
|
|
11
|
-
export default meta;
|
|
12
|
-
type Story = StoryObj<typeof meta>;
|
|
13
|
-
|
|
14
|
-
export const Default: Story = {
|
|
15
|
-
args: { status: "uploading" },
|
|
16
|
-
};
|
|
17
|
-
|
|
18
|
-
export const AllStatuses: Story = {
|
|
19
|
-
render: () => (
|
|
20
|
-
<div className="flex flex-wrap gap-2">
|
|
21
|
-
<FileUploadStatusBadge status="uploading" />
|
|
22
|
-
<FileUploadStatusBadge status="processing" />
|
|
23
|
-
<FileUploadStatusBadge status="queued" />
|
|
24
|
-
<FileUploadStatusBadge status="done" />
|
|
25
|
-
<FileUploadStatusBadge status="error" />
|
|
26
|
-
</div>
|
|
27
|
-
),
|
|
28
|
-
};
|
|
@@ -1,33 +0,0 @@
|
|
|
1
|
-
import { memo } from "react";
|
|
2
|
-
|
|
3
|
-
import { cn } from "../../lib/utils";
|
|
4
|
-
|
|
5
|
-
export type FileUploadStatusValue = "uploading" | "processing" | "queued" | "done" | "error";
|
|
6
|
-
|
|
7
|
-
const STATUS_CONFIG: Record<FileUploadStatusValue, { bg: string; text: string; label: string }> = {
|
|
8
|
-
uploading: { bg: "bg-muted", text: "text-foreground", label: "Uploading" },
|
|
9
|
-
processing: { bg: "bg-muted", text: "text-foreground", label: "Processing" },
|
|
10
|
-
queued: { bg: "bg-secondary", text: "text-secondary-foreground", label: "Queued" },
|
|
11
|
-
done: { bg: "bg-muted", text: "text-muted-foreground", label: "Complete" },
|
|
12
|
-
error: { bg: "bg-destructive/10", text: "text-destructive-text", label: "Error" },
|
|
13
|
-
};
|
|
14
|
-
|
|
15
|
-
export const FileUploadStatusBadge = memo(function FileUploadStatusBadge({
|
|
16
|
-
status,
|
|
17
|
-
}: {
|
|
18
|
-
status?: string | undefined;
|
|
19
|
-
}) {
|
|
20
|
-
if (!status) return null;
|
|
21
|
-
|
|
22
|
-
const config = STATUS_CONFIG[status as FileUploadStatusValue] ?? {
|
|
23
|
-
bg: "bg-muted",
|
|
24
|
-
text: "text-foreground",
|
|
25
|
-
label: status,
|
|
26
|
-
};
|
|
27
|
-
|
|
28
|
-
return (
|
|
29
|
-
<span className={cn("px-2 py-1 text-xs font-medium", config.bg, config.text)}>
|
|
30
|
-
{config.label}
|
|
31
|
-
</span>
|
|
32
|
-
);
|
|
33
|
-
});
|
|
@@ -1,62 +0,0 @@
|
|
|
1
|
-
import type { Meta, StoryObj } from "@storybook/react-vite";
|
|
2
|
-
import { expect, userEvent, within } from "storybook/test";
|
|
3
|
-
|
|
4
|
-
import { Field } from "./field";
|
|
5
|
-
import { Input } from "./input";
|
|
6
|
-
|
|
7
|
-
const meta = {
|
|
8
|
-
title: "Forms/Field",
|
|
9
|
-
component: Field,
|
|
10
|
-
tags: ["autodocs"],
|
|
11
|
-
args: {
|
|
12
|
-
children: <Input placeholder="you@example.com" />,
|
|
13
|
-
},
|
|
14
|
-
} satisfies Meta<typeof Field>;
|
|
15
|
-
|
|
16
|
-
export default meta;
|
|
17
|
-
type Story = StoryObj<typeof meta>;
|
|
18
|
-
|
|
19
|
-
export const Default: Story = {
|
|
20
|
-
render: () => (
|
|
21
|
-
<Field label="Email" helpText="We will never share your address.">
|
|
22
|
-
<Input placeholder="you@example.com" />
|
|
23
|
-
</Field>
|
|
24
|
-
),
|
|
25
|
-
play: async ({ canvasElement }) => {
|
|
26
|
-
const canvas = within(canvasElement);
|
|
27
|
-
const input = canvas.getByLabelText("Email");
|
|
28
|
-
|
|
29
|
-
await userEvent.type(input, "you@example.com");
|
|
30
|
-
|
|
31
|
-
await expect(input).toHaveValue("you@example.com");
|
|
32
|
-
await expect(canvas.getByText("We will never share your address.")).toBeInTheDocument();
|
|
33
|
-
},
|
|
34
|
-
};
|
|
35
|
-
|
|
36
|
-
export const Error: Story = {
|
|
37
|
-
render: () => (
|
|
38
|
-
<Field label="Email" error="Email is required">
|
|
39
|
-
<Input placeholder="you@example.com" />
|
|
40
|
-
</Field>
|
|
41
|
-
),
|
|
42
|
-
play: async ({ canvasElement }) => {
|
|
43
|
-
const canvas = within(canvasElement);
|
|
44
|
-
|
|
45
|
-
await expect(canvas.getByLabelText("Email")).toHaveAttribute("aria-invalid", "true");
|
|
46
|
-
await expect(canvas.getByRole("alert")).toHaveTextContent("Email is required");
|
|
47
|
-
},
|
|
48
|
-
};
|
|
49
|
-
|
|
50
|
-
export const Textarea: Story = {
|
|
51
|
-
render: () => (
|
|
52
|
-
<Field label="Notes" helpText="Markdown supported">
|
|
53
|
-
<textarea rows={5} placeholder="Write something..." />
|
|
54
|
-
</Field>
|
|
55
|
-
),
|
|
56
|
-
play: async ({ canvasElement }) => {
|
|
57
|
-
const canvas = within(canvasElement);
|
|
58
|
-
|
|
59
|
-
await expect(canvas.getByLabelText("Notes")).toBeInTheDocument();
|
|
60
|
-
await expect(canvas.getByText("Markdown supported")).toBeInTheDocument();
|
|
61
|
-
},
|
|
62
|
-
};
|
|
@@ -1,65 +0,0 @@
|
|
|
1
|
-
import * as React from "react";
|
|
2
|
-
|
|
3
|
-
import { cn } from "../../lib/utils";
|
|
4
|
-
import type { FieldBaseProps } from "./field.types";
|
|
5
|
-
|
|
6
|
-
interface FieldProps extends FieldBaseProps {
|
|
7
|
-
children: React.ReactNode;
|
|
8
|
-
}
|
|
9
|
-
|
|
10
|
-
/**
|
|
11
|
-
* Field — standalone label + control + helper/error slot.
|
|
12
|
-
* Wires label ↔ control association automatically via a generated id.
|
|
13
|
-
* Does NOT require react-hook-form — works with any controlled or uncontrolled input.
|
|
14
|
-
*
|
|
15
|
-
* @example
|
|
16
|
-
* <Field label="Email" error={errors.email}>
|
|
17
|
-
* <Input type="email" />
|
|
18
|
-
* </Field>
|
|
19
|
-
*/
|
|
20
|
-
function Field({ label, helpText, error, required, children, id: externalId }: FieldProps) {
|
|
21
|
-
const generatedId = React.useId();
|
|
22
|
-
const id = externalId ?? generatedId;
|
|
23
|
-
const descId = `${id}-desc`;
|
|
24
|
-
const errorId = `${id}-error`;
|
|
25
|
-
|
|
26
|
-
return (
|
|
27
|
-
<div className="flex flex-col gap-1.5">
|
|
28
|
-
{label && (
|
|
29
|
-
<label
|
|
30
|
-
htmlFor={id}
|
|
31
|
-
className={cn(
|
|
32
|
-
"text-foreground text-sm font-medium",
|
|
33
|
-
required && "after:text-destructive-text after:ml-0.5 after:content-['*']",
|
|
34
|
-
)}
|
|
35
|
-
>
|
|
36
|
-
{label}
|
|
37
|
-
</label>
|
|
38
|
-
)}
|
|
39
|
-
|
|
40
|
-
{/* Clone child to inject id and aria attributes */}
|
|
41
|
-
{React.isValidElement(children)
|
|
42
|
-
? React.cloneElement(
|
|
43
|
-
children as React.ReactElement<React.HTMLAttributes<HTMLElement>>,
|
|
44
|
-
{
|
|
45
|
-
id,
|
|
46
|
-
"aria-describedby": error ? errorId : helpText ? descId : undefined,
|
|
47
|
-
"aria-invalid": error ? true : undefined,
|
|
48
|
-
} as React.HTMLAttributes<HTMLElement>,
|
|
49
|
-
)
|
|
50
|
-
: children}
|
|
51
|
-
|
|
52
|
-
{error ? (
|
|
53
|
-
<p id={errorId} role="alert" className="text-destructive-text text-sm">
|
|
54
|
-
{error}
|
|
55
|
-
</p>
|
|
56
|
-
) : helpText ? (
|
|
57
|
-
<p id={descId} className="text-muted-foreground text-sm">
|
|
58
|
-
{helpText}
|
|
59
|
-
</p>
|
|
60
|
-
) : null}
|
|
61
|
-
</div>
|
|
62
|
-
);
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
export { Field, type FieldProps };
|
|
@@ -1,26 +0,0 @@
|
|
|
1
|
-
import type { Meta, StoryObj } from "@storybook/react-vite";
|
|
2
|
-
import { useState } from "react";
|
|
3
|
-
|
|
4
|
-
import { GroupBySelect } from "./group-by-select";
|
|
5
|
-
|
|
6
|
-
const meta = {
|
|
7
|
-
title: "Forms/GroupBySelect",
|
|
8
|
-
component: GroupBySelect,
|
|
9
|
-
tags: ["autodocs"],
|
|
10
|
-
} satisfies Meta<typeof GroupBySelect>;
|
|
11
|
-
|
|
12
|
-
export default meta;
|
|
13
|
-
type Story = StoryObj<typeof meta>;
|
|
14
|
-
|
|
15
|
-
function GroupBySelectPreview() {
|
|
16
|
-
const [groupBy, setGroupBy] = useState<"month" | "week" | "day">("month");
|
|
17
|
-
return <GroupBySelect groupBy={groupBy} onGroupByChange={setGroupBy} />;
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
export const Default: Story = {
|
|
21
|
-
args: {
|
|
22
|
-
groupBy: "month",
|
|
23
|
-
onGroupByChange: () => {},
|
|
24
|
-
},
|
|
25
|
-
render: () => <GroupBySelectPreview />,
|
|
26
|
-
};
|