@ponti-studios/ui 1.1.0 → 1.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/package.json +1 -1
- package/src/components/data-display/accordion.tsx +10 -1
- package/src/components/feedback/empty-state.tsx +1 -1
- package/src/components/feedback/file-upload-status.tsx +14 -6
- package/src/components/feedback/progress-bar.tsx +3 -2
- package/src/components/feedback/spinner.tsx +3 -4
- package/src/components/feedback/update-guard.stories.tsx +1 -1
- package/src/components/feedback/update-guard.tsx +7 -3
- package/src/components/filters/active-filters-bar.stories.tsx +36 -36
- package/src/components/filters/active-filters-bar.tsx +21 -23
- package/src/components/filters/filter-select.stories.tsx +26 -26
- package/src/components/filters/filter-select.tsx +5 -8
- package/src/components/filters/index.ts +0 -4
- package/src/components/forms/date-month-select.tsx +2 -2
- package/src/components/forms/date-picker.tsx +2 -2
- package/src/components/forms/drop-zone.stories.tsx +112 -28
- package/src/components/forms/drop-zone.tsx +265 -112
- package/src/components/forms/input.tsx +5 -1
- package/src/components/forms/radio-group.tsx +3 -2
- package/src/components/forms/select.tsx +14 -4
- package/src/components/forms/slider.stories.tsx +2 -2
- package/src/components/forms/slider.tsx +12 -9
- package/src/components/forms/switch.tsx +5 -2
- package/src/components/forms/textarea.tsx +5 -1
- package/src/components/layout/index.ts +0 -2
- package/src/components/layout/section-intro.tsx +1 -1
- package/src/components/layout/surface-panel.tsx +10 -2
- package/src/components/navigation/app-navigation.stories.tsx +5 -1
- package/src/components/navigation/app-navigation.tsx +19 -4
- package/src/components/navigation/tabs.tsx +4 -1
- package/src/components/overlays/command.tsx +39 -14
- package/src/components/overlays/popover.stories.tsx +16 -4
- package/src/components/primitives/badge.tsx +1 -1
- package/src/components/primitives/button.tsx +6 -1
- package/src/hooks/use-derived-filter-state.ts +1 -3
- package/src/hooks/use-filter-state.ts +1 -2
- package/src/native/card.tsx +3 -1
- package/src/native/index.ts +21 -11
- package/src/native/list-row.tsx +67 -0
- package/src/styles/tokens/generated/native.generated.ts +34 -6
- package/src/styles/tokens/index.ts +2 -0
- package/src/styles/tokens/normalize-native.ts +5 -3
- package/src/styles/tokens/source/native.tokens.json +42 -7
- package/src/styles/tokens/style-dictionary.native.mjs +4 -1
- package/src/components/filters/filters.stories.tsx +0 -72
- package/src/components/filters/filters.tsx +0 -78
- package/src/components/filters/search-filter-bar.stories.tsx +0 -41
- package/src/components/filters/search-filter-bar.tsx +0 -48
package/package.json
CHANGED
|
@@ -6,8 +6,9 @@ import { cn } from "../../lib/utils";
|
|
|
6
6
|
|
|
7
7
|
function Accordion({
|
|
8
8
|
type,
|
|
9
|
-
collapsible
|
|
9
|
+
collapsible = true,
|
|
10
10
|
className,
|
|
11
|
+
onValueChange,
|
|
11
12
|
...props
|
|
12
13
|
}: React.ComponentProps<typeof AccordionPrimitive.Root> & {
|
|
13
14
|
type?: "single" | "multiple";
|
|
@@ -17,6 +18,14 @@ function Accordion({
|
|
|
17
18
|
<AccordionPrimitive.Root
|
|
18
19
|
data-slot="accordion"
|
|
19
20
|
multiple={type === "multiple"}
|
|
21
|
+
onValueChange={(value, details) => {
|
|
22
|
+
if (type !== "multiple" && !collapsible && value.length === 0) {
|
|
23
|
+
details.cancel();
|
|
24
|
+
return;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
onValueChange?.(value, details);
|
|
28
|
+
}}
|
|
20
29
|
{...props}
|
|
21
30
|
className={cn("w-full", className)}
|
|
22
31
|
/>
|
|
@@ -40,7 +40,7 @@ export function EmptyState({
|
|
|
40
40
|
variant === "default" && "bg-card border",
|
|
41
41
|
variant === "dashed" && "bg-card border border-dashed",
|
|
42
42
|
variant === "search" && "bg-muted border border-dashed",
|
|
43
|
-
variant === "quiet" && "bg-
|
|
43
|
+
variant === "quiet" && "bg-card",
|
|
44
44
|
centered
|
|
45
45
|
? cn(
|
|
46
46
|
"flex flex-col items-center justify-center text-center",
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import { XIcon } from "lucide-react";
|
|
2
2
|
import { memo } from "react";
|
|
3
3
|
|
|
4
|
-
import { cn } from "../../lib/utils";
|
|
5
4
|
import { ProgressBar } from "./progress-bar";
|
|
6
5
|
|
|
7
6
|
export interface FileUploadStats {
|
|
@@ -47,11 +46,12 @@ export const FileUploadStatus = memo(function FileUploadStatus({
|
|
|
47
46
|
{typeof progress === "number" ? (
|
|
48
47
|
<ProgressBar
|
|
49
48
|
progress={progress}
|
|
50
|
-
className=
|
|
49
|
+
className="bg-muted h-2"
|
|
50
|
+
indicatorClassName="border-primary"
|
|
51
51
|
/>
|
|
52
52
|
) : (
|
|
53
53
|
<div className="bg-muted h-2 overflow-hidden">
|
|
54
|
-
<div className="bg-
|
|
54
|
+
<div className="bg-primary void-anim-breezy-progress h-full" />
|
|
55
55
|
</div>
|
|
56
56
|
)}
|
|
57
57
|
</div>
|
|
@@ -66,7 +66,11 @@ export const FileUploadStatus = memo(function FileUploadStatus({
|
|
|
66
66
|
<span className="text-muted-foreground text-sm font-medium">Queue position</span>
|
|
67
67
|
</div>
|
|
68
68
|
<div className="w-full">
|
|
69
|
-
<ProgressBar
|
|
69
|
+
<ProgressBar
|
|
70
|
+
progress={0}
|
|
71
|
+
className="bg-warning/10 h-2"
|
|
72
|
+
indicatorClassName="border-warning"
|
|
73
|
+
/>
|
|
70
74
|
</div>
|
|
71
75
|
</output>
|
|
72
76
|
);
|
|
@@ -76,7 +80,11 @@ export const FileUploadStatus = memo(function FileUploadStatus({
|
|
|
76
80
|
return (
|
|
77
81
|
<output className="space-y-3">
|
|
78
82
|
<div className="w-full">
|
|
79
|
-
<ProgressBar
|
|
83
|
+
<ProgressBar
|
|
84
|
+
progress={100}
|
|
85
|
+
className="bg-muted h-2"
|
|
86
|
+
indicatorClassName="border-primary"
|
|
87
|
+
/>
|
|
80
88
|
</div>
|
|
81
89
|
{stats && <ProcessingStats stats={stats} />}
|
|
82
90
|
</output>
|
|
@@ -87,7 +95,7 @@ export const FileUploadStatus = memo(function FileUploadStatus({
|
|
|
87
95
|
return (
|
|
88
96
|
<output className="mt-2" role="alert">
|
|
89
97
|
<div className="bg-destructive/10 text-destructive-text flex items-start gap-2 p-3">
|
|
90
|
-
<XIcon className="mt-0.5 h-5 w-5
|
|
98
|
+
<XIcon className="mt-0.5 h-5 w-5 shrink-0" aria-hidden="true" />
|
|
91
99
|
<span className="text-sm">{error}</span>
|
|
92
100
|
</div>
|
|
93
101
|
</output>
|
|
@@ -2,15 +2,16 @@ import { cn } from "../../lib/utils";
|
|
|
2
2
|
|
|
3
3
|
export interface ProgressBarProps {
|
|
4
4
|
className?: string;
|
|
5
|
+
indicatorClassName?: string;
|
|
5
6
|
progress?: number;
|
|
6
7
|
}
|
|
7
8
|
|
|
8
9
|
/** Thin linear progress indicator, distinct from the Radix-backed `Progress`. */
|
|
9
|
-
export function ProgressBar({ className, progress = 0 }: ProgressBarProps) {
|
|
10
|
+
export function ProgressBar({ className, indicatorClassName, progress = 0 }: ProgressBarProps) {
|
|
10
11
|
return (
|
|
11
12
|
<div className={cn("border-border h-[2px] w-full overflow-hidden border-t", className)}>
|
|
12
13
|
<div
|
|
13
|
-
className={cn("border-warning h-full border-t")}
|
|
14
|
+
className={cn("border-warning h-full border-t", indicatorClassName)}
|
|
14
15
|
style={{
|
|
15
16
|
width: `${Math.min(100, Math.max(0, progress))}%`,
|
|
16
17
|
opacity: progress === 100 ? 0 : 0.8,
|
|
@@ -9,7 +9,7 @@ const sizeClasses = {
|
|
|
9
9
|
xl: "size-12",
|
|
10
10
|
} as const;
|
|
11
11
|
|
|
12
|
-
export interface SpinnerProps extends React.ComponentProps<"
|
|
12
|
+
export interface SpinnerProps extends React.ComponentProps<"output"> {
|
|
13
13
|
label?: string | false;
|
|
14
14
|
presentation?: "inline" | "centered";
|
|
15
15
|
size?: keyof typeof sizeClasses;
|
|
@@ -25,13 +25,12 @@ export function Spinner({
|
|
|
25
25
|
const accessibleLabel = typeof label === "string" ? label : "Loading";
|
|
26
26
|
|
|
27
27
|
return (
|
|
28
|
-
<
|
|
28
|
+
<output
|
|
29
29
|
className={cn(
|
|
30
30
|
"inline-flex items-center justify-center gap-2",
|
|
31
31
|
presentation === "centered" && "flex w-full py-12",
|
|
32
32
|
className,
|
|
33
33
|
)}
|
|
34
|
-
role="status"
|
|
35
34
|
aria-label={accessibleLabel}
|
|
36
35
|
{...props}
|
|
37
36
|
>
|
|
@@ -43,6 +42,6 @@ export function Spinner({
|
|
|
43
42
|
aria-hidden="true"
|
|
44
43
|
/>
|
|
45
44
|
{label && <span className="text-muted-foreground text-xs">{label}</span>}
|
|
46
|
-
</
|
|
45
|
+
</output>
|
|
47
46
|
);
|
|
48
47
|
}
|
|
@@ -23,7 +23,7 @@ export const Default: Story = {
|
|
|
23
23
|
args: {
|
|
24
24
|
hideInDev: false,
|
|
25
25
|
children: (
|
|
26
|
-
<div className="border-border bg-
|
|
26
|
+
<div className="border-border bg-card text-muted-foreground rounded-md border p-6 text-sm">
|
|
27
27
|
App content renders here. Offline/update prompts appear fixed to the bottom of the viewport
|
|
28
28
|
when the service worker reports a change.
|
|
29
29
|
</div>
|
|
@@ -196,7 +196,11 @@ function UpdateGuardClient({
|
|
|
196
196
|
updateServiceWorker,
|
|
197
197
|
} = useRegisterSW({ serviceWorkerPath });
|
|
198
198
|
|
|
199
|
-
const isOnline = useSyncExternalStore(
|
|
199
|
+
const isOnline = useSyncExternalStore(
|
|
200
|
+
subscribeOnline,
|
|
201
|
+
getOnlineSnapshot,
|
|
202
|
+
getOnlineServerSnapshot,
|
|
203
|
+
);
|
|
200
204
|
|
|
201
205
|
const closePrompt = () => {
|
|
202
206
|
setOfflineReady(false);
|
|
@@ -221,7 +225,7 @@ function UpdateGuardClient({
|
|
|
221
225
|
<>
|
|
222
226
|
{offlineMessage ? (
|
|
223
227
|
<div className="fixed inset-x-0 bottom-16 z-50 flex justify-center px-4">
|
|
224
|
-
<div className="border-border bg-
|
|
228
|
+
<div className="border-border bg-card flex items-center gap-3 rounded-md border px-4 py-2">
|
|
225
229
|
<span className="text-foreground text-sm">{offlineMessage}</span>
|
|
226
230
|
</div>
|
|
227
231
|
</div>
|
|
@@ -229,7 +233,7 @@ function UpdateGuardClient({
|
|
|
229
233
|
|
|
230
234
|
{offlineReady || needRefresh ? (
|
|
231
235
|
<div className="fixed inset-x-0 bottom-4 z-50 flex justify-center px-4">
|
|
232
|
-
<div className="border-border bg-
|
|
236
|
+
<div className="border-border bg-card flex items-center gap-3 rounded-md border px-4 py-2">
|
|
233
237
|
<span className="text-foreground text-sm">
|
|
234
238
|
{offlineReady
|
|
235
239
|
? (copy?.offlineReady ?? "App ready to work offline")
|
|
@@ -1,13 +1,13 @@
|
|
|
1
|
-
import type { Meta, StoryObj } from
|
|
2
|
-
import { expect, userEvent, within } from
|
|
3
|
-
import { fn } from
|
|
1
|
+
import type { Meta, StoryObj } from "@storybook/react-vite";
|
|
2
|
+
import { expect, userEvent, within } from "storybook/test";
|
|
3
|
+
import { fn } from "storybook/test";
|
|
4
4
|
|
|
5
|
-
import { ActiveFiltersBar } from
|
|
5
|
+
import { ActiveFiltersBar } from "./active-filters-bar";
|
|
6
6
|
|
|
7
7
|
const meta = {
|
|
8
|
-
title:
|
|
8
|
+
title: "Filters/ActiveFiltersBar",
|
|
9
9
|
component: ActiveFiltersBar,
|
|
10
|
-
tags: [
|
|
10
|
+
tags: ["autodocs"],
|
|
11
11
|
decorators: [
|
|
12
12
|
(Story) => (
|
|
13
13
|
<div className="w-[600px]">
|
|
@@ -23,36 +23,37 @@ type Story = StoryObj<typeof meta>;
|
|
|
23
23
|
export const Default: Story = {
|
|
24
24
|
args: {
|
|
25
25
|
filters: [
|
|
26
|
-
{ id:
|
|
27
|
-
{ id:
|
|
26
|
+
{ id: "1", label: "Status: Active", onRemove: fn() },
|
|
27
|
+
{ id: "2", label: "Created after Jan 1", onRemove: fn() },
|
|
28
28
|
],
|
|
29
|
+
onClearAll: fn(),
|
|
29
30
|
},
|
|
30
31
|
play: async ({ canvasElement }) => {
|
|
31
32
|
const canvas = within(canvasElement);
|
|
32
|
-
const list = canvas.getByRole(
|
|
33
|
+
const list = canvas.getByRole("list");
|
|
33
34
|
await expect(list).toBeInTheDocument();
|
|
34
|
-
const items = within(list).getAllByRole(
|
|
35
|
+
const items = within(list).getAllByRole("listitem");
|
|
35
36
|
expect(items.length).toBe(2);
|
|
36
|
-
await expect(canvas.getByText(
|
|
37
|
-
await expect(canvas.getByText(
|
|
38
|
-
await expect(canvas.getByRole(
|
|
37
|
+
await expect(canvas.getByText("Status: Active")).toBeInTheDocument();
|
|
38
|
+
await expect(canvas.getByText("Created after Jan 1")).toBeInTheDocument();
|
|
39
|
+
await expect(canvas.getByRole("button", { name: "Clear all" })).toBeInTheDocument();
|
|
39
40
|
},
|
|
40
41
|
};
|
|
41
42
|
|
|
42
43
|
export const WithClearAll: Story = {
|
|
43
44
|
args: {
|
|
44
45
|
filters: [
|
|
45
|
-
{ id:
|
|
46
|
-
{ id:
|
|
46
|
+
{ id: "1", label: "Status: Active", onRemove: fn() },
|
|
47
|
+
{ id: "2", label: "Role: Engineer", onRemove: fn() },
|
|
47
48
|
],
|
|
48
49
|
onClearAll: fn(),
|
|
49
50
|
},
|
|
50
51
|
play: async ({ canvasElement, args }) => {
|
|
51
52
|
const canvas = within(canvasElement);
|
|
52
|
-
const list = canvas.getByRole(
|
|
53
|
-
const items = within(list).getAllByRole(
|
|
53
|
+
const list = canvas.getByRole("list");
|
|
54
|
+
const items = within(list).getAllByRole("listitem");
|
|
54
55
|
expect(items.length).toBe(2);
|
|
55
|
-
const clearButton = canvas.getByRole(
|
|
56
|
+
const clearButton = canvas.getByRole("button", { name: "Clear all" });
|
|
56
57
|
await expect(clearButton).toBeInTheDocument();
|
|
57
58
|
await userEvent.click(clearButton);
|
|
58
59
|
await expect(args.onClearAll).toHaveBeenCalledTimes(1);
|
|
@@ -61,36 +62,35 @@ export const WithClearAll: Story = {
|
|
|
61
62
|
|
|
62
63
|
export const WithSortOnly: Story = {
|
|
63
64
|
args: {
|
|
64
|
-
filters: [
|
|
65
|
-
|
|
66
|
-
],
|
|
65
|
+
filters: [{ id: "1", label: "Date", onRemove: fn(), variant: "sort", direction: "desc" }],
|
|
66
|
+
onClearAll: fn(),
|
|
67
67
|
},
|
|
68
68
|
play: async ({ canvasElement }) => {
|
|
69
69
|
const canvas = within(canvasElement);
|
|
70
|
-
const list = canvas.getByRole(
|
|
71
|
-
const items = within(list).getAllByRole(
|
|
70
|
+
const list = canvas.getByRole("list");
|
|
71
|
+
const items = within(list).getAllByRole("listitem");
|
|
72
72
|
expect(items.length).toBe(1);
|
|
73
|
-
await expect(canvas.getByText(
|
|
74
|
-
await expect(canvas.getByRole(
|
|
73
|
+
await expect(canvas.getByText("Date")).toBeInTheDocument();
|
|
74
|
+
await expect(canvas.getByRole("button", { name: "Clear all" })).toBeInTheDocument();
|
|
75
75
|
},
|
|
76
76
|
};
|
|
77
77
|
|
|
78
78
|
export const WithSortAndFilter: Story = {
|
|
79
79
|
args: {
|
|
80
80
|
filters: [
|
|
81
|
-
{ id:
|
|
82
|
-
{ id:
|
|
81
|
+
{ id: "filter-1", label: "Status: Active", onRemove: fn(), variant: "filter" },
|
|
82
|
+
{ id: "sort-1", label: "Date", onRemove: fn(), variant: "sort", direction: "desc" },
|
|
83
83
|
],
|
|
84
84
|
onClearAll: fn(),
|
|
85
85
|
},
|
|
86
86
|
play: async ({ canvasElement }) => {
|
|
87
87
|
const canvas = within(canvasElement);
|
|
88
|
-
const list = canvas.getByRole(
|
|
89
|
-
const items = within(list).getAllByRole(
|
|
88
|
+
const list = canvas.getByRole("list");
|
|
89
|
+
const items = within(list).getAllByRole("listitem");
|
|
90
90
|
expect(items.length).toBe(2);
|
|
91
|
-
await expect(canvas.getByText(
|
|
92
|
-
await expect(canvas.getByText(
|
|
93
|
-
await expect(canvas.getByRole(
|
|
91
|
+
await expect(canvas.getByText("Status: Active")).toBeInTheDocument();
|
|
92
|
+
await expect(canvas.getByText("Date")).toBeInTheDocument();
|
|
93
|
+
await expect(canvas.getByRole("button", { name: "Clear all" })).toBeInTheDocument();
|
|
94
94
|
},
|
|
95
95
|
};
|
|
96
96
|
|
|
@@ -98,17 +98,17 @@ export const Empty: Story = {
|
|
|
98
98
|
args: { filters: [] },
|
|
99
99
|
play: async ({ canvasElement }) => {
|
|
100
100
|
const canvas = within(canvasElement);
|
|
101
|
-
expect(canvas.queryByRole(
|
|
101
|
+
expect(canvas.queryByRole("list")).toBeNull();
|
|
102
102
|
},
|
|
103
103
|
};
|
|
104
104
|
|
|
105
105
|
export const WithClassName: Story = {
|
|
106
106
|
args: {
|
|
107
|
-
filters: [{ id:
|
|
108
|
-
className:
|
|
107
|
+
filters: [{ id: "1", label: "Status: Active", onRemove: fn() }],
|
|
108
|
+
className: "mt-4",
|
|
109
109
|
},
|
|
110
110
|
play: async ({ canvasElement }) => {
|
|
111
|
-
const container = canvasElement.querySelector(
|
|
111
|
+
const container = canvasElement.querySelector(".mt-4");
|
|
112
112
|
await expect(container).not.toBeNull();
|
|
113
113
|
},
|
|
114
114
|
};
|
|
@@ -33,31 +33,29 @@ export function ActiveFiltersBar({ filters, className, onClearAll }: ActiveFilte
|
|
|
33
33
|
<span className="text-muted-foreground shrink-0 self-stretch flex items-center text-sm border-r px-4">
|
|
34
34
|
Filters
|
|
35
35
|
</span>
|
|
36
|
-
<
|
|
37
|
-
role="list"
|
|
38
|
-
className="flex flex-1 min-w-0 items-center gap-2 overflow-x-auto scroll-fade-x pl-4 py-1.5"
|
|
39
|
-
>
|
|
36
|
+
<ul className="flex flex-1 min-w-0 items-center gap-2 overflow-x-auto scroll-fade-x pl-4 py-1.5">
|
|
40
37
|
{filters.map((filter) => (
|
|
41
|
-
<
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
/>
|
|
38
|
+
<li key={filter.id} className="shrink-0">
|
|
39
|
+
<FilterChip
|
|
40
|
+
label={filter.label}
|
|
41
|
+
onRemove={filter.onRemove}
|
|
42
|
+
onClick={filter.onClick}
|
|
43
|
+
variant={filter.variant === "sort" ? "sort" : "default"}
|
|
44
|
+
direction={filter.direction}
|
|
45
|
+
/>
|
|
46
|
+
</li>
|
|
51
47
|
))}
|
|
52
|
-
</
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
48
|
+
</ul>
|
|
49
|
+
{onClearAll ? (
|
|
50
|
+
<button
|
|
51
|
+
type="button"
|
|
52
|
+
onClick={onClearAll}
|
|
53
|
+
className="hover:bg-muted text-muted-foreground hover:text-foreground focus-visible:ring-ring inline-flex shrink-0 items-center justify-center self-stretch aspect-square rounded-r-lg border-l border-border transition-colors focus-visible:ring-2 focus-visible:outline-none"
|
|
54
|
+
aria-label="Clear all"
|
|
55
|
+
>
|
|
56
|
+
<X className="size-4" aria-hidden />
|
|
57
|
+
</button>
|
|
58
|
+
) : null}
|
|
61
59
|
</div>
|
|
62
60
|
);
|
|
63
61
|
}
|
|
@@ -1,22 +1,22 @@
|
|
|
1
|
-
import type { Meta, StoryObj } from
|
|
2
|
-
import { expect, within } from
|
|
1
|
+
import type { Meta, StoryObj } from "@storybook/react-vite";
|
|
2
|
+
import { expect, within } from "storybook/test";
|
|
3
3
|
|
|
4
|
-
import { FilterSelect } from
|
|
4
|
+
import { FilterSelect } from "./filter-select";
|
|
5
5
|
|
|
6
6
|
const meta = {
|
|
7
|
-
title:
|
|
7
|
+
title: "Filters/FilterSelect",
|
|
8
8
|
component: FilterSelect,
|
|
9
|
-
tags: [
|
|
9
|
+
tags: ["autodocs"],
|
|
10
10
|
args: {
|
|
11
11
|
options: [
|
|
12
|
-
{ value:
|
|
13
|
-
{ value:
|
|
14
|
-
{ value:
|
|
15
|
-
{ value:
|
|
16
|
-
{ value:
|
|
12
|
+
{ value: "active", label: "Active" },
|
|
13
|
+
{ value: "interview", label: "Interview" },
|
|
14
|
+
{ value: "offer", label: "Offer" },
|
|
15
|
+
{ value: "rejected", label: "Rejected" },
|
|
16
|
+
{ value: "archived", label: "Archived" },
|
|
17
17
|
],
|
|
18
|
-
placeholder:
|
|
19
|
-
label:
|
|
18
|
+
placeholder: "All statuses",
|
|
19
|
+
label: "Status",
|
|
20
20
|
onChange: () => {},
|
|
21
21
|
},
|
|
22
22
|
} satisfies Meta<typeof FilterSelect>;
|
|
@@ -25,47 +25,47 @@ export default meta;
|
|
|
25
25
|
type Story = StoryObj<typeof meta>;
|
|
26
26
|
|
|
27
27
|
export const Default: Story = {
|
|
28
|
-
args: { value:
|
|
28
|
+
args: { value: "" },
|
|
29
29
|
play: async ({ canvasElement }) => {
|
|
30
30
|
const canvas = within(canvasElement);
|
|
31
|
-
const trigger = canvas.getByRole(
|
|
31
|
+
const trigger = canvas.getByRole("combobox", { name: "Status" });
|
|
32
32
|
await expect(trigger).toBeInTheDocument();
|
|
33
|
-
await expect(trigger).toHaveTextContent(
|
|
33
|
+
await expect(trigger).toHaveTextContent("All statuses");
|
|
34
34
|
},
|
|
35
35
|
};
|
|
36
36
|
|
|
37
37
|
export const WithValue: Story = {
|
|
38
|
-
args: { value:
|
|
38
|
+
args: { value: "active" },
|
|
39
39
|
play: async ({ canvasElement }) => {
|
|
40
40
|
const canvas = within(canvasElement);
|
|
41
|
-
const trigger = canvas.getByRole(
|
|
42
|
-
await expect(trigger).toHaveTextContent(
|
|
41
|
+
const trigger = canvas.getByRole("combobox", { name: "Status" });
|
|
42
|
+
await expect(trigger).toHaveTextContent("Active");
|
|
43
43
|
},
|
|
44
44
|
};
|
|
45
45
|
|
|
46
46
|
export const Loading: Story = {
|
|
47
|
-
args: { value:
|
|
47
|
+
args: { value: "", isLoading: true, options: [] },
|
|
48
48
|
play: async ({ canvasElement }) => {
|
|
49
49
|
const canvas = within(canvasElement);
|
|
50
|
-
await expect(canvas.getByRole(
|
|
51
|
-
await expect(canvas.getByText(
|
|
50
|
+
await expect(canvas.getByRole("combobox", { name: "Status" })).toBeInTheDocument();
|
|
51
|
+
await expect(canvas.getByText("Status")).toBeInTheDocument();
|
|
52
52
|
},
|
|
53
53
|
};
|
|
54
54
|
|
|
55
55
|
export const Empty: Story = {
|
|
56
|
-
args: { value:
|
|
56
|
+
args: { value: "", options: [], emptyLabel: "No statuses found" },
|
|
57
57
|
play: async ({ canvasElement }) => {
|
|
58
58
|
const canvas = within(canvasElement);
|
|
59
|
-
await expect(canvas.getByRole(
|
|
60
|
-
await expect(canvas.getByText(
|
|
59
|
+
await expect(canvas.getByRole("combobox", { name: "Status" })).toBeInTheDocument();
|
|
60
|
+
await expect(canvas.getByText("Status")).toBeInTheDocument();
|
|
61
61
|
},
|
|
62
62
|
};
|
|
63
63
|
|
|
64
64
|
export const Disabled: Story = {
|
|
65
|
-
args: { value:
|
|
65
|
+
args: { value: "active", disabled: true },
|
|
66
66
|
play: async ({ canvasElement }) => {
|
|
67
67
|
const canvas = within(canvasElement);
|
|
68
|
-
const trigger = canvas.getByRole(
|
|
68
|
+
const trigger = canvas.getByRole("combobox", { name: "Status" });
|
|
69
69
|
await expect(trigger).toBeDisabled();
|
|
70
70
|
},
|
|
71
71
|
};
|
|
@@ -14,6 +14,7 @@ export interface FilterSelectProps {
|
|
|
14
14
|
onChange: (value: string) => void;
|
|
15
15
|
options: FilterSelectOption[];
|
|
16
16
|
label: string;
|
|
17
|
+
loadingText?: string;
|
|
17
18
|
placeholder?: string;
|
|
18
19
|
disabled?: boolean;
|
|
19
20
|
isLoading?: boolean;
|
|
@@ -29,6 +30,7 @@ export function FilterSelect({
|
|
|
29
30
|
value,
|
|
30
31
|
options,
|
|
31
32
|
onChange,
|
|
33
|
+
loadingText = "Loading...",
|
|
32
34
|
placeholder = "All",
|
|
33
35
|
disabled = false,
|
|
34
36
|
isLoading = false,
|
|
@@ -44,21 +46,16 @@ export function FilterSelect({
|
|
|
44
46
|
);
|
|
45
47
|
|
|
46
48
|
return (
|
|
47
|
-
<div className={cn("space-y-2 min-w-36
|
|
49
|
+
<div className={cn("space-y-2 min-w-36", className)}>
|
|
48
50
|
<Label htmlFor={selectId}>{label}</Label>
|
|
49
51
|
<Select value={value} onValueChange={onChange} items={items}>
|
|
50
|
-
<SelectTrigger
|
|
51
|
-
id={selectId}
|
|
52
|
-
disabled={disabled}
|
|
53
|
-
aria-label={label}
|
|
54
|
-
className="w-full"
|
|
55
|
-
>
|
|
52
|
+
<SelectTrigger id={selectId} disabled={disabled} aria-label={label} className="w-full">
|
|
56
53
|
<SelectValue placeholder={placeholder} />
|
|
57
54
|
</SelectTrigger>
|
|
58
55
|
<SelectContent className="bg-background max-h-[250px] min-w-36 overflow-y-auto">
|
|
59
56
|
<SelectItem value={ALL_VALUE}>{placeholder}</SelectItem>
|
|
60
57
|
{isLoading ? (
|
|
61
|
-
<div className="text-muted-foreground px-2 py-1.5 text-sm">
|
|
58
|
+
<div className="text-muted-foreground px-2 py-1.5 text-sm">{loadingText}</div>
|
|
62
59
|
) : options.length === 0 ? (
|
|
63
60
|
<div className="text-muted-foreground px-2 py-1.5 text-sm">{emptyLabel}</div>
|
|
64
61
|
) : (
|
|
@@ -4,7 +4,3 @@ export { FilterChip } from "./filter-chip";
|
|
|
4
4
|
export type { FilterChipProps } from "./filter-chip";
|
|
5
5
|
export { FilterSelect } from "./filter-select";
|
|
6
6
|
export type { FilterSelectOption, FilterSelectProps } from "./filter-select";
|
|
7
|
-
export { Filters } from "./filters";
|
|
8
|
-
export type { FiltersProps } from "./filters";
|
|
9
|
-
export { SearchFilterBar } from "./search-filter-bar";
|
|
10
|
-
export type { SearchFilterBarProps } from "./search-filter-bar";
|
|
@@ -42,7 +42,7 @@ export function DateMonthSelect({
|
|
|
42
42
|
|
|
43
43
|
return (
|
|
44
44
|
<div className="flex items-center">
|
|
45
|
-
<div className="flex items-center justify-center
|
|
45
|
+
<div className="border-border bg-background text-foreground flex size-10 items-center justify-center rounded-l-xl border">
|
|
46
46
|
<Calendar className="size-4" />
|
|
47
47
|
</div>
|
|
48
48
|
<Select
|
|
@@ -51,7 +51,7 @@ export function DateMonthSelect({
|
|
|
51
51
|
>
|
|
52
52
|
<SelectTrigger
|
|
53
53
|
className={cn(
|
|
54
|
-
"
|
|
54
|
+
"border-border bg-background text-foreground min-h-10 max-h-10 rounded-l-none rounded-r-xl border-l-0",
|
|
55
55
|
className,
|
|
56
56
|
)}
|
|
57
57
|
aria-label={placeholder}
|
|
@@ -41,7 +41,7 @@ export function DatePicker({
|
|
|
41
41
|
) : null}
|
|
42
42
|
<Popover>
|
|
43
43
|
<div className="flex w-full items-center">
|
|
44
|
-
<div className="border-border bg-
|
|
44
|
+
<div className="border-border bg-background text-foreground flex size-10 shrink-0 items-center justify-center rounded-l-xl border">
|
|
45
45
|
<CalendarIcon className="size-4" aria-hidden />
|
|
46
46
|
</div>
|
|
47
47
|
<PopoverTrigger asChild>
|
|
@@ -49,7 +49,7 @@ export function DatePicker({
|
|
|
49
49
|
id={id}
|
|
50
50
|
variant={variant}
|
|
51
51
|
disabled={disabled}
|
|
52
|
-
className="border-border bg-
|
|
52
|
+
className="border-border bg-background text-foreground 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
53
|
>
|
|
54
54
|
{value ? format(value, dateFormat) : <span>{placeholder}</span>}
|
|
55
55
|
</Button>
|