@ponti-studios/ui 0.1.2 → 0.4.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/LICENSE +21 -0
- package/README.md +7 -7
- package/THIRD_PARTY_NOTICES.md +17 -0
- package/dist/index.js +988 -640
- package/dist/index.js.map +1 -1
- package/package.json +41 -41
- package/src/animations.css +42 -383
- package/src/components/data-display/accordion.tsx +22 -10
- package/src/components/data-display/filter-chip.stories.tsx +38 -0
- package/src/components/data-display/filter-chip.tsx +10 -4
- package/src/components/data-display/index.ts +5 -0
- package/src/components/data-display/metric-card.stories.tsx +48 -0
- package/src/components/data-display/metric-card.tsx +20 -0
- package/src/components/data-display/sort-controls.stories.tsx +45 -0
- package/src/components/data-display/sort-controls.tsx +99 -0
- package/src/components/data-display/sort-row.tsx +74 -0
- package/src/components/data-display/table.tsx +5 -9
- package/src/components/feedback/alert.tsx +4 -4
- package/src/components/feedback/empty-state.tsx +16 -6
- package/src/components/feedback/file-upload-status-badge.stories.tsx +28 -0
- package/src/components/feedback/file-upload-status-badge.tsx +33 -0
- package/src/components/feedback/file-upload-status.stories.tsx +57 -0
- package/src/components/feedback/file-upload-status.tsx +152 -0
- package/src/components/feedback/index.ts +9 -0
- package/src/components/feedback/progress-bar.stories.tsx +39 -0
- package/src/components/feedback/progress-bar.tsx +21 -0
- package/src/components/feedback/progress.stories.tsx +8 -6
- package/src/components/feedback/progress.tsx +3 -3
- package/src/components/feedback/skeleton.stories.tsx +36 -0
- package/src/components/feedback/skeleton.tsx +7 -0
- package/src/components/feedback/spinner.tsx +2 -2
- package/src/components/feedback/update-guard.stories.tsx +32 -0
- package/src/components/feedback/update-guard.tsx +280 -0
- package/src/components/forms/calendar.tsx +14 -11
- package/src/components/forms/date-month-select.stories.tsx +26 -0
- package/src/components/forms/date-month-select.tsx +70 -0
- package/src/components/forms/date-picker.tsx +3 -3
- package/src/components/forms/drop-zone.stories.tsx +56 -0
- package/src/components/forms/drop-zone.tsx +146 -0
- package/src/components/forms/entity-select.stories.tsx +65 -0
- package/src/components/forms/entity-select.tsx +79 -0
- package/src/components/forms/field.tsx +3 -3
- package/src/components/forms/group-by-select.stories.tsx +26 -0
- package/src/components/forms/group-by-select.tsx +47 -0
- package/src/components/forms/index.ts +10 -0
- package/src/components/forms/input.stories.tsx +4 -4
- package/src/components/forms/input.tsx +1 -1
- package/src/components/forms/passkey-management.stories.tsx +81 -0
- package/src/components/forms/passkey-management.tsx +138 -0
- package/src/components/forms/radio-group.tsx +14 -13
- package/src/components/forms/select.stories.tsx +4 -4
- package/src/components/forms/select.tsx +47 -48
- package/src/components/forms/slider.stories.tsx +5 -2
- package/src/components/forms/slider.tsx +39 -17
- package/src/components/forms/stepper.tsx +4 -4
- package/src/components/forms/switch.stories.tsx +1 -1
- package/src/components/forms/switch.tsx +2 -2
- package/src/components/forms/textarea.stories.tsx +2 -2
- package/src/components/forms/textarea.tsx +1 -1
- package/src/components/layout/index.ts +3 -0
- package/src/components/layout/scroll-area.stories.tsx +4 -4
- package/src/components/layout/scroll-area.tsx +23 -21
- package/src/components/layout/section-intro.stories.tsx +29 -0
- package/src/components/layout/section-intro.tsx +31 -0
- package/src/components/layout/surface-panel.stories.tsx +32 -0
- package/src/components/layout/surface-panel.tsx +15 -0
- package/src/components/navigation/app-navigation.stories.tsx +2 -2
- package/src/components/navigation/app-navigation.tsx +13 -13
- package/src/components/navigation/index.ts +4 -0
- package/src/components/navigation/pagination-controls.stories.tsx +41 -0
- package/src/components/navigation/pagination-controls.tsx +50 -0
- package/src/components/navigation/route-link.stories.tsx +30 -0
- package/src/components/navigation/route-link.tsx +42 -0
- package/src/components/navigation/tabs.stories.tsx +5 -5
- package/src/components/navigation/tabs.tsx +8 -8
- package/src/components/overlays/alert-dialog.tsx +22 -25
- package/src/components/overlays/command.tsx +98 -26
- package/src/components/overlays/dialog.tsx +5 -5
- package/src/components/overlays/dropdown-menu.stories.tsx +0 -5
- package/src/components/overlays/dropdown-menu.tsx +49 -31
- package/src/components/overlays/popover.stories.tsx +2 -2
- package/src/components/overlays/popover.tsx +43 -14
- package/src/components/overlays/sheet.stories.tsx +2 -2
- package/src/components/overlays/sheet.tsx +51 -17
- package/src/components/primitives/avatar.tsx +9 -5
- package/src/components/primitives/badge.tsx +11 -12
- package/src/components/primitives/button.tsx +49 -23
- package/src/components/primitives/card.stories.tsx +2 -2
- package/src/components/primitives/card.tsx +2 -5
- package/src/components/primitives/index.ts +2 -0
- package/src/components/primitives/label.stories.tsx +1 -1
- package/src/components/primitives/label.tsx +5 -6
- package/src/components/primitives/status-badge.stories.tsx +52 -0
- package/src/components/primitives/status-badge.tsx +33 -0
- package/src/constants/chart-colors.ts +7 -7
- package/src/hooks/index.ts +1 -0
- package/src/hooks/use-sort.ts +20 -0
- package/src/index.ts +30 -11
- package/src/styles.css +100 -974
- package/src/tokens/chart-colors.stories.tsx +192 -0
- package/src/tokens/color-systems.stories.tsx +20 -112
- package/src/tokens/foundations.stories.tsx +58 -100
- package/src/tokens/generated/dark.colors.generated.ts +29 -0
- package/src/tokens/generated/dark.css +29 -0
- package/src/tokens/generated/foundations.css +148 -0
- package/src/tokens/generated/foundations.generated.ts +451 -0
- package/src/tokens/generated/light.colors.generated.ts +29 -0
- package/src/tokens/generated/light.css +29 -0
- package/src/tokens/index.ts +79 -16
- package/src/tokens/layout.stories.tsx +23 -91
- package/src/tokens/colors.ts +0 -156
- package/src/tokens/motion.ts +0 -51
- package/src/tokens/radii.ts +0 -10
- package/src/tokens/shadows.ts +0 -34
- package/src/tokens/spacing.ts +0 -30
- package/src/tokens/typography.ts +0 -42
|
@@ -13,24 +13,30 @@ export interface FilterChipProps {
|
|
|
13
13
|
/** An applied filter with an optional edit action and an explicit remove action. */
|
|
14
14
|
export function FilterChip({ label, onRemove, onClick, className }: FilterChipProps) {
|
|
15
15
|
const labelContent = onClick ? (
|
|
16
|
-
<button
|
|
16
|
+
<button
|
|
17
|
+
type="button"
|
|
18
|
+
className="focus-visible:ring-ring min-h-9 rounded-full px-3 text-left transition-colors focus-visible:ring-2 focus-visible:ring-offset-2 focus-visible:outline-none"
|
|
19
|
+
onClick={onClick}
|
|
20
|
+
>
|
|
17
21
|
{label}
|
|
18
22
|
</button>
|
|
19
23
|
) : (
|
|
20
|
-
<span className="flex min-h-
|
|
24
|
+
<span className="flex min-h-9 items-center px-3">{label}</span>
|
|
21
25
|
);
|
|
22
26
|
|
|
23
27
|
return (
|
|
24
28
|
<span
|
|
29
|
+
role="group"
|
|
30
|
+
aria-label={typeof label === "string" ? `${label} filter` : "filter"}
|
|
25
31
|
className={cn(
|
|
26
|
-
"border-
|
|
32
|
+
"border-border bg-secondary text-secondary-foreground inline-flex items-center rounded-full border text-sm font-medium",
|
|
27
33
|
className,
|
|
28
34
|
)}
|
|
29
35
|
>
|
|
30
36
|
{labelContent}
|
|
31
37
|
<button
|
|
32
38
|
type="button"
|
|
33
|
-
className="text-
|
|
39
|
+
className="text-muted-foreground hover:text-foreground focus-visible:ring-ring mr-1 flex size-9 items-center justify-center rounded-full transition-colors focus-visible:ring-2 focus-visible:ring-offset-2 focus-visible:outline-none"
|
|
34
40
|
onClick={onRemove}
|
|
35
41
|
aria-label={`Remove filter: ${typeof label === "string" ? label : "selected filter"}`}
|
|
36
42
|
>
|
|
@@ -1,6 +1,11 @@
|
|
|
1
1
|
export { Accordion, AccordionContent, AccordionItem, AccordionTrigger } from "./accordion";
|
|
2
2
|
export { FilterChip } from "./filter-chip";
|
|
3
3
|
export type { FilterChipProps } from "./filter-chip";
|
|
4
|
+
export { MetricCard } from "./metric-card";
|
|
5
|
+
export { SortControls } from "./sort-controls";
|
|
6
|
+
export type { SortControlsProps } from "./sort-controls";
|
|
7
|
+
export { SortRow } from "./sort-row";
|
|
8
|
+
export type { SortRowProps } from "./sort-row";
|
|
4
9
|
export {
|
|
5
10
|
Table,
|
|
6
11
|
TableBody,
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import type { Meta, StoryObj } from "@storybook/react-vite";
|
|
2
|
+
|
|
3
|
+
import { MetricCard } from "./metric-card";
|
|
4
|
+
|
|
5
|
+
const meta = {
|
|
6
|
+
title: "DataDisplay/MetricCard",
|
|
7
|
+
component: MetricCard,
|
|
8
|
+
tags: ["autodocs"],
|
|
9
|
+
} satisfies Meta<typeof MetricCard>;
|
|
10
|
+
|
|
11
|
+
export default meta;
|
|
12
|
+
type Story = StoryObj<typeof meta>;
|
|
13
|
+
|
|
14
|
+
export const Default: Story = {
|
|
15
|
+
args: {
|
|
16
|
+
label: "Net worth",
|
|
17
|
+
value: "$48,210",
|
|
18
|
+
},
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
export const WithChange: Story = {
|
|
22
|
+
args: {
|
|
23
|
+
label: "Monthly spend",
|
|
24
|
+
value: "$3,120",
|
|
25
|
+
change: "+4.2% vs last month",
|
|
26
|
+
},
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
export const Empty: Story = {
|
|
30
|
+
args: {
|
|
31
|
+
label: "Runway",
|
|
32
|
+
value: undefined,
|
|
33
|
+
},
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
export const Grid: Story = {
|
|
37
|
+
args: {
|
|
38
|
+
label: "Net worth",
|
|
39
|
+
value: "$48,210",
|
|
40
|
+
},
|
|
41
|
+
render: () => (
|
|
42
|
+
<div className="grid w-[520px] grid-cols-3 gap-3">
|
|
43
|
+
<MetricCard label="Net worth" value="$48,210" change="+2.1%" />
|
|
44
|
+
<MetricCard label="Income" value="$6,400" />
|
|
45
|
+
<MetricCard label="Expenses" value="$3,120" change="-1.4%" />
|
|
46
|
+
</div>
|
|
47
|
+
),
|
|
48
|
+
};
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import type { ComponentProps, ReactNode } from "react";
|
|
2
|
+
|
|
3
|
+
import { cn } from "../../lib/utils";
|
|
4
|
+
|
|
5
|
+
interface MetricCardProps extends ComponentProps<"div"> {
|
|
6
|
+
label: string;
|
|
7
|
+
value: ReactNode;
|
|
8
|
+
change?: ReactNode;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
/** Label/value/change stat tile. */
|
|
12
|
+
export function MetricCard({ label, value, change, className, ...props }: MetricCardProps) {
|
|
13
|
+
return (
|
|
14
|
+
<div className={cn("ui-flat-card", className)} {...props}>
|
|
15
|
+
<p className="ui-data-label mb-2">{label}</p>
|
|
16
|
+
<p className="ui-data-value">{value ?? "—"}</p>
|
|
17
|
+
{change != null ? <p className="text-muted-foreground mt-1.5 text-xs">{change}</p> : null}
|
|
18
|
+
</div>
|
|
19
|
+
);
|
|
20
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import type { Meta, StoryObj } from "@storybook/react-vite";
|
|
2
|
+
import { useState } from "react";
|
|
3
|
+
|
|
4
|
+
import type { SortOption } from "../../hooks/sort.types";
|
|
5
|
+
import { SortControls } from "./sort-controls";
|
|
6
|
+
|
|
7
|
+
const meta = {
|
|
8
|
+
title: "DataDisplay/SortControls",
|
|
9
|
+
component: SortControls,
|
|
10
|
+
tags: ["autodocs"],
|
|
11
|
+
} satisfies Meta<typeof SortControls>;
|
|
12
|
+
|
|
13
|
+
export default meta;
|
|
14
|
+
type Story = StoryObj<typeof meta>;
|
|
15
|
+
|
|
16
|
+
const sortableFields = ["amount", "date", "description", "category"];
|
|
17
|
+
|
|
18
|
+
function SortControlsPreview() {
|
|
19
|
+
const [sortOptions, setSortOptions] = useState<SortOption[]>([
|
|
20
|
+
{ field: "date", direction: "desc" },
|
|
21
|
+
]);
|
|
22
|
+
|
|
23
|
+
return (
|
|
24
|
+
<SortControls
|
|
25
|
+
sortOptions={sortOptions}
|
|
26
|
+
sortableFields={sortableFields}
|
|
27
|
+
addSortOption={(option) => setSortOptions((prev) => [...prev, option])}
|
|
28
|
+
updateSortOption={(index, option) =>
|
|
29
|
+
setSortOptions((prev) => prev.map((o, i) => (i === index ? option : o)))
|
|
30
|
+
}
|
|
31
|
+
removeSortOption={(index) => setSortOptions((prev) => prev.filter((_, i) => i !== index))}
|
|
32
|
+
/>
|
|
33
|
+
);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export const Default: Story = {
|
|
37
|
+
args: {
|
|
38
|
+
sortOptions: [],
|
|
39
|
+
sortableFields,
|
|
40
|
+
addSortOption: () => {},
|
|
41
|
+
updateSortOption: () => {},
|
|
42
|
+
removeSortOption: () => {},
|
|
43
|
+
},
|
|
44
|
+
render: () => <SortControlsPreview />,
|
|
45
|
+
};
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import { ListOrdered, PlusCircle } from "lucide-react";
|
|
2
|
+
|
|
3
|
+
import type { SortField, SortOption } from "../../hooks/sort.types";
|
|
4
|
+
import {
|
|
5
|
+
DropdownMenu,
|
|
6
|
+
DropdownMenuContent,
|
|
7
|
+
DropdownMenuGroup,
|
|
8
|
+
DropdownMenuItem,
|
|
9
|
+
DropdownMenuLabel,
|
|
10
|
+
DropdownMenuSeparator,
|
|
11
|
+
DropdownMenuTrigger,
|
|
12
|
+
} from "../overlays/dropdown-menu";
|
|
13
|
+
import { Button } from "../primitives/button";
|
|
14
|
+
import { SortRow } from "./sort-row";
|
|
15
|
+
|
|
16
|
+
export interface SortControlsProps {
|
|
17
|
+
sortOptions: SortOption[];
|
|
18
|
+
sortableFields: SortField[];
|
|
19
|
+
addSortOption: (option: SortOption) => void;
|
|
20
|
+
updateSortOption: (index: number, option: SortOption) => void;
|
|
21
|
+
removeSortOption: (index: number) => void;
|
|
22
|
+
open?: boolean | undefined;
|
|
23
|
+
onOpenChange?: ((open: boolean) => void) | undefined;
|
|
24
|
+
focusedSortIndex?: (number | null) | undefined;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/** Multi-field sort builder, driven by the `SortOption`/`SortField` shared types. */
|
|
28
|
+
export function SortControls({
|
|
29
|
+
sortOptions,
|
|
30
|
+
sortableFields,
|
|
31
|
+
addSortOption,
|
|
32
|
+
updateSortOption,
|
|
33
|
+
removeSortOption,
|
|
34
|
+
open,
|
|
35
|
+
onOpenChange,
|
|
36
|
+
focusedSortIndex,
|
|
37
|
+
}: SortControlsProps) {
|
|
38
|
+
const availableFieldsToAdd = sortableFields.filter(
|
|
39
|
+
(field) => !sortOptions.some((option) => option.field === field),
|
|
40
|
+
);
|
|
41
|
+
|
|
42
|
+
return (
|
|
43
|
+
<DropdownMenu {...(open !== undefined && { open })} {...(onOpenChange && { onOpenChange })}>
|
|
44
|
+
<DropdownMenuTrigger asChild>
|
|
45
|
+
<Button variant="outline">
|
|
46
|
+
<ListOrdered className="mr-2 size-4" />
|
|
47
|
+
Sort
|
|
48
|
+
</Button>
|
|
49
|
+
</DropdownMenuTrigger>
|
|
50
|
+
<DropdownMenuContent className="w-80 space-y-2 p-2">
|
|
51
|
+
<DropdownMenuLabel>Define Sort Order</DropdownMenuLabel>
|
|
52
|
+
<DropdownMenuSeparator />
|
|
53
|
+
|
|
54
|
+
<DropdownMenuGroup className="space-y-1">
|
|
55
|
+
{sortOptions.map((sort, index) => {
|
|
56
|
+
const usedFields = sortOptions
|
|
57
|
+
.filter((_, i) => i !== index)
|
|
58
|
+
.map((option) => option.field);
|
|
59
|
+
return (
|
|
60
|
+
<DropdownMenuItem
|
|
61
|
+
key={sort.field}
|
|
62
|
+
onSelect={(e) => e.preventDefault()}
|
|
63
|
+
className={`p-0 focus:bg-transparent ${index === focusedSortIndex ? "bg-muted" : ""}`}
|
|
64
|
+
>
|
|
65
|
+
<SortRow
|
|
66
|
+
sortOption={sort}
|
|
67
|
+
index={index}
|
|
68
|
+
sortableFields={sortableFields}
|
|
69
|
+
usedFields={usedFields}
|
|
70
|
+
updateSortOption={updateSortOption}
|
|
71
|
+
removeSortOption={removeSortOption}
|
|
72
|
+
/>
|
|
73
|
+
</DropdownMenuItem>
|
|
74
|
+
);
|
|
75
|
+
})}
|
|
76
|
+
</DropdownMenuGroup>
|
|
77
|
+
{availableFieldsToAdd.length > 0 && (
|
|
78
|
+
<>
|
|
79
|
+
<DropdownMenuSeparator />
|
|
80
|
+
<DropdownMenuItem
|
|
81
|
+
onSelect={(e) => {
|
|
82
|
+
e.preventDefault();
|
|
83
|
+
const firstAvailable = availableFieldsToAdd[0];
|
|
84
|
+
if (firstAvailable) {
|
|
85
|
+
addSortOption({ field: firstAvailable, direction: "desc" });
|
|
86
|
+
}
|
|
87
|
+
}}
|
|
88
|
+
disabled={availableFieldsToAdd.length === 0}
|
|
89
|
+
className="mt-1"
|
|
90
|
+
>
|
|
91
|
+
<PlusCircle className="mr-2 size-4" />
|
|
92
|
+
Add Sort Criterion
|
|
93
|
+
</DropdownMenuItem>
|
|
94
|
+
</>
|
|
95
|
+
)}
|
|
96
|
+
</DropdownMenuContent>
|
|
97
|
+
</DropdownMenu>
|
|
98
|
+
);
|
|
99
|
+
}
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import type { SortField, SortOption } from "../../hooks/sort.types";
|
|
2
|
+
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "../forms/select";
|
|
3
|
+
import { Button } from "../primitives/button";
|
|
4
|
+
import { X } from "lucide-react";
|
|
5
|
+
|
|
6
|
+
export interface SortRowProps {
|
|
7
|
+
sortOption: SortOption;
|
|
8
|
+
index: number;
|
|
9
|
+
sortableFields: SortField[];
|
|
10
|
+
usedFields: SortField[];
|
|
11
|
+
updateSortOption: (index: number, option: SortOption) => void;
|
|
12
|
+
removeSortOption: (index: number) => void;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function SortRow({
|
|
16
|
+
sortOption,
|
|
17
|
+
index,
|
|
18
|
+
sortableFields,
|
|
19
|
+
usedFields,
|
|
20
|
+
updateSortOption,
|
|
21
|
+
removeSortOption,
|
|
22
|
+
}: SortRowProps) {
|
|
23
|
+
const availableFieldsForCurrentSelect = sortableFields.filter(
|
|
24
|
+
(field) => !usedFields.includes(field) || field === sortOption.field,
|
|
25
|
+
);
|
|
26
|
+
|
|
27
|
+
return (
|
|
28
|
+
<div key={sortOption.field} className="flex items-center gap-2">
|
|
29
|
+
<Select
|
|
30
|
+
value={sortOption.field}
|
|
31
|
+
onValueChange={(value) => updateSortOption(index, { ...sortOption, field: value })}
|
|
32
|
+
>
|
|
33
|
+
<SelectTrigger className="h-8 w-[130px] text-xs">
|
|
34
|
+
<SelectValue placeholder="Select field" />
|
|
35
|
+
</SelectTrigger>
|
|
36
|
+
<SelectContent>
|
|
37
|
+
{availableFieldsForCurrentSelect.map((field) => (
|
|
38
|
+
<SelectItem key={field} value={field} className="text-xs">
|
|
39
|
+
{field.charAt(0).toUpperCase() + field.slice(1)}
|
|
40
|
+
</SelectItem>
|
|
41
|
+
))}
|
|
42
|
+
</SelectContent>
|
|
43
|
+
</Select>
|
|
44
|
+
<Select
|
|
45
|
+
value={sortOption.direction}
|
|
46
|
+
onValueChange={(value) => {
|
|
47
|
+
if (value === "asc" || value === "desc") {
|
|
48
|
+
updateSortOption(index, { ...sortOption, direction: value });
|
|
49
|
+
}
|
|
50
|
+
}}
|
|
51
|
+
>
|
|
52
|
+
<SelectTrigger className="h-8 w-[110px] text-xs">
|
|
53
|
+
<SelectValue placeholder="Direction" />
|
|
54
|
+
</SelectTrigger>
|
|
55
|
+
<SelectContent>
|
|
56
|
+
<SelectItem value="asc" className="text-xs">
|
|
57
|
+
Ascending
|
|
58
|
+
</SelectItem>
|
|
59
|
+
<SelectItem value="desc" className="text-xs">
|
|
60
|
+
Descending
|
|
61
|
+
</SelectItem>
|
|
62
|
+
</SelectContent>
|
|
63
|
+
</Select>
|
|
64
|
+
<Button
|
|
65
|
+
variant="ghost"
|
|
66
|
+
size="sm"
|
|
67
|
+
onClick={() => removeSortOption(index)}
|
|
68
|
+
className="size-8 p-0"
|
|
69
|
+
>
|
|
70
|
+
<X className="text-muted-foreground size-4" />
|
|
71
|
+
</Button>
|
|
72
|
+
</div>
|
|
73
|
+
);
|
|
74
|
+
}
|
|
@@ -5,7 +5,7 @@ import { cn } from "../../lib/utils";
|
|
|
5
5
|
const Table = React.forwardRef<HTMLTableElement, React.HTMLAttributes<HTMLTableElement>>(
|
|
6
6
|
({ className, ...props }, ref) => (
|
|
7
7
|
<div className="w-full overflow-auto">
|
|
8
|
-
<table ref={ref} className={cn("
|
|
8
|
+
<table ref={ref} className={cn("w-full caption-bottom text-sm", className)} {...props} />
|
|
9
9
|
</div>
|
|
10
10
|
),
|
|
11
11
|
);
|
|
@@ -31,17 +31,13 @@ const TableFooter = React.forwardRef<
|
|
|
31
31
|
HTMLTableSectionElement,
|
|
32
32
|
React.HTMLAttributes<HTMLTableSectionElement>
|
|
33
33
|
>(({ className, ...props }, ref) => (
|
|
34
|
-
<tfoot ref={ref} className={cn("bg-
|
|
34
|
+
<tfoot ref={ref} className={cn("bg-muted font-medium", className)} {...props} />
|
|
35
35
|
));
|
|
36
36
|
TableFooter.displayName = "TableFooter";
|
|
37
37
|
|
|
38
38
|
const TableRow = React.forwardRef<HTMLTableRowElement, React.HTMLAttributes<HTMLTableRowElement>>(
|
|
39
39
|
({ className, ...props }, ref) => (
|
|
40
|
-
<tr
|
|
41
|
-
ref={ref}
|
|
42
|
-
className={cn("data-[state=selected]:bg-inset border-default border-b", className)}
|
|
43
|
-
{...props}
|
|
44
|
-
/>
|
|
40
|
+
<tr ref={ref} className={cn("data-[state=selected]:bg-muted border-b", className)} {...props} />
|
|
45
41
|
),
|
|
46
42
|
);
|
|
47
43
|
TableRow.displayName = "TableRow";
|
|
@@ -53,7 +49,7 @@ const TableHead = React.forwardRef<
|
|
|
53
49
|
<th
|
|
54
50
|
ref={ref}
|
|
55
51
|
className={cn(
|
|
56
|
-
"
|
|
52
|
+
"text-muted-foreground h-9 px-3 text-left align-middle text-xs font-medium [&:has([role=checkbox])]:pr-0",
|
|
57
53
|
className,
|
|
58
54
|
)}
|
|
59
55
|
{...props}
|
|
@@ -77,7 +73,7 @@ const TableCaption = React.forwardRef<
|
|
|
77
73
|
HTMLTableCaptionElement,
|
|
78
74
|
React.HTMLAttributes<HTMLTableCaptionElement>
|
|
79
75
|
>(({ className, ...props }, ref) => (
|
|
80
|
-
<caption ref={ref} className={cn("
|
|
76
|
+
<caption ref={ref} className={cn("text-muted-foreground mt-4 text-sm", className)} {...props} />
|
|
81
77
|
));
|
|
82
78
|
TableCaption.displayName = "TableCaption";
|
|
83
79
|
|
|
@@ -8,9 +8,9 @@ const alertVariants = cva(
|
|
|
8
8
|
{
|
|
9
9
|
variants: {
|
|
10
10
|
variant: {
|
|
11
|
-
default: "bg-
|
|
11
|
+
default: "bg-card text-card-foreground",
|
|
12
12
|
destructive:
|
|
13
|
-
"text-destructive bg-
|
|
13
|
+
"border-destructive/50 text-destructive bg-card [&>svg]:text-current *:data-[slot=alert-description]:text-destructive/90",
|
|
14
14
|
},
|
|
15
15
|
},
|
|
16
16
|
defaultVariants: {
|
|
@@ -38,7 +38,7 @@ function AlertTitle({ className, ...props }: React.ComponentProps<"div">) {
|
|
|
38
38
|
return (
|
|
39
39
|
<div
|
|
40
40
|
data-slot="alert-title"
|
|
41
|
-
className={cn("
|
|
41
|
+
className={cn("col-start-2 line-clamp-1 min-h-4 text-sm font-medium", className)}
|
|
42
42
|
{...props}
|
|
43
43
|
/>
|
|
44
44
|
);
|
|
@@ -49,7 +49,7 @@ function AlertDescription({ className, ...props }: React.ComponentProps<"div">)
|
|
|
49
49
|
<div
|
|
50
50
|
data-slot="alert-description"
|
|
51
51
|
className={cn(
|
|
52
|
-
"text-
|
|
52
|
+
"text-muted-foreground col-start-2 grid justify-items-start gap-1 text-sm [&_p]:leading-relaxed",
|
|
53
53
|
className,
|
|
54
54
|
)}
|
|
55
55
|
{...props}
|
|
@@ -37,9 +37,9 @@ export function EmptyState({
|
|
|
37
37
|
<section
|
|
38
38
|
className={cn(
|
|
39
39
|
"rounded-lg px-5 py-8",
|
|
40
|
-
variant === "default" && "
|
|
41
|
-
variant === "dashed" && "
|
|
42
|
-
variant === "search" && "
|
|
40
|
+
variant === "default" && "bg-card border",
|
|
41
|
+
variant === "dashed" && "bg-card border border-dashed",
|
|
42
|
+
variant === "search" && "bg-muted border border-dashed",
|
|
43
43
|
variant === "quiet" && "bg-surface",
|
|
44
44
|
centered
|
|
45
45
|
? cn(
|
|
@@ -51,13 +51,23 @@ export function EmptyState({
|
|
|
51
51
|
)}
|
|
52
52
|
>
|
|
53
53
|
{icon ? (
|
|
54
|
-
<div
|
|
54
|
+
<div
|
|
55
|
+
className="text-muted-foreground mb-3 flex size-11 items-center justify-center"
|
|
56
|
+
aria-hidden
|
|
57
|
+
>
|
|
55
58
|
{icon}
|
|
56
59
|
</div>
|
|
57
60
|
) : null}
|
|
58
61
|
<div className={cn("space-y-2", centered && "max-w-[34ch]")}>
|
|
59
|
-
<h2
|
|
60
|
-
|
|
62
|
+
<h2
|
|
63
|
+
className={cn(
|
|
64
|
+
"text-foreground font-semibold tracking-tight",
|
|
65
|
+
large ? "text-lg" : "text-base",
|
|
66
|
+
)}
|
|
67
|
+
>
|
|
68
|
+
{title}
|
|
69
|
+
</h2>
|
|
70
|
+
{description ? <p className="text-muted-foreground text-sm">{description}</p> : null}
|
|
61
71
|
</div>
|
|
62
72
|
{children ? <div className="mt-3">{children}</div> : null}
|
|
63
73
|
{action ? <div className="mt-4">{action}</div> : null}
|
|
@@ -0,0 +1,28 @@
|
|
|
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
|
+
};
|
|
@@ -0,0 +1,33 @@
|
|
|
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", 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
|
+
});
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import type { Meta, StoryObj } from "@storybook/react-vite";
|
|
2
|
+
|
|
3
|
+
import { FileUploadStatus } from "./file-upload-status";
|
|
4
|
+
|
|
5
|
+
const meta = {
|
|
6
|
+
title: "Feedback/FileUploadStatus",
|
|
7
|
+
component: FileUploadStatus,
|
|
8
|
+
tags: ["autodocs"],
|
|
9
|
+
} satisfies Meta<typeof FileUploadStatus>;
|
|
10
|
+
|
|
11
|
+
export default meta;
|
|
12
|
+
type Story = StoryObj<typeof meta>;
|
|
13
|
+
|
|
14
|
+
export const Uploading: Story = {
|
|
15
|
+
render: () => (
|
|
16
|
+
<div className="w-80">
|
|
17
|
+
<FileUploadStatus uploadStatus={{ status: "uploading", stats: { progress: 42 } }} />
|
|
18
|
+
</div>
|
|
19
|
+
),
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
export const Queued: Story = {
|
|
23
|
+
render: () => (
|
|
24
|
+
<div className="w-80">
|
|
25
|
+
<FileUploadStatus uploadStatus={{ status: "queued" }} />
|
|
26
|
+
</div>
|
|
27
|
+
),
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
export const Done: Story = {
|
|
31
|
+
render: () => (
|
|
32
|
+
<div className="w-80">
|
|
33
|
+
<FileUploadStatus
|
|
34
|
+
uploadStatus={{
|
|
35
|
+
status: "done",
|
|
36
|
+
stats: {
|
|
37
|
+
processingTime: 4200,
|
|
38
|
+
total: 128,
|
|
39
|
+
created: 90,
|
|
40
|
+
updated: 30,
|
|
41
|
+
skipped: 8,
|
|
42
|
+
},
|
|
43
|
+
}}
|
|
44
|
+
/>
|
|
45
|
+
</div>
|
|
46
|
+
),
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
export const Error: Story = {
|
|
50
|
+
render: () => (
|
|
51
|
+
<div className="w-80">
|
|
52
|
+
<FileUploadStatus
|
|
53
|
+
uploadStatus={{ status: "error", error: "Could not parse the CSV file." }}
|
|
54
|
+
/>
|
|
55
|
+
</div>
|
|
56
|
+
),
|
|
57
|
+
};
|