create-app-ui 1.0.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 +117 -0
- package/boilerplate/README.md +18 -0
- package/boilerplate/react-base/.env.example +1 -0
- package/boilerplate/react-base/README.md +3 -0
- package/boilerplate/react-base/components.json +19 -0
- package/boilerplate/react-base/eslint.config.js +32 -0
- package/boilerplate/react-base/index.html +12 -0
- package/boilerplate/react-base/package.json +71 -0
- package/boilerplate/react-base/postcss.config.js +6 -0
- package/boilerplate/react-base/prettier.config.js +6 -0
- package/boilerplate/react-base/src/api/axios.ts +20 -0
- package/boilerplate/react-base/src/app/store.ts +13 -0
- package/boilerplate/react-base/src/components/data-table.tsx +919 -0
- package/boilerplate/react-base/src/components/ui/accordion.tsx +44 -0
- package/boilerplate/react-base/src/components/ui/alert-dialog.tsx +105 -0
- package/boilerplate/react-base/src/components/ui/alert.tsx +40 -0
- package/boilerplate/react-base/src/components/ui/avatar.tsx +30 -0
- package/boilerplate/react-base/src/components/ui/badge.tsx +27 -0
- package/boilerplate/react-base/src/components/ui/bar-chart.tsx +76 -0
- package/boilerplate/react-base/src/components/ui/breadcrumb.tsx +87 -0
- package/boilerplate/react-base/src/components/ui/button.tsx +34 -0
- package/boilerplate/react-base/src/components/ui/calendar.tsx +63 -0
- package/boilerplate/react-base/src/components/ui/card.tsx +36 -0
- package/boilerplate/react-base/src/components/ui/chart.tsx +280 -0
- package/boilerplate/react-base/src/components/ui/checkbox.tsx +51 -0
- package/boilerplate/react-base/src/components/ui/context-menu.tsx +173 -0
- package/boilerplate/react-base/src/components/ui/date-picker.tsx +42 -0
- package/boilerplate/react-base/src/components/ui/dialog.tsx +87 -0
- package/boilerplate/react-base/src/components/ui/drawer.tsx +81 -0
- package/boilerplate/react-base/src/components/ui/dropdown-menu.tsx +81 -0
- package/boilerplate/react-base/src/components/ui/dropdown-types.ts +28 -0
- package/boilerplate/react-base/src/components/ui/field.tsx +194 -0
- package/boilerplate/react-base/src/components/ui/hover-card.tsx +26 -0
- package/boilerplate/react-base/src/components/ui/input-group.tsx +98 -0
- package/boilerplate/react-base/src/components/ui/input-otp.tsx +63 -0
- package/boilerplate/react-base/src/components/ui/input.tsx +12 -0
- package/boilerplate/react-base/src/components/ui/item.tsx +152 -0
- package/boilerplate/react-base/src/components/ui/kbd.tsx +13 -0
- package/boilerplate/react-base/src/components/ui/label.tsx +14 -0
- package/boilerplate/react-base/src/components/ui/line-chart.tsx +65 -0
- package/boilerplate/react-base/src/components/ui/menubar.tsx +217 -0
- package/boilerplate/react-base/src/components/ui/multi-select-dropdown.tsx +200 -0
- package/boilerplate/react-base/src/components/ui/navigation-menu.tsx +120 -0
- package/boilerplate/react-base/src/components/ui/pie-chart.tsx +87 -0
- package/boilerplate/react-base/src/components/ui/popover.tsx +29 -0
- package/boilerplate/react-base/src/components/ui/progress.tsx +19 -0
- package/boilerplate/react-base/src/components/ui/radio-group.tsx +36 -0
- package/boilerplate/react-base/src/components/ui/scroll-area.tsx +38 -0
- package/boilerplate/react-base/src/components/ui/searchable-dropdown.tsx +118 -0
- package/boilerplate/react-base/src/components/ui/select.tsx +140 -0
- package/boilerplate/react-base/src/components/ui/separator.tsx +20 -0
- package/boilerplate/react-base/src/components/ui/sheet.tsx +70 -0
- package/boilerplate/react-base/src/components/ui/sidebar.tsx +470 -0
- package/boilerplate/react-base/src/components/ui/skeleton.tsx +11 -0
- package/boilerplate/react-base/src/components/ui/slider.tsx +23 -0
- package/boilerplate/react-base/src/components/ui/sonner.tsx +21 -0
- package/boilerplate/react-base/src/components/ui/sparkline.tsx +38 -0
- package/boilerplate/react-base/src/components/ui/spinner.tsx +10 -0
- package/boilerplate/react-base/src/components/ui/switch.tsx +16 -0
- package/boilerplate/react-base/src/components/ui/table.tsx +80 -0
- package/boilerplate/react-base/src/components/ui/tabs.tsx +32 -0
- package/boilerplate/react-base/src/components/ui/textarea.tsx +12 -0
- package/boilerplate/react-base/src/components/ui/toggle-group.tsx +49 -0
- package/boilerplate/react-base/src/components/ui/toggle.tsx +33 -0
- package/boilerplate/react-base/src/components/ui/tooltip.tsx +23 -0
- package/boilerplate/react-base/src/components/ui/typography.tsx +76 -0
- package/boilerplate/react-base/src/config/constants.ts +3 -0
- package/boilerplate/react-base/src/config/theme.ts +432 -0
- package/boilerplate/react-base/src/config/user.ts +52 -0
- package/boilerplate/react-base/src/context/theme-provider.tsx +12 -0
- package/boilerplate/react-base/src/features/auth/authSlice.ts +19 -0
- package/boilerplate/react-base/src/hooks/index.ts +1 -0
- package/boilerplate/react-base/src/hooks/use-mobile.ts +17 -0
- package/boilerplate/react-base/src/lib/utils.ts +6 -0
- package/boilerplate/react-base/src/routes/index.tsx +7 -0
- package/boilerplate/react-base/src/styles/globals.css +15 -0
- package/boilerplate/react-base/src/vite-env.d.ts +31 -0
- package/boilerplate/react-base/tailwind.config.ts +75 -0
- package/boilerplate/react-base/tsconfig.app.json +20 -0
- package/boilerplate/react-base/tsconfig.json +7 -0
- package/boilerplate/react-base/tsconfig.node.json +16 -0
- package/boilerplate/react-base/vite.config.ts +12 -0
- package/dist/bin/index.js +8 -0
- package/dist/src/cli-args.js +52 -0
- package/dist/src/generator.js +85 -0
- package/dist/src/installer.js +7 -0
- package/dist/src/paths.js +61 -0
- package/dist/src/prompts.js +79 -0
- package/dist/src/replace-placeholders.js +22 -0
- package/dist/src/utils.js +16 -0
- package/package.json +63 -0
- package/templates/admin-portal/README.md +26 -0
- package/templates/admin-portal/src/App.tsx +85 -0
- package/templates/admin-portal/src/assets/auth-hero.jpg +0 -0
- package/templates/admin-portal/src/assets/brand-logo.png +0 -0
- package/templates/admin-portal/src/components/app-breadcrumb.tsx +41 -0
- package/templates/admin-portal/src/components/app-header.tsx +20 -0
- package/templates/admin-portal/src/components/app-sidebar.tsx +78 -0
- package/templates/admin-portal/src/components/auth-layout.tsx +66 -0
- package/templates/admin-portal/src/components/dashboard-metric-card.tsx +105 -0
- package/templates/admin-portal/src/components/data-table.tsx +919 -0
- package/templates/admin-portal/src/components/layout-shell.tsx +23 -0
- package/templates/admin-portal/src/components/notifications-sheet.tsx +91 -0
- package/templates/admin-portal/src/components/sidebar-nav.tsx +164 -0
- package/templates/admin-portal/src/components/user-avatar.tsx +26 -0
- package/templates/admin-portal/src/components/user-menu.tsx +163 -0
- package/templates/admin-portal/src/config/branding.ts +17 -0
- package/templates/admin-portal/src/config/chart-data.ts +44 -0
- package/templates/admin-portal/src/config/navigation.ts +42 -0
- package/templates/admin-portal/src/context/auth-context.tsx +32 -0
- package/templates/admin-portal/src/lib/breadcrumbs.ts +58 -0
- package/templates/admin-portal/src/main.tsx +18 -0
- package/templates/admin-portal/src/pages/components/demo-columns.tsx +170 -0
- package/templates/admin-portal/src/pages/components.tsx +1368 -0
- package/templates/admin-portal/src/pages/dashboard.tsx +143 -0
- package/templates/admin-portal/src/pages/login.tsx +81 -0
- package/templates/admin-portal/src/pages/settings/notifications.tsx +31 -0
- package/templates/admin-portal/src/pages/settings/profile.tsx +26 -0
- package/templates/admin-portal/src/pages/signup.tsx +81 -0
- package/templates/admin-portal/src/pages/users.tsx +12 -0
- package/templates/admin-portal/tsconfig.json +10 -0
- package/templates/blank/README.md +15 -0
- package/templates/blank/src/App.tsx +5 -0
- package/templates/blank/src/main.tsx +15 -0
- package/templates/blank/src/pages/home.tsx +20 -0
- package/templates/blank/tsconfig.json +10 -0
- package/templates/tsconfig.overlay.base.json +7 -0
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
import { Slot } from "@radix-ui/react-slot";
|
|
2
|
+
import { cva, type VariantProps } from "class-variance-authority";
|
|
3
|
+
import * as React from "react";
|
|
4
|
+
import { Separator } from "@/components/ui/separator";
|
|
5
|
+
import { ui } from "@/config/theme";
|
|
6
|
+
import { cn } from "@/lib/utils";
|
|
7
|
+
|
|
8
|
+
function ItemGroup({ className, ...props }: React.ComponentProps<"div">) {
|
|
9
|
+
return <div role="list" data-slot="item-group" className={cn("flex flex-col gap-2", className)} {...props} />;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
function ItemSeparator({ className, ...props }: React.ComponentProps<typeof Separator>) {
|
|
13
|
+
return <Separator data-slot="item-separator" className={cn("my-2", className)} {...props} />;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
const itemVariants = cva(
|
|
17
|
+
ui("itemBase"),
|
|
18
|
+
{
|
|
19
|
+
variants: {
|
|
20
|
+
variant: {
|
|
21
|
+
default: "bg-transparent",
|
|
22
|
+
outline: "border-border",
|
|
23
|
+
muted: "bg-muted/50",
|
|
24
|
+
},
|
|
25
|
+
size: {
|
|
26
|
+
default: "gap-4 p-4",
|
|
27
|
+
sm: "gap-3 p-3",
|
|
28
|
+
},
|
|
29
|
+
},
|
|
30
|
+
defaultVariants: {
|
|
31
|
+
variant: "default",
|
|
32
|
+
size: "default",
|
|
33
|
+
},
|
|
34
|
+
},
|
|
35
|
+
);
|
|
36
|
+
|
|
37
|
+
function Item({
|
|
38
|
+
className,
|
|
39
|
+
variant = "default",
|
|
40
|
+
size = "default",
|
|
41
|
+
asChild = false,
|
|
42
|
+
...props
|
|
43
|
+
}: React.ComponentProps<"div"> &
|
|
44
|
+
VariantProps<typeof itemVariants> & {
|
|
45
|
+
asChild?: boolean;
|
|
46
|
+
}) {
|
|
47
|
+
const Comp = asChild ? Slot : "div";
|
|
48
|
+
return (
|
|
49
|
+
<Comp
|
|
50
|
+
role="listitem"
|
|
51
|
+
data-slot="item"
|
|
52
|
+
data-variant={variant}
|
|
53
|
+
data-size={size}
|
|
54
|
+
className={cn(itemVariants({ variant, size, className }))}
|
|
55
|
+
{...props}
|
|
56
|
+
/>
|
|
57
|
+
);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const itemMediaVariants = cva(
|
|
61
|
+
"flex shrink-0 items-center justify-center [&_svg]:pointer-events-none",
|
|
62
|
+
{
|
|
63
|
+
variants: {
|
|
64
|
+
variant: {
|
|
65
|
+
default: "",
|
|
66
|
+
icon: "size-9 rounded-md border bg-muted [&_svg:not([class*='size-'])]:size-4",
|
|
67
|
+
image: "size-10 overflow-hidden rounded-md [&_img]:size-full [&_img]:object-cover",
|
|
68
|
+
},
|
|
69
|
+
},
|
|
70
|
+
defaultVariants: {
|
|
71
|
+
variant: "default",
|
|
72
|
+
},
|
|
73
|
+
},
|
|
74
|
+
);
|
|
75
|
+
|
|
76
|
+
function ItemMedia({
|
|
77
|
+
className,
|
|
78
|
+
variant = "default",
|
|
79
|
+
...props
|
|
80
|
+
}: React.ComponentProps<"div"> & VariantProps<typeof itemMediaVariants>) {
|
|
81
|
+
return (
|
|
82
|
+
<div
|
|
83
|
+
data-slot="item-media"
|
|
84
|
+
data-variant={variant}
|
|
85
|
+
className={cn(itemMediaVariants({ variant, className }))}
|
|
86
|
+
{...props}
|
|
87
|
+
/>
|
|
88
|
+
);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function ItemContent({ className, ...props }: React.ComponentProps<"div">) {
|
|
92
|
+
return (
|
|
93
|
+
<div
|
|
94
|
+
data-slot="item-content"
|
|
95
|
+
className={cn("flex min-w-0 flex-1 flex-col gap-1 [&+[data-slot=item-content]]:flex-none", className)}
|
|
96
|
+
{...props}
|
|
97
|
+
/>
|
|
98
|
+
);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function ItemTitle({ className, ...props }: React.ComponentProps<"div">) {
|
|
102
|
+
return (
|
|
103
|
+
<div data-slot="item-title" className={cn("flex w-fit items-center gap-2 text-sm font-medium leading-snug", className)} {...props} />
|
|
104
|
+
);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function ItemDescription({ className, ...props }: React.ComponentProps<"p">) {
|
|
108
|
+
return (
|
|
109
|
+
<p
|
|
110
|
+
data-slot="item-description"
|
|
111
|
+
className={cn(ui("typographyMuted"), "leading-normal [&>a]:underline [&>a]:underline-offset-4", className)}
|
|
112
|
+
{...props}
|
|
113
|
+
/>
|
|
114
|
+
);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function ItemActions({ className, ...props }: React.ComponentProps<"div">) {
|
|
118
|
+
return <div data-slot="item-actions" className={cn("flex shrink-0 items-center gap-2", className)} {...props} />;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function ItemHeader({ className, ...props }: React.ComponentProps<"div">) {
|
|
122
|
+
return (
|
|
123
|
+
<div
|
|
124
|
+
data-slot="item-header"
|
|
125
|
+
className={cn("flex w-full flex-wrap items-center gap-2 pb-2", className)}
|
|
126
|
+
{...props}
|
|
127
|
+
/>
|
|
128
|
+
);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
function ItemFooter({ className, ...props }: React.ComponentProps<"div">) {
|
|
132
|
+
return (
|
|
133
|
+
<div
|
|
134
|
+
data-slot="item-footer"
|
|
135
|
+
className={cn("flex w-full flex-wrap items-center gap-2 pt-2", className)}
|
|
136
|
+
{...props}
|
|
137
|
+
/>
|
|
138
|
+
);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
export {
|
|
142
|
+
Item,
|
|
143
|
+
ItemActions,
|
|
144
|
+
ItemContent,
|
|
145
|
+
ItemDescription,
|
|
146
|
+
ItemFooter,
|
|
147
|
+
ItemGroup,
|
|
148
|
+
ItemHeader,
|
|
149
|
+
ItemMedia,
|
|
150
|
+
ItemSeparator,
|
|
151
|
+
ItemTitle,
|
|
152
|
+
};
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
import { ui } from "@/config/theme";
|
|
3
|
+
import { cn } from "@/lib/utils";
|
|
4
|
+
|
|
5
|
+
function Kbd({ className, ...props }: React.ComponentProps<"kbd">) {
|
|
6
|
+
return <kbd className={cn(ui("kbd"), className)} {...props} />;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
function KbdGroup({ className, ...props }: React.ComponentProps<"div">) {
|
|
10
|
+
return <div className={cn("inline-flex items-center gap-1", className)} {...props} />;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export { Kbd, KbdGroup };
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
import * as LabelPrimitive from "@radix-ui/react-label";
|
|
3
|
+
import { ui } from "@/config/theme";
|
|
4
|
+
import { cn } from "@/lib/utils";
|
|
5
|
+
|
|
6
|
+
const Label = React.forwardRef<
|
|
7
|
+
React.ElementRef<typeof LabelPrimitive.Root>,
|
|
8
|
+
React.ComponentPropsWithoutRef<typeof LabelPrimitive.Root>
|
|
9
|
+
>(({ className, ...props }, ref) => (
|
|
10
|
+
<LabelPrimitive.Root ref={ref} className={cn(ui("label"), className)} {...props} />
|
|
11
|
+
));
|
|
12
|
+
Label.displayName = "Label";
|
|
13
|
+
|
|
14
|
+
export { Label };
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import { CartesianGrid, Line, LineChart as RechartsLineChart, XAxis, YAxis } from "recharts";
|
|
2
|
+
import {
|
|
3
|
+
ChartContainer,
|
|
4
|
+
ChartLegend,
|
|
5
|
+
ChartLegendContent,
|
|
6
|
+
ChartTooltip,
|
|
7
|
+
ChartTooltipContent,
|
|
8
|
+
type ChartConfig,
|
|
9
|
+
} from "@/components/ui/chart";
|
|
10
|
+
import { cn } from "@/lib/utils";
|
|
11
|
+
|
|
12
|
+
export type LineChartDataPoint = Record<string, string | number>;
|
|
13
|
+
|
|
14
|
+
type LineChartProps = {
|
|
15
|
+
data: LineChartDataPoint[];
|
|
16
|
+
config: ChartConfig;
|
|
17
|
+
dataKeys: string[];
|
|
18
|
+
categoryKey: string;
|
|
19
|
+
className?: string;
|
|
20
|
+
showLegend?: boolean;
|
|
21
|
+
showDots?: boolean;
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
export function LineChart({
|
|
25
|
+
data,
|
|
26
|
+
config,
|
|
27
|
+
dataKeys,
|
|
28
|
+
categoryKey,
|
|
29
|
+
className,
|
|
30
|
+
showLegend = true,
|
|
31
|
+
showDots = true,
|
|
32
|
+
}: LineChartProps) {
|
|
33
|
+
return (
|
|
34
|
+
<ChartContainer config={config} className={cn("aspect-auto h-[300px] w-full", className)}>
|
|
35
|
+
<RechartsLineChart
|
|
36
|
+
accessibilityLayer
|
|
37
|
+
data={data}
|
|
38
|
+
margin={{ left: 12, right: 12, top: 12, bottom: 0 }}
|
|
39
|
+
>
|
|
40
|
+
<CartesianGrid vertical={false} strokeDasharray="3 3" />
|
|
41
|
+
<XAxis
|
|
42
|
+
dataKey={categoryKey}
|
|
43
|
+
tickLine={false}
|
|
44
|
+
axisLine={false}
|
|
45
|
+
tickMargin={8}
|
|
46
|
+
tickFormatter={(value) => String(value).slice(0, 3)}
|
|
47
|
+
/>
|
|
48
|
+
<YAxis tickLine={false} axisLine={false} tickMargin={8} width={40} />
|
|
49
|
+
<ChartTooltip cursor={false} content={<ChartTooltipContent indicator="line" />} />
|
|
50
|
+
{showLegend && <ChartLegend content={<ChartLegendContent />} />}
|
|
51
|
+
{dataKeys.map((key) => (
|
|
52
|
+
<Line
|
|
53
|
+
key={key}
|
|
54
|
+
dataKey={key}
|
|
55
|
+
type="monotone"
|
|
56
|
+
stroke={`var(--color-${key})`}
|
|
57
|
+
strokeWidth={2}
|
|
58
|
+
dot={showDots ? { fill: `var(--color-${key})`, r: 3 } : false}
|
|
59
|
+
activeDot={{ r: 5 }}
|
|
60
|
+
/>
|
|
61
|
+
))}
|
|
62
|
+
</RechartsLineChart>
|
|
63
|
+
</ChartContainer>
|
|
64
|
+
);
|
|
65
|
+
}
|
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
import * as MenubarPrimitive from "@radix-ui/react-menubar";
|
|
2
|
+
import { Check, ChevronRight, Circle } from "lucide-react";
|
|
3
|
+
import * as React from "react";
|
|
4
|
+
import { ui } from "@/config/theme";
|
|
5
|
+
import { cn } from "@/lib/utils";
|
|
6
|
+
|
|
7
|
+
function MenubarMenu({ ...props }: React.ComponentProps<typeof MenubarPrimitive.Menu>) {
|
|
8
|
+
return <MenubarPrimitive.Menu {...props} />;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
function MenubarGroup({ ...props }: React.ComponentProps<typeof MenubarPrimitive.Group>) {
|
|
12
|
+
return <MenubarPrimitive.Group {...props} />;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function MenubarPortal({ ...props }: React.ComponentProps<typeof MenubarPrimitive.Portal>) {
|
|
16
|
+
return <MenubarPrimitive.Portal {...props} />;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function MenubarRadioGroup({ ...props }: React.ComponentProps<typeof MenubarPrimitive.RadioGroup>) {
|
|
20
|
+
return <MenubarPrimitive.RadioGroup {...props} />;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function MenubarSub({ ...props }: React.ComponentProps<typeof MenubarPrimitive.Sub>) {
|
|
24
|
+
return <MenubarPrimitive.Sub {...props} />;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const Menubar = React.forwardRef<
|
|
28
|
+
React.ElementRef<typeof MenubarPrimitive.Root>,
|
|
29
|
+
React.ComponentPropsWithoutRef<typeof MenubarPrimitive.Root>
|
|
30
|
+
>(({ className, ...props }, ref) => (
|
|
31
|
+
<MenubarPrimitive.Root
|
|
32
|
+
ref={ref}
|
|
33
|
+
className={cn("flex h-9 items-center space-x-1 rounded-md border bg-background p-1 shadow-sm", className)}
|
|
34
|
+
{...props}
|
|
35
|
+
/>
|
|
36
|
+
));
|
|
37
|
+
Menubar.displayName = MenubarPrimitive.Root.displayName;
|
|
38
|
+
|
|
39
|
+
const MenubarTrigger = React.forwardRef<
|
|
40
|
+
React.ElementRef<typeof MenubarPrimitive.Trigger>,
|
|
41
|
+
React.ComponentPropsWithoutRef<typeof MenubarPrimitive.Trigger>
|
|
42
|
+
>(({ className, ...props }, ref) => (
|
|
43
|
+
<MenubarPrimitive.Trigger
|
|
44
|
+
ref={ref}
|
|
45
|
+
className={cn(
|
|
46
|
+
"flex cursor-default select-none items-center rounded-sm px-3 py-1 text-sm font-medium outline-none focus:bg-accent focus:text-accent-foreground data-[state=open]:bg-accent data-[state=open]:text-accent-foreground",
|
|
47
|
+
className,
|
|
48
|
+
)}
|
|
49
|
+
{...props}
|
|
50
|
+
/>
|
|
51
|
+
));
|
|
52
|
+
MenubarTrigger.displayName = MenubarPrimitive.Trigger.displayName;
|
|
53
|
+
|
|
54
|
+
const MenubarSubTrigger = React.forwardRef<
|
|
55
|
+
React.ElementRef<typeof MenubarPrimitive.SubTrigger>,
|
|
56
|
+
React.ComponentPropsWithoutRef<typeof MenubarPrimitive.SubTrigger> & {
|
|
57
|
+
inset?: boolean;
|
|
58
|
+
}
|
|
59
|
+
>(({ className, inset, children, ...props }, ref) => (
|
|
60
|
+
<MenubarPrimitive.SubTrigger
|
|
61
|
+
ref={ref}
|
|
62
|
+
className={cn(
|
|
63
|
+
"flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none focus:bg-accent focus:text-accent-foreground data-[state=open]:bg-accent data-[state=open]:text-accent-foreground",
|
|
64
|
+
inset && "pl-8",
|
|
65
|
+
className,
|
|
66
|
+
)}
|
|
67
|
+
{...props}
|
|
68
|
+
>
|
|
69
|
+
{children}
|
|
70
|
+
<ChevronRight className="ml-auto h-4 w-4" />
|
|
71
|
+
</MenubarPrimitive.SubTrigger>
|
|
72
|
+
));
|
|
73
|
+
MenubarSubTrigger.displayName = MenubarPrimitive.SubTrigger.displayName;
|
|
74
|
+
|
|
75
|
+
const MenubarSubContent = React.forwardRef<
|
|
76
|
+
React.ElementRef<typeof MenubarPrimitive.SubContent>,
|
|
77
|
+
React.ComponentPropsWithoutRef<typeof MenubarPrimitive.SubContent>
|
|
78
|
+
>(({ className, ...props }, ref) => (
|
|
79
|
+
<MenubarPrimitive.SubContent
|
|
80
|
+
ref={ref}
|
|
81
|
+
className={cn(
|
|
82
|
+
ui("popoverMenu"),
|
|
83
|
+
className,
|
|
84
|
+
)}
|
|
85
|
+
{...props}
|
|
86
|
+
/>
|
|
87
|
+
));
|
|
88
|
+
MenubarSubContent.displayName = MenubarPrimitive.SubContent.displayName;
|
|
89
|
+
|
|
90
|
+
const MenubarContent = React.forwardRef<
|
|
91
|
+
React.ElementRef<typeof MenubarPrimitive.Content>,
|
|
92
|
+
React.ComponentPropsWithoutRef<typeof MenubarPrimitive.Content>
|
|
93
|
+
>(({ className, align = "start", alignOffset = -4, sideOffset = 8, ...props }, ref) => (
|
|
94
|
+
<MenubarPortal>
|
|
95
|
+
<MenubarPrimitive.Content
|
|
96
|
+
ref={ref}
|
|
97
|
+
align={align}
|
|
98
|
+
alignOffset={alignOffset}
|
|
99
|
+
sideOffset={sideOffset}
|
|
100
|
+
className={cn(
|
|
101
|
+
ui("popoverMenuWide"),
|
|
102
|
+
className,
|
|
103
|
+
)}
|
|
104
|
+
{...props}
|
|
105
|
+
/>
|
|
106
|
+
</MenubarPortal>
|
|
107
|
+
));
|
|
108
|
+
MenubarContent.displayName = MenubarPrimitive.Content.displayName;
|
|
109
|
+
|
|
110
|
+
const MenubarItem = React.forwardRef<
|
|
111
|
+
React.ElementRef<typeof MenubarPrimitive.Item>,
|
|
112
|
+
React.ComponentPropsWithoutRef<typeof MenubarPrimitive.Item> & {
|
|
113
|
+
inset?: boolean;
|
|
114
|
+
}
|
|
115
|
+
>(({ className, inset, ...props }, ref) => (
|
|
116
|
+
<MenubarPrimitive.Item
|
|
117
|
+
ref={ref}
|
|
118
|
+
className={cn(
|
|
119
|
+
"relative flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
|
|
120
|
+
inset && "pl-8",
|
|
121
|
+
className,
|
|
122
|
+
)}
|
|
123
|
+
{...props}
|
|
124
|
+
/>
|
|
125
|
+
));
|
|
126
|
+
MenubarItem.displayName = MenubarPrimitive.Item.displayName;
|
|
127
|
+
|
|
128
|
+
const MenubarCheckboxItem = React.forwardRef<
|
|
129
|
+
React.ElementRef<typeof MenubarPrimitive.CheckboxItem>,
|
|
130
|
+
React.ComponentPropsWithoutRef<typeof MenubarPrimitive.CheckboxItem>
|
|
131
|
+
>(({ className, children, checked, ...props }, ref) => (
|
|
132
|
+
<MenubarPrimitive.CheckboxItem
|
|
133
|
+
ref={ref}
|
|
134
|
+
className={cn(
|
|
135
|
+
"relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
|
|
136
|
+
className,
|
|
137
|
+
)}
|
|
138
|
+
checked={checked}
|
|
139
|
+
{...props}
|
|
140
|
+
>
|
|
141
|
+
<span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
|
|
142
|
+
<MenubarPrimitive.ItemIndicator>
|
|
143
|
+
<Check className="h-4 w-4" />
|
|
144
|
+
</MenubarPrimitive.ItemIndicator>
|
|
145
|
+
</span>
|
|
146
|
+
{children}
|
|
147
|
+
</MenubarPrimitive.CheckboxItem>
|
|
148
|
+
));
|
|
149
|
+
MenubarCheckboxItem.displayName = MenubarPrimitive.CheckboxItem.displayName;
|
|
150
|
+
|
|
151
|
+
const MenubarRadioItem = React.forwardRef<
|
|
152
|
+
React.ElementRef<typeof MenubarPrimitive.RadioItem>,
|
|
153
|
+
React.ComponentPropsWithoutRef<typeof MenubarPrimitive.RadioItem>
|
|
154
|
+
>(({ className, children, ...props }, ref) => (
|
|
155
|
+
<MenubarPrimitive.RadioItem
|
|
156
|
+
ref={ref}
|
|
157
|
+
className={cn(
|
|
158
|
+
"relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
|
|
159
|
+
className,
|
|
160
|
+
)}
|
|
161
|
+
{...props}
|
|
162
|
+
>
|
|
163
|
+
<span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
|
|
164
|
+
<MenubarPrimitive.ItemIndicator>
|
|
165
|
+
<Circle className="h-2 w-2 fill-current" />
|
|
166
|
+
</MenubarPrimitive.ItemIndicator>
|
|
167
|
+
</span>
|
|
168
|
+
{children}
|
|
169
|
+
</MenubarPrimitive.RadioItem>
|
|
170
|
+
));
|
|
171
|
+
MenubarRadioItem.displayName = MenubarPrimitive.RadioItem.displayName;
|
|
172
|
+
|
|
173
|
+
const MenubarLabel = React.forwardRef<
|
|
174
|
+
React.ElementRef<typeof MenubarPrimitive.Label>,
|
|
175
|
+
React.ComponentPropsWithoutRef<typeof MenubarPrimitive.Label> & {
|
|
176
|
+
inset?: boolean;
|
|
177
|
+
}
|
|
178
|
+
>(({ className, inset, ...props }, ref) => (
|
|
179
|
+
<MenubarPrimitive.Label
|
|
180
|
+
ref={ref}
|
|
181
|
+
className={cn("px-2 py-1.5 text-sm font-semibold", inset && "pl-8", className)}
|
|
182
|
+
{...props}
|
|
183
|
+
/>
|
|
184
|
+
));
|
|
185
|
+
MenubarLabel.displayName = MenubarPrimitive.Label.displayName;
|
|
186
|
+
|
|
187
|
+
const MenubarSeparator = React.forwardRef<
|
|
188
|
+
React.ElementRef<typeof MenubarPrimitive.Separator>,
|
|
189
|
+
React.ComponentPropsWithoutRef<typeof MenubarPrimitive.Separator>
|
|
190
|
+
>(({ className, ...props }, ref) => (
|
|
191
|
+
<MenubarPrimitive.Separator ref={ref} className={cn("-mx-1 my-1 h-px bg-muted", className)} {...props} />
|
|
192
|
+
));
|
|
193
|
+
MenubarSeparator.displayName = MenubarPrimitive.Separator.displayName;
|
|
194
|
+
|
|
195
|
+
function MenubarShortcut({ className, ...props }: React.ComponentProps<"span">) {
|
|
196
|
+
return <span className={cn("ml-auto text-xs tracking-widest text-muted-foreground", className)} {...props} />;
|
|
197
|
+
}
|
|
198
|
+
MenubarShortcut.displayName = "MenubarShortcut";
|
|
199
|
+
|
|
200
|
+
export {
|
|
201
|
+
Menubar,
|
|
202
|
+
MenubarCheckboxItem,
|
|
203
|
+
MenubarContent,
|
|
204
|
+
MenubarGroup,
|
|
205
|
+
MenubarItem,
|
|
206
|
+
MenubarLabel,
|
|
207
|
+
MenubarMenu,
|
|
208
|
+
MenubarPortal,
|
|
209
|
+
MenubarRadioGroup,
|
|
210
|
+
MenubarRadioItem,
|
|
211
|
+
MenubarSeparator,
|
|
212
|
+
MenubarShortcut,
|
|
213
|
+
MenubarSub,
|
|
214
|
+
MenubarSubContent,
|
|
215
|
+
MenubarSubTrigger,
|
|
216
|
+
MenubarTrigger,
|
|
217
|
+
};
|
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
import { Check, ChevronsUpDown, Search, X } from "lucide-react";
|
|
2
|
+
import * as React from "react";
|
|
3
|
+
import { Badge } from "@/components/ui/badge";
|
|
4
|
+
import { Button } from "@/components/ui/button";
|
|
5
|
+
import { Checkbox } from "@/components/ui/checkbox";
|
|
6
|
+
import { dropdownClasses, type DropdownOption } from "@/components/ui/dropdown-types";
|
|
7
|
+
import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover";
|
|
8
|
+
import { ScrollArea } from "@/components/ui/scroll-area";
|
|
9
|
+
import { ui } from "@/config/theme";
|
|
10
|
+
import { cn } from "@/lib/utils";
|
|
11
|
+
|
|
12
|
+
function filterOptions(options: DropdownOption[], search: string) {
|
|
13
|
+
const query = search.trim().toLowerCase();
|
|
14
|
+
if (!query) {
|
|
15
|
+
return options;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
return options.filter(
|
|
19
|
+
(option) =>
|
|
20
|
+
option.label.toLowerCase().includes(query) || option.description?.toLowerCase().includes(query),
|
|
21
|
+
);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export type MultiSelectDropdownProps = {
|
|
25
|
+
options: DropdownOption[];
|
|
26
|
+
value?: string[];
|
|
27
|
+
onValueChange?: (value: string[]) => void;
|
|
28
|
+
placeholder?: string;
|
|
29
|
+
searchPlaceholder?: string;
|
|
30
|
+
emptyText?: string;
|
|
31
|
+
maxDisplay?: number;
|
|
32
|
+
disabled?: boolean;
|
|
33
|
+
className?: string;
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
export function MultiSelectDropdown({
|
|
37
|
+
options,
|
|
38
|
+
value = [],
|
|
39
|
+
onValueChange,
|
|
40
|
+
placeholder = "Select options...",
|
|
41
|
+
searchPlaceholder = "Search...",
|
|
42
|
+
emptyText = "No results found.",
|
|
43
|
+
maxDisplay = 2,
|
|
44
|
+
disabled = false,
|
|
45
|
+
className,
|
|
46
|
+
}: MultiSelectDropdownProps) {
|
|
47
|
+
const [open, setOpen] = React.useState(false);
|
|
48
|
+
const [search, setSearch] = React.useState("");
|
|
49
|
+
|
|
50
|
+
const filteredOptions = filterOptions(options, search);
|
|
51
|
+
const selectedOptions = options.filter((option) => value.includes(option.value));
|
|
52
|
+
const handleOpenChange = (nextOpen: boolean) => {
|
|
53
|
+
setOpen(nextOpen);
|
|
54
|
+
if (!nextOpen) {
|
|
55
|
+
setSearch("");
|
|
56
|
+
}
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
const toggleValue = (optionValue: string) => {
|
|
60
|
+
if (value.includes(optionValue)) {
|
|
61
|
+
onValueChange?.(value.filter((current) => current !== optionValue));
|
|
62
|
+
return;
|
|
63
|
+
}
|
|
64
|
+
onValueChange?.([...value, optionValue]);
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
const clearAll = () => onValueChange?.([]);
|
|
68
|
+
|
|
69
|
+
const selectAllFiltered = () => {
|
|
70
|
+
const next = new Set(value);
|
|
71
|
+
filteredOptions.forEach((option) => {
|
|
72
|
+
if (!option.disabled) {
|
|
73
|
+
next.add(option.value);
|
|
74
|
+
}
|
|
75
|
+
});
|
|
76
|
+
onValueChange?.(Array.from(next));
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
const removeValue = (optionValue: string, event: React.MouseEvent) => {
|
|
80
|
+
event.stopPropagation();
|
|
81
|
+
onValueChange?.(value.filter((current) => current !== optionValue));
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
return (
|
|
85
|
+
<Popover open={open} onOpenChange={handleOpenChange}>
|
|
86
|
+
<PopoverTrigger asChild>
|
|
87
|
+
<Button
|
|
88
|
+
variant="outline"
|
|
89
|
+
role="combobox"
|
|
90
|
+
aria-expanded={open}
|
|
91
|
+
disabled={disabled}
|
|
92
|
+
className={cn(dropdownClasses.triggerMulti, className)}
|
|
93
|
+
>
|
|
94
|
+
<div className="flex min-w-0 flex-1 flex-wrap items-center gap-1">
|
|
95
|
+
{selectedOptions.length === 0 ? (
|
|
96
|
+
<span className={dropdownClasses.emptyText}>{placeholder}</span>
|
|
97
|
+
) : (
|
|
98
|
+
<>
|
|
99
|
+
{selectedOptions.slice(0, maxDisplay).map((option) => (
|
|
100
|
+
<Badge key={option.value} variant="secondary" className="gap-1 pr-1">
|
|
101
|
+
{option.label}
|
|
102
|
+
<span
|
|
103
|
+
role="button"
|
|
104
|
+
tabIndex={0}
|
|
105
|
+
className={cn("rounded-full hover:bg-muted", ui("focusRing"))}
|
|
106
|
+
aria-label={`Remove ${option.label}`}
|
|
107
|
+
onMouseDown={(event) => {
|
|
108
|
+
event.preventDefault();
|
|
109
|
+
event.stopPropagation();
|
|
110
|
+
}}
|
|
111
|
+
onClick={(event) => removeValue(option.value, event)}
|
|
112
|
+
onKeyDown={(event) => {
|
|
113
|
+
if (event.key === "Enter" || event.key === " ") {
|
|
114
|
+
event.preventDefault();
|
|
115
|
+
removeValue(option.value, event as unknown as React.MouseEvent);
|
|
116
|
+
}
|
|
117
|
+
}}
|
|
118
|
+
>
|
|
119
|
+
<X className="h-3 w-3" />
|
|
120
|
+
</span>
|
|
121
|
+
</Badge>
|
|
122
|
+
))}
|
|
123
|
+
{selectedOptions.length > maxDisplay && (
|
|
124
|
+
<Badge variant="secondary">+{selectedOptions.length - maxDisplay} more</Badge>
|
|
125
|
+
)}
|
|
126
|
+
</>
|
|
127
|
+
)}
|
|
128
|
+
</div>
|
|
129
|
+
<ChevronsUpDown className={dropdownClasses.chevron} />
|
|
130
|
+
</Button>
|
|
131
|
+
</PopoverTrigger>
|
|
132
|
+
<PopoverContent align="start" className={dropdownClasses.popoverContent}>
|
|
133
|
+
<div className={dropdownClasses.searchBar}>
|
|
134
|
+
<Search className={dropdownClasses.searchIcon} />
|
|
135
|
+
<input
|
|
136
|
+
value={search}
|
|
137
|
+
onChange={(event) => setSearch(event.target.value)}
|
|
138
|
+
placeholder={searchPlaceholder}
|
|
139
|
+
className={dropdownClasses.searchInput}
|
|
140
|
+
/>
|
|
141
|
+
</div>
|
|
142
|
+
<ScrollArea className="max-h-60">
|
|
143
|
+
<div className="p-1">
|
|
144
|
+
{filteredOptions.length === 0 ? (
|
|
145
|
+
<p className={cn(dropdownClasses.empty, dropdownClasses.emptyText)}>{emptyText}</p>
|
|
146
|
+
) : (
|
|
147
|
+
filteredOptions.map((option) => {
|
|
148
|
+
const isSelected = value.includes(option.value);
|
|
149
|
+
|
|
150
|
+
return (
|
|
151
|
+
<button
|
|
152
|
+
key={option.value}
|
|
153
|
+
type="button"
|
|
154
|
+
disabled={option.disabled}
|
|
155
|
+
onClick={() => toggleValue(option.value)}
|
|
156
|
+
className={cn(
|
|
157
|
+
dropdownClasses.optionItemMulti,
|
|
158
|
+
isSelected && dropdownClasses.optionItemMultiSelected,
|
|
159
|
+
)}
|
|
160
|
+
>
|
|
161
|
+
<Checkbox checked={isSelected} tabIndex={-1} aria-hidden className="pointer-events-none" />
|
|
162
|
+
<div className="flex flex-1 flex-col items-start text-left">
|
|
163
|
+
<span>{option.label}</span>
|
|
164
|
+
{option.description ? (
|
|
165
|
+
<span className={cn("text-xs", dropdownClasses.optionDescription)}>{option.description}</span>
|
|
166
|
+
) : null}
|
|
167
|
+
</div>
|
|
168
|
+
{isSelected ? <Check className="h-4 w-4 shrink-0 opacity-70" /> : null}
|
|
169
|
+
</button>
|
|
170
|
+
);
|
|
171
|
+
})
|
|
172
|
+
)}
|
|
173
|
+
</div>
|
|
174
|
+
</ScrollArea>
|
|
175
|
+
<div className={dropdownClasses.footer}>
|
|
176
|
+
<Button
|
|
177
|
+
type="button"
|
|
178
|
+
variant="outline"
|
|
179
|
+
size="sm"
|
|
180
|
+
className="h-8"
|
|
181
|
+
onClick={selectAllFiltered}
|
|
182
|
+
disabled={filteredOptions.length === 0}
|
|
183
|
+
>
|
|
184
|
+
Select visible
|
|
185
|
+
</Button>
|
|
186
|
+
<Button
|
|
187
|
+
type="button"
|
|
188
|
+
variant="outline"
|
|
189
|
+
size="sm"
|
|
190
|
+
className="h-8"
|
|
191
|
+
onClick={clearAll}
|
|
192
|
+
disabled={value.length === 0}
|
|
193
|
+
>
|
|
194
|
+
Clear all
|
|
195
|
+
</Button>
|
|
196
|
+
</div>
|
|
197
|
+
</PopoverContent>
|
|
198
|
+
</Popover>
|
|
199
|
+
);
|
|
200
|
+
}
|