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.
Files changed (128) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +117 -0
  3. package/boilerplate/README.md +18 -0
  4. package/boilerplate/react-base/.env.example +1 -0
  5. package/boilerplate/react-base/README.md +3 -0
  6. package/boilerplate/react-base/components.json +19 -0
  7. package/boilerplate/react-base/eslint.config.js +32 -0
  8. package/boilerplate/react-base/index.html +12 -0
  9. package/boilerplate/react-base/package.json +71 -0
  10. package/boilerplate/react-base/postcss.config.js +6 -0
  11. package/boilerplate/react-base/prettier.config.js +6 -0
  12. package/boilerplate/react-base/src/api/axios.ts +20 -0
  13. package/boilerplate/react-base/src/app/store.ts +13 -0
  14. package/boilerplate/react-base/src/components/data-table.tsx +919 -0
  15. package/boilerplate/react-base/src/components/ui/accordion.tsx +44 -0
  16. package/boilerplate/react-base/src/components/ui/alert-dialog.tsx +105 -0
  17. package/boilerplate/react-base/src/components/ui/alert.tsx +40 -0
  18. package/boilerplate/react-base/src/components/ui/avatar.tsx +30 -0
  19. package/boilerplate/react-base/src/components/ui/badge.tsx +27 -0
  20. package/boilerplate/react-base/src/components/ui/bar-chart.tsx +76 -0
  21. package/boilerplate/react-base/src/components/ui/breadcrumb.tsx +87 -0
  22. package/boilerplate/react-base/src/components/ui/button.tsx +34 -0
  23. package/boilerplate/react-base/src/components/ui/calendar.tsx +63 -0
  24. package/boilerplate/react-base/src/components/ui/card.tsx +36 -0
  25. package/boilerplate/react-base/src/components/ui/chart.tsx +280 -0
  26. package/boilerplate/react-base/src/components/ui/checkbox.tsx +51 -0
  27. package/boilerplate/react-base/src/components/ui/context-menu.tsx +173 -0
  28. package/boilerplate/react-base/src/components/ui/date-picker.tsx +42 -0
  29. package/boilerplate/react-base/src/components/ui/dialog.tsx +87 -0
  30. package/boilerplate/react-base/src/components/ui/drawer.tsx +81 -0
  31. package/boilerplate/react-base/src/components/ui/dropdown-menu.tsx +81 -0
  32. package/boilerplate/react-base/src/components/ui/dropdown-types.ts +28 -0
  33. package/boilerplate/react-base/src/components/ui/field.tsx +194 -0
  34. package/boilerplate/react-base/src/components/ui/hover-card.tsx +26 -0
  35. package/boilerplate/react-base/src/components/ui/input-group.tsx +98 -0
  36. package/boilerplate/react-base/src/components/ui/input-otp.tsx +63 -0
  37. package/boilerplate/react-base/src/components/ui/input.tsx +12 -0
  38. package/boilerplate/react-base/src/components/ui/item.tsx +152 -0
  39. package/boilerplate/react-base/src/components/ui/kbd.tsx +13 -0
  40. package/boilerplate/react-base/src/components/ui/label.tsx +14 -0
  41. package/boilerplate/react-base/src/components/ui/line-chart.tsx +65 -0
  42. package/boilerplate/react-base/src/components/ui/menubar.tsx +217 -0
  43. package/boilerplate/react-base/src/components/ui/multi-select-dropdown.tsx +200 -0
  44. package/boilerplate/react-base/src/components/ui/navigation-menu.tsx +120 -0
  45. package/boilerplate/react-base/src/components/ui/pie-chart.tsx +87 -0
  46. package/boilerplate/react-base/src/components/ui/popover.tsx +29 -0
  47. package/boilerplate/react-base/src/components/ui/progress.tsx +19 -0
  48. package/boilerplate/react-base/src/components/ui/radio-group.tsx +36 -0
  49. package/boilerplate/react-base/src/components/ui/scroll-area.tsx +38 -0
  50. package/boilerplate/react-base/src/components/ui/searchable-dropdown.tsx +118 -0
  51. package/boilerplate/react-base/src/components/ui/select.tsx +140 -0
  52. package/boilerplate/react-base/src/components/ui/separator.tsx +20 -0
  53. package/boilerplate/react-base/src/components/ui/sheet.tsx +70 -0
  54. package/boilerplate/react-base/src/components/ui/sidebar.tsx +470 -0
  55. package/boilerplate/react-base/src/components/ui/skeleton.tsx +11 -0
  56. package/boilerplate/react-base/src/components/ui/slider.tsx +23 -0
  57. package/boilerplate/react-base/src/components/ui/sonner.tsx +21 -0
  58. package/boilerplate/react-base/src/components/ui/sparkline.tsx +38 -0
  59. package/boilerplate/react-base/src/components/ui/spinner.tsx +10 -0
  60. package/boilerplate/react-base/src/components/ui/switch.tsx +16 -0
  61. package/boilerplate/react-base/src/components/ui/table.tsx +80 -0
  62. package/boilerplate/react-base/src/components/ui/tabs.tsx +32 -0
  63. package/boilerplate/react-base/src/components/ui/textarea.tsx +12 -0
  64. package/boilerplate/react-base/src/components/ui/toggle-group.tsx +49 -0
  65. package/boilerplate/react-base/src/components/ui/toggle.tsx +33 -0
  66. package/boilerplate/react-base/src/components/ui/tooltip.tsx +23 -0
  67. package/boilerplate/react-base/src/components/ui/typography.tsx +76 -0
  68. package/boilerplate/react-base/src/config/constants.ts +3 -0
  69. package/boilerplate/react-base/src/config/theme.ts +432 -0
  70. package/boilerplate/react-base/src/config/user.ts +52 -0
  71. package/boilerplate/react-base/src/context/theme-provider.tsx +12 -0
  72. package/boilerplate/react-base/src/features/auth/authSlice.ts +19 -0
  73. package/boilerplate/react-base/src/hooks/index.ts +1 -0
  74. package/boilerplate/react-base/src/hooks/use-mobile.ts +17 -0
  75. package/boilerplate/react-base/src/lib/utils.ts +6 -0
  76. package/boilerplate/react-base/src/routes/index.tsx +7 -0
  77. package/boilerplate/react-base/src/styles/globals.css +15 -0
  78. package/boilerplate/react-base/src/vite-env.d.ts +31 -0
  79. package/boilerplate/react-base/tailwind.config.ts +75 -0
  80. package/boilerplate/react-base/tsconfig.app.json +20 -0
  81. package/boilerplate/react-base/tsconfig.json +7 -0
  82. package/boilerplate/react-base/tsconfig.node.json +16 -0
  83. package/boilerplate/react-base/vite.config.ts +12 -0
  84. package/dist/bin/index.js +8 -0
  85. package/dist/src/cli-args.js +52 -0
  86. package/dist/src/generator.js +85 -0
  87. package/dist/src/installer.js +7 -0
  88. package/dist/src/paths.js +61 -0
  89. package/dist/src/prompts.js +79 -0
  90. package/dist/src/replace-placeholders.js +22 -0
  91. package/dist/src/utils.js +16 -0
  92. package/package.json +63 -0
  93. package/templates/admin-portal/README.md +26 -0
  94. package/templates/admin-portal/src/App.tsx +85 -0
  95. package/templates/admin-portal/src/assets/auth-hero.jpg +0 -0
  96. package/templates/admin-portal/src/assets/brand-logo.png +0 -0
  97. package/templates/admin-portal/src/components/app-breadcrumb.tsx +41 -0
  98. package/templates/admin-portal/src/components/app-header.tsx +20 -0
  99. package/templates/admin-portal/src/components/app-sidebar.tsx +78 -0
  100. package/templates/admin-portal/src/components/auth-layout.tsx +66 -0
  101. package/templates/admin-portal/src/components/dashboard-metric-card.tsx +105 -0
  102. package/templates/admin-portal/src/components/data-table.tsx +919 -0
  103. package/templates/admin-portal/src/components/layout-shell.tsx +23 -0
  104. package/templates/admin-portal/src/components/notifications-sheet.tsx +91 -0
  105. package/templates/admin-portal/src/components/sidebar-nav.tsx +164 -0
  106. package/templates/admin-portal/src/components/user-avatar.tsx +26 -0
  107. package/templates/admin-portal/src/components/user-menu.tsx +163 -0
  108. package/templates/admin-portal/src/config/branding.ts +17 -0
  109. package/templates/admin-portal/src/config/chart-data.ts +44 -0
  110. package/templates/admin-portal/src/config/navigation.ts +42 -0
  111. package/templates/admin-portal/src/context/auth-context.tsx +32 -0
  112. package/templates/admin-portal/src/lib/breadcrumbs.ts +58 -0
  113. package/templates/admin-portal/src/main.tsx +18 -0
  114. package/templates/admin-portal/src/pages/components/demo-columns.tsx +170 -0
  115. package/templates/admin-portal/src/pages/components.tsx +1368 -0
  116. package/templates/admin-portal/src/pages/dashboard.tsx +143 -0
  117. package/templates/admin-portal/src/pages/login.tsx +81 -0
  118. package/templates/admin-portal/src/pages/settings/notifications.tsx +31 -0
  119. package/templates/admin-portal/src/pages/settings/profile.tsx +26 -0
  120. package/templates/admin-portal/src/pages/signup.tsx +81 -0
  121. package/templates/admin-portal/src/pages/users.tsx +12 -0
  122. package/templates/admin-portal/tsconfig.json +10 -0
  123. package/templates/blank/README.md +15 -0
  124. package/templates/blank/src/App.tsx +5 -0
  125. package/templates/blank/src/main.tsx +15 -0
  126. package/templates/blank/src/pages/home.tsx +20 -0
  127. package/templates/blank/tsconfig.json +10 -0
  128. 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
+ }