@withmata/blueprints 0.3.5 → 0.5.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 (62) hide show
  1. package/.claude/skills/audit/SKILL.md +4 -4
  2. package/.claude/skills/blueprint-catalog/SKILL.md +17 -7
  3. package/.claude/skills/copywrite/SKILL.md +187 -0
  4. package/.claude/skills/copywrite-landing/SKILL.md +489 -0
  5. package/.claude/skills/design-system/SKILL.md +970 -0
  6. package/.claude/skills/new-project/SKILL.md +168 -112
  7. package/.claude/skills/scaffold-auth/SKILL.md +9 -9
  8. package/.claude/skills/scaffold-db/SKILL.md +14 -14
  9. package/.claude/skills/scaffold-env/SKILL.md +4 -4
  10. package/.claude/skills/scaffold-foundation/SKILL.md +15 -15
  11. package/.claude/skills/scaffold-tailwind/SKILL.md +17 -3
  12. package/.claude/skills/scaffold-ui/SKILL.md +155 -36
  13. package/ENGINEERING.md +2 -2
  14. package/blueprints/discovery/design-system/BLUEPRINT.md +1479 -0
  15. package/blueprints/discovery/marketing-copywriting/BLUEPRINT.md +664 -0
  16. package/blueprints/features/auth-better-auth/BLUEPRINT.md +20 -22
  17. package/blueprints/features/db-drizzle-postgres/BLUEPRINT.md +12 -12
  18. package/blueprints/features/db-drizzle-postgres/files/db/src/example-entity.ts +1 -1
  19. package/blueprints/features/db-drizzle-postgres/files/db/src/scripts/seed.ts +1 -1
  20. package/blueprints/features/env-t3/BLUEPRINT.md +1 -1
  21. package/blueprints/features/tailwind-v4/BLUEPRINT.md +9 -2
  22. package/blueprints/features/tailwind-v4/files/tailwind-config/shared-styles.css +80 -1
  23. package/blueprints/features/ui-shared-components/BLUEPRINT.md +411 -78
  24. package/blueprints/features/ui-shared-components/files/ui/components/ui/alert-dialog.tsx +192 -0
  25. package/blueprints/features/ui-shared-components/files/ui/components/ui/avatar.tsx +71 -0
  26. package/blueprints/features/ui-shared-components/files/ui/components/ui/badge.tsx +52 -0
  27. package/blueprints/features/ui-shared-components/files/ui/components/ui/breadcrumb.tsx +122 -0
  28. package/blueprints/features/ui-shared-components/files/ui/components/ui/button.tsx +56 -0
  29. package/blueprints/features/ui-shared-components/files/ui/components/ui/card-select.tsx +72 -0
  30. package/blueprints/features/ui-shared-components/files/ui/components/ui/card.tsx +100 -0
  31. package/blueprints/features/ui-shared-components/files/ui/components/ui/collapsible.tsx +34 -0
  32. package/blueprints/features/ui-shared-components/files/ui/components/ui/combobox.tsx +301 -0
  33. package/blueprints/features/ui-shared-components/files/ui/components/ui/dropdown-menu.tsx +264 -0
  34. package/blueprints/features/ui-shared-components/files/ui/components/ui/empty-state.tsx +43 -0
  35. package/blueprints/features/ui-shared-components/files/ui/components/ui/entity-select.tsx +110 -0
  36. package/blueprints/features/ui-shared-components/files/ui/components/ui/field.tsx +237 -0
  37. package/blueprints/features/ui-shared-components/files/ui/components/ui/form-field.tsx +217 -0
  38. package/blueprints/features/ui-shared-components/files/ui/components/ui/input-group.tsx +161 -0
  39. package/blueprints/features/ui-shared-components/files/ui/components/ui/input.tsx +20 -0
  40. package/blueprints/features/ui-shared-components/files/ui/components/ui/label.tsx +20 -0
  41. package/blueprints/features/ui-shared-components/files/ui/components/ui/org-switcher.tsx +114 -0
  42. package/blueprints/features/ui-shared-components/files/ui/components/ui/page-header.tsx +45 -0
  43. package/blueprints/features/ui-shared-components/files/ui/components/ui/pagination.tsx +52 -0
  44. package/blueprints/features/ui-shared-components/files/ui/components/ui/pill-select.tsx +151 -0
  45. package/blueprints/features/ui-shared-components/files/ui/components/ui/popover.tsx +41 -0
  46. package/blueprints/features/ui-shared-components/files/ui/components/ui/search-input.tsx +49 -0
  47. package/blueprints/features/ui-shared-components/files/ui/components/ui/select.tsx +205 -0
  48. package/blueprints/features/ui-shared-components/files/ui/components/ui/selected-entity-card.tsx +47 -0
  49. package/blueprints/features/ui-shared-components/files/ui/components/ui/separator.tsx +25 -0
  50. package/blueprints/features/ui-shared-components/files/ui/components/ui/sidebar.tsx +389 -0
  51. package/blueprints/features/ui-shared-components/files/ui/components/ui/status-filter.tsx +43 -0
  52. package/blueprints/features/ui-shared-components/files/ui/components/ui/tag-input.tsx +131 -0
  53. package/blueprints/features/ui-shared-components/files/ui/components/ui/textarea.tsx +18 -0
  54. package/blueprints/features/ui-shared-components/files/ui/components/ui/user-menu.tsx +149 -0
  55. package/blueprints/features/ui-shared-components/files/ui/components.json +11 -8
  56. package/blueprints/features/ui-shared-components/files/ui/package.json +20 -11
  57. package/blueprints/foundation/monorepo-turbo/BLUEPRINT.md +19 -20
  58. package/blueprints/foundation/monorepo-turbo/files/root/package.json +1 -1
  59. package/dist/index.js +27 -10
  60. package/package.json +1 -1
  61. package/blueprints/features/tailwind-v4/files/tailwind-config/package.json +0 -20
  62. package/blueprints/foundation/monorepo-turbo/files/root/pnpm-workspace.yaml +0 -5
@@ -0,0 +1,205 @@
1
+ "use client";
2
+
3
+ import { Select as SelectPrimitive } from "@base-ui/react/select";
4
+ import { CaretDownIcon } from "@phosphor-icons/react/CaretDown";
5
+ import { CaretUpIcon } from "@phosphor-icons/react/CaretUp";
6
+ import { CheckIcon } from "@phosphor-icons/react/Check";
7
+ import type * as React from "react";
8
+ import { cn } from "#utils/cn";
9
+
10
+ const Select = SelectPrimitive.Root;
11
+
12
+ function SelectGroup({ className, ...props }: SelectPrimitive.Group.Props) {
13
+ return (
14
+ <SelectPrimitive.Group
15
+ data-slot="select-group"
16
+ className={cn("scroll-my-1 p-1", className)}
17
+ {...props}
18
+ />
19
+ );
20
+ }
21
+
22
+ function SelectValue({ className, ...props }: SelectPrimitive.Value.Props) {
23
+ return (
24
+ <SelectPrimitive.Value
25
+ data-slot="select-value"
26
+ className={cn("flex flex-1 text-left", className)}
27
+ {...props}
28
+ />
29
+ );
30
+ }
31
+
32
+ function SelectTrigger({
33
+ className,
34
+ size = "default",
35
+ children,
36
+ ...props
37
+ }: SelectPrimitive.Trigger.Props & {
38
+ size?: "sm" | "default";
39
+ }) {
40
+ return (
41
+ <SelectPrimitive.Trigger
42
+ data-slot="select-trigger"
43
+ data-size={size}
44
+ className={cn(
45
+ "border-input data-[placeholder]:text-muted-foreground bg-input/30 dark:hover:bg-input/50 focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive dark:aria-invalid:border-destructive/50 gap-1.5 rounded-4xl border px-3 py-2 text-sm transition-colors focus-visible:ring-[3px] aria-invalid:ring-[3px] data-[size=default]:h-9 data-[size=sm]:h-8 *:data-[slot=select-value]:flex *:data-[slot=select-value]:gap-1.5 [&_svg:not([class*='size-'])]:size-4 flex w-fit items-center justify-between whitespace-nowrap outline-none disabled:cursor-not-allowed disabled:opacity-50 *:data-[slot=select-value]:line-clamp-1 *:data-[slot=select-value]:flex *:data-[slot=select-value]:items-center [&_svg]:pointer-events-none [&_svg]:shrink-0",
46
+ className,
47
+ )}
48
+ {...props}
49
+ >
50
+ {children}
51
+ <SelectPrimitive.Icon
52
+ render={
53
+ <CaretDownIcon className="text-muted-foreground size-4 pointer-events-none" />
54
+ }
55
+ />
56
+ </SelectPrimitive.Trigger>
57
+ );
58
+ }
59
+
60
+ function SelectContent({
61
+ className,
62
+ children,
63
+ side = "bottom",
64
+ sideOffset = 4,
65
+ align = "center",
66
+ alignOffset = 0,
67
+ alignItemWithTrigger = true,
68
+ ...props
69
+ }: SelectPrimitive.Popup.Props &
70
+ Pick<
71
+ SelectPrimitive.Positioner.Props,
72
+ "align" | "alignOffset" | "side" | "sideOffset" | "alignItemWithTrigger"
73
+ >) {
74
+ return (
75
+ <SelectPrimitive.Portal>
76
+ <SelectPrimitive.Positioner
77
+ side={side}
78
+ sideOffset={sideOffset}
79
+ align={align}
80
+ alignOffset={alignOffset}
81
+ alignItemWithTrigger={alignItemWithTrigger}
82
+ className="isolate z-50"
83
+ >
84
+ <SelectPrimitive.Popup
85
+ data-slot="select-content"
86
+ className={cn(
87
+ "bg-popover text-popover-foreground data-open:animate-in data-closed:animate-out data-closed:fade-out-0 data-open:fade-in-0 data-closed:zoom-out-95 data-open:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 ring-foreground/5 min-w-36 rounded-2xl shadow-2xl ring-1 duration-100 relative isolate z-50 max-h-(--available-height) w-(--anchor-width) origin-(--transform-origin) overflow-x-hidden overflow-y-auto",
88
+ className,
89
+ )}
90
+ {...props}
91
+ >
92
+ <SelectScrollUpButton />
93
+ <SelectPrimitive.List>{children}</SelectPrimitive.List>
94
+ <SelectScrollDownButton />
95
+ </SelectPrimitive.Popup>
96
+ </SelectPrimitive.Positioner>
97
+ </SelectPrimitive.Portal>
98
+ );
99
+ }
100
+
101
+ function SelectLabel({
102
+ className,
103
+ ...props
104
+ }: SelectPrimitive.GroupLabel.Props) {
105
+ return (
106
+ <SelectPrimitive.GroupLabel
107
+ data-slot="select-label"
108
+ className={cn("text-muted-foreground px-3 py-2.5 text-xs", className)}
109
+ {...props}
110
+ />
111
+ );
112
+ }
113
+
114
+ function SelectItem({
115
+ className,
116
+ children,
117
+ ...props
118
+ }: SelectPrimitive.Item.Props) {
119
+ return (
120
+ <SelectPrimitive.Item
121
+ data-slot="select-item"
122
+ className={cn(
123
+ "focus:bg-accent focus:text-accent-foreground not-data-[variant=destructive]:focus:**:text-accent-foreground gap-2.5 rounded-xl py-2 pr-8 pl-3 text-sm [&_svg:not([class*='size-'])]:size-4 *:[span]:last:flex *:[span]:last:items-center *:[span]:last:gap-2 relative flex w-full cursor-default items-center outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0",
124
+ className,
125
+ )}
126
+ {...props}
127
+ >
128
+ <SelectPrimitive.ItemText className="flex flex-1 gap-2 shrink-0 whitespace-nowrap">
129
+ {children}
130
+ </SelectPrimitive.ItemText>
131
+ <SelectPrimitive.ItemIndicator
132
+ render={
133
+ <span className="pointer-events-none absolute right-2 flex size-4 items-center justify-center" />
134
+ }
135
+ >
136
+ <CheckIcon className="pointer-events-none" />
137
+ </SelectPrimitive.ItemIndicator>
138
+ </SelectPrimitive.Item>
139
+ );
140
+ }
141
+
142
+ function SelectSeparator({
143
+ className,
144
+ ...props
145
+ }: SelectPrimitive.Separator.Props) {
146
+ return (
147
+ <SelectPrimitive.Separator
148
+ data-slot="select-separator"
149
+ className={cn(
150
+ "bg-border/50 -mx-1 my-1 h-px pointer-events-none",
151
+ className,
152
+ )}
153
+ {...props}
154
+ />
155
+ );
156
+ }
157
+
158
+ function SelectScrollUpButton({
159
+ className,
160
+ ...props
161
+ }: React.ComponentProps<typeof SelectPrimitive.ScrollUpArrow>) {
162
+ return (
163
+ <SelectPrimitive.ScrollUpArrow
164
+ data-slot="select-scroll-up-button"
165
+ className={cn(
166
+ "bg-popover z-10 flex cursor-default items-center justify-center py-1 [&_svg:not([class*='size-'])]:size-4 top-0 w-full",
167
+ className,
168
+ )}
169
+ {...props}
170
+ >
171
+ <CaretUpIcon />
172
+ </SelectPrimitive.ScrollUpArrow>
173
+ );
174
+ }
175
+
176
+ function SelectScrollDownButton({
177
+ className,
178
+ ...props
179
+ }: React.ComponentProps<typeof SelectPrimitive.ScrollDownArrow>) {
180
+ return (
181
+ <SelectPrimitive.ScrollDownArrow
182
+ data-slot="select-scroll-down-button"
183
+ className={cn(
184
+ "bg-popover z-10 flex cursor-default items-center justify-center py-1 [&_svg:not([class*='size-'])]:size-4 bottom-0 w-full",
185
+ className,
186
+ )}
187
+ {...props}
188
+ >
189
+ <CaretDownIcon />
190
+ </SelectPrimitive.ScrollDownArrow>
191
+ );
192
+ }
193
+
194
+ export {
195
+ Select,
196
+ SelectContent,
197
+ SelectGroup,
198
+ SelectItem,
199
+ SelectLabel,
200
+ SelectScrollDownButton,
201
+ SelectScrollUpButton,
202
+ SelectSeparator,
203
+ SelectTrigger,
204
+ SelectValue,
205
+ };
@@ -0,0 +1,47 @@
1
+ "use client";
2
+
3
+ import { Button } from "#components/ui/button";
4
+ import { PencilIcon } from "@phosphor-icons/react/Pencil";
5
+ import { XIcon } from "@phosphor-icons/react/X";
6
+ import type { ReactNode } from "react";
7
+
8
+ export interface SelectedEntityCardProps {
9
+ name: string;
10
+ onRemove: () => void;
11
+ children?: ReactNode;
12
+ }
13
+
14
+ export function EditButton({ onEdit }: { onEdit: () => void }) {
15
+ return (
16
+ <Button
17
+ type="button"
18
+ variant="ghost"
19
+ size="xs"
20
+ onClick={onEdit}
21
+ className="ml-auto h-auto p-0 text-[10px] text-muted-foreground hover:text-foreground hover:bg-transparent"
22
+ >
23
+ Edit
24
+ <PencilIcon className="size-3.5" />
25
+ </Button>
26
+ );
27
+ }
28
+
29
+ export function SelectedEntityCard({
30
+ name,
31
+ onRemove,
32
+ children,
33
+ }: SelectedEntityCardProps) {
34
+ return (
35
+ <div data-slot="selected-entity-card" className="rounded-xl border p-3 relative">
36
+ <button
37
+ type="button"
38
+ onClick={onRemove}
39
+ className="absolute top-2.5 right-2.5 rounded-md p-0.5 text-muted-foreground hover:text-foreground transition-colors"
40
+ >
41
+ <XIcon className="size-4" />
42
+ </button>
43
+ <p className="font-medium text-sm pr-6">{name}</p>
44
+ {children}
45
+ </div>
46
+ );
47
+ }
@@ -0,0 +1,25 @@
1
+ "use client";
2
+
3
+ import { Separator as SeparatorPrimitive } from "@base-ui/react/separator";
4
+
5
+ import { cn } from "#utils/cn";
6
+
7
+ function Separator({
8
+ className,
9
+ orientation = "horizontal",
10
+ ...props
11
+ }: SeparatorPrimitive.Props) {
12
+ return (
13
+ <SeparatorPrimitive
14
+ data-slot="separator"
15
+ orientation={orientation}
16
+ className={cn(
17
+ "bg-border shrink-0 data-[orientation=horizontal]:h-px data-[orientation=horizontal]:w-full data-[orientation=vertical]:w-px data-[orientation=vertical]:self-stretch",
18
+ className,
19
+ )}
20
+ {...props}
21
+ />
22
+ );
23
+ }
24
+
25
+ export { Separator };
@@ -0,0 +1,389 @@
1
+ "use client";
2
+
3
+ import { SidebarSimpleIcon } from "@phosphor-icons/react/dist/icons/SidebarSimple";
4
+ import { Slot } from "@radix-ui/react-slot";
5
+ import { cva, type VariantProps } from "class-variance-authority";
6
+ import * as React from "react";
7
+ import { cn } from "#utils/cn";
8
+
9
+ const SIDEBAR_COOKIE_NAME = "sidebar:state";
10
+ const SIDEBAR_COOKIE_MAX_AGE = 60 * 60 * 24 * 7; // 7 days
11
+ const SIDEBAR_WIDTH = "16rem";
12
+ const SIDEBAR_WIDTH_ICON = "3rem";
13
+
14
+ type SidebarContextValue = {
15
+ state: "expanded" | "collapsed";
16
+ open: boolean;
17
+ setOpen: (open: boolean) => void;
18
+ toggleSidebar: () => void;
19
+ };
20
+
21
+ const SidebarContext = React.createContext<SidebarContextValue | null>(null);
22
+
23
+ function useSidebar() {
24
+ const context = React.useContext(SidebarContext);
25
+ if (!context) {
26
+ throw new Error("useSidebar must be used within a SidebarProvider");
27
+ }
28
+ return context;
29
+ }
30
+
31
+ function SidebarProvider({
32
+ defaultOpen = true,
33
+ open: openProp,
34
+ onOpenChange: setOpenProp,
35
+ children,
36
+ className,
37
+ style,
38
+ ...props
39
+ }: React.ComponentProps<"div"> & {
40
+ defaultOpen?: boolean;
41
+ open?: boolean;
42
+ onOpenChange?: (open: boolean) => void;
43
+ }) {
44
+ const [_open, _setOpen] = React.useState(defaultOpen);
45
+ const open = openProp ?? _open;
46
+
47
+ const setOpen = React.useCallback(
48
+ (value: boolean | ((value: boolean) => boolean)) => {
49
+ const openState = typeof value === "function" ? value(open) : value;
50
+
51
+ if (setOpenProp) {
52
+ setOpenProp(openState);
53
+ } else {
54
+ _setOpen(openState);
55
+ }
56
+
57
+ // Set cookie for persistence
58
+ document.cookie = `${SIDEBAR_COOKIE_NAME}=${openState}; path=/; max-age=${SIDEBAR_COOKIE_MAX_AGE}`;
59
+ },
60
+ [setOpenProp, open],
61
+ );
62
+
63
+ const toggleSidebar = React.useCallback(() => {
64
+ setOpen((prev) => !prev);
65
+ }, [setOpen]);
66
+
67
+ const state = open ? "expanded" : "collapsed";
68
+
69
+ const contextValue = React.useMemo<SidebarContextValue>(
70
+ () => ({
71
+ state,
72
+ open,
73
+ setOpen,
74
+ toggleSidebar,
75
+ }),
76
+ [state, open, setOpen, toggleSidebar],
77
+ );
78
+
79
+ return (
80
+ <SidebarContext.Provider value={contextValue}>
81
+ <div
82
+ data-slot="sidebar-wrapper"
83
+ style={
84
+ {
85
+ "--sidebar-width": SIDEBAR_WIDTH,
86
+ "--sidebar-width-icon": SIDEBAR_WIDTH_ICON,
87
+ ...style,
88
+ } as React.CSSProperties
89
+ }
90
+ className={cn(
91
+ "group/sidebar-wrapper flex min-h-svh w-full has-data-[variant=inset]:bg-sidebar",
92
+ className,
93
+ )}
94
+ {...props}
95
+ >
96
+ {children}
97
+ </div>
98
+ </SidebarContext.Provider>
99
+ );
100
+ }
101
+
102
+ function Sidebar({
103
+ side = "left",
104
+ variant = "sidebar",
105
+ collapsible = "icon",
106
+ className,
107
+ children,
108
+ ...props
109
+ }: React.ComponentProps<"div"> & {
110
+ side?: "left" | "right";
111
+ variant?: "sidebar" | "floating" | "inset";
112
+ collapsible?: "offcanvas" | "icon" | "none";
113
+ }) {
114
+ const { state } = useSidebar();
115
+
116
+ if (collapsible === "none") {
117
+ return (
118
+ <div
119
+ data-slot="sidebar"
120
+ className={cn(
121
+ "bg-sidebar text-sidebar-foreground flex h-full w-(--sidebar-width) flex-col",
122
+ className,
123
+ )}
124
+ {...props}
125
+ >
126
+ {children}
127
+ </div>
128
+ );
129
+ }
130
+
131
+ return (
132
+ <div
133
+ data-slot="sidebar"
134
+ data-state={state}
135
+ data-collapsible={state === "collapsed" ? collapsible : ""}
136
+ data-variant={variant}
137
+ data-side={side}
138
+ className="group peer hidden md:block text-sidebar-foreground"
139
+ >
140
+ {/* This handles the sidebar gap on desktop */}
141
+ <div
142
+ className={cn(
143
+ "relative h-[calc(100svh-var(--header-height,0px))] w-(--sidebar-width) bg-transparent transition-[width] duration-200 ease-linear",
144
+ "group-data-[collapsible=offcanvas]:w-0",
145
+ "group-data-[side=right]:rotate-180",
146
+ "group-data-[collapsible=icon]:w-(--sidebar-width-icon)",
147
+ )}
148
+ />
149
+ <div
150
+ className={cn(
151
+ "group/sidebar-panel fixed top-(--header-height) z-10 hidden h-[calc(100svh-var(--header-height,0px))] w-(--sidebar-width) transition-[left,right,width] duration-200 ease-linear md:flex",
152
+ side === "left"
153
+ ? "left-0 group-data-[collapsible=offcanvas]:left-[calc(var(--sidebar-width)*-1)]"
154
+ : "right-0 group-data-[collapsible=offcanvas]:right-[calc(var(--sidebar-width)*-1)]",
155
+ "group-data-[collapsible=icon]:w-(--sidebar-width-icon)",
156
+ "group-data-[side=right]:border-l group-data-[side=left]:border-r",
157
+ className,
158
+ )}
159
+ {...props}
160
+ >
161
+ <div
162
+ data-sidebar="sidebar"
163
+ className="bg-sidebar group-data-[variant=floating]:border-sidebar-border relative flex h-full w-full flex-col overflow-visible group-data-[variant=floating]:rounded-lg group-data-[variant=floating]:border group-data-[variant=floating]:shadow-sm"
164
+ >
165
+ {children}
166
+ </div>
167
+ </div>
168
+ </div>
169
+ );
170
+ }
171
+
172
+ function SidebarTrigger({
173
+ className,
174
+ onClick,
175
+ ...props
176
+ }: React.ComponentProps<"button">) {
177
+ const { toggleSidebar } = useSidebar();
178
+
179
+ return (
180
+ <button
181
+ data-slot="sidebar-trigger"
182
+ data-sidebar="trigger"
183
+ className={cn(
184
+ "inline-flex size-7 items-center justify-center rounded-md text-sm font-medium hover:bg-accent hover:text-accent-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring",
185
+ className,
186
+ )}
187
+ onClick={(event) => {
188
+ onClick?.(event);
189
+ toggleSidebar();
190
+ }}
191
+ {...props}
192
+ >
193
+ <SidebarSimpleIcon weight="duotone" className="size-4" />
194
+ <span className="sr-only">Toggle Sidebar</span>
195
+ </button>
196
+ );
197
+ }
198
+
199
+ function SidebarRail({ className, ...props }: React.ComponentProps<"button">) {
200
+ const { toggleSidebar } = useSidebar();
201
+
202
+ return (
203
+ <button
204
+ data-slot="sidebar-rail"
205
+ data-sidebar="rail"
206
+ aria-label="Toggle Sidebar"
207
+ tabIndex={-1}
208
+ onClick={toggleSidebar}
209
+ title="Toggle Sidebar"
210
+ className={cn(
211
+ "hover:after:bg-sidebar-border absolute inset-y-0 z-20 hidden w-4 -translate-x-1/2 transition-all ease-linear group-data-[side=left]:right-0 group-data-[side=right]:left-0 after:absolute after:inset-y-0 after:left-1/2 after:w-[2px] sm:flex",
212
+ "in-data-[side=left]:cursor-w-resize in-data-[side=right]:cursor-e-resize",
213
+ "[[data-side=left][data-state=collapsed]_&]:cursor-e-resize [[data-side=right][data-state=collapsed]_&]:cursor-w-resize",
214
+ "hover:group-data-[collapsible=offcanvas]:bg-sidebar group-data-[collapsible=offcanvas]:translate-x-0 group-data-[collapsible=offcanvas]:after:left-full",
215
+ "[[data-side=left][data-collapsible=offcanvas]_&]:-right-2",
216
+ "[[data-side=right][data-collapsible=offcanvas]_&]:-left-2",
217
+ className,
218
+ )}
219
+ {...props}
220
+ />
221
+ );
222
+ }
223
+
224
+ function SidebarInset({ className, ...props }: React.ComponentProps<"main">) {
225
+ return (
226
+ <main
227
+ data-slot="sidebar-inset"
228
+ className={cn(
229
+ "bg-background relative flex min-h-svh flex-1 flex-col",
230
+ className,
231
+ )}
232
+ {...props}
233
+ />
234
+ );
235
+ }
236
+
237
+ function SidebarHeader({ className, ...props }: React.ComponentProps<"div">) {
238
+ return (
239
+ <div
240
+ data-slot="sidebar-header"
241
+ data-sidebar="header"
242
+ className={cn("flex flex-col gap-2 p-2", className)}
243
+ {...props}
244
+ />
245
+ );
246
+ }
247
+
248
+ function SidebarFooter({ className, ...props }: React.ComponentProps<"div">) {
249
+ return (
250
+ <div
251
+ data-slot="sidebar-footer"
252
+ data-sidebar="footer"
253
+ className={cn("flex flex-col gap-2 p-2 mt-auto", className)}
254
+ {...props}
255
+ />
256
+ );
257
+ }
258
+
259
+ function SidebarContent({ className, ...props }: React.ComponentProps<"div">) {
260
+ return (
261
+ <div
262
+ data-slot="sidebar-content"
263
+ data-sidebar="content"
264
+ className={cn(
265
+ "flex min-h-0 flex-1 flex-col gap-2 overflow-auto group-data-[collapsible=icon]:overflow-hidden",
266
+ className,
267
+ )}
268
+ {...props}
269
+ />
270
+ );
271
+ }
272
+
273
+ function SidebarGroup({ className, ...props }: React.ComponentProps<"div">) {
274
+ return (
275
+ <div
276
+ data-slot="sidebar-group"
277
+ data-sidebar="group"
278
+ className={cn("relative flex w-full min-w-0 flex-col p-2", className)}
279
+ {...props}
280
+ />
281
+ );
282
+ }
283
+
284
+ function SidebarGroupLabel({
285
+ className,
286
+ asChild = false,
287
+ ...props
288
+ }: React.ComponentProps<"div"> & { asChild?: boolean }) {
289
+ const Comp = asChild ? Slot : "div";
290
+
291
+ return (
292
+ <Comp
293
+ data-slot="sidebar-group-label"
294
+ data-sidebar="group-label"
295
+ className={cn(
296
+ "text-sidebar-foreground/70 ring-sidebar-ring flex h-8 shrink-0 items-center rounded-md px-2 text-xs font-medium outline-hidden transition-[margin,opacity] duration-200 ease-linear focus-visible:ring-2",
297
+ "group-data-[collapsible=icon]:-mt-8 group-data-[collapsible=icon]:opacity-0",
298
+ className,
299
+ )}
300
+ {...props}
301
+ />
302
+ );
303
+ }
304
+
305
+ function SidebarMenu({ className, ...props }: React.ComponentProps<"ul">) {
306
+ return (
307
+ <ul
308
+ data-slot="sidebar-menu"
309
+ data-sidebar="menu"
310
+ className={cn("flex w-full min-w-0 flex-col gap-1", className)}
311
+ {...props}
312
+ />
313
+ );
314
+ }
315
+
316
+ function SidebarMenuItem({ className, ...props }: React.ComponentProps<"li">) {
317
+ return (
318
+ <li
319
+ data-slot="sidebar-menu-item"
320
+ data-sidebar="menu-item"
321
+ className={cn("group/menu-item relative", className)}
322
+ {...props}
323
+ />
324
+ );
325
+ }
326
+
327
+ const sidebarMenuButtonVariants = cva(
328
+ "peer/menu-button flex w-full items-center gap-2 overflow-hidden rounded-md p-2 text-left text-sm outline-hidden ring-sidebar-ring transition-[width,height,padding] hover:bg-sidebar-accent hover:text-sidebar-accent-foreground focus-visible:ring-2 active:bg-sidebar-accent active:text-sidebar-accent-foreground disabled:pointer-events-none disabled:opacity-50 group-has-data-[sidebar=menu-action]/menu-item:pr-8 aria-disabled:pointer-events-none aria-disabled:opacity-50 data-[active=true]:bg-sidebar-accent data-[active=true]:font-medium data-[active=true]:text-sidebar-accent-foreground data-[state=open]:hover:bg-sidebar-accent data-[state=open]:hover:text-sidebar-accent-foreground group-data-[collapsible=icon]:size-8 group-data-[collapsible=icon]:p-2 [&>span:last-child]:truncate [&>svg]:size-4 [&>svg]:shrink-0",
329
+ {
330
+ variants: {
331
+ variant: {
332
+ default: "hover:bg-sidebar-accent hover:text-sidebar-accent-foreground",
333
+ outline:
334
+ "bg-background shadow-[0_0_0_1px_hsl(var(--sidebar-border))] hover:bg-sidebar-accent hover:text-sidebar-accent-foreground hover:shadow-[0_0_0_1px_hsl(var(--sidebar-accent))]",
335
+ },
336
+ size: {
337
+ default: "h-8 text-sm",
338
+ sm: "h-7 text-xs",
339
+ lg: "h-12 text-sm group-data-[collapsible=icon]:p-0",
340
+ },
341
+ },
342
+ defaultVariants: {
343
+ variant: "default",
344
+ size: "default",
345
+ },
346
+ },
347
+ );
348
+
349
+ function SidebarMenuButton({
350
+ asChild = false,
351
+ isActive = false,
352
+ variant = "default",
353
+ size = "default",
354
+ className,
355
+ ...props
356
+ }: React.ComponentProps<"button"> & {
357
+ asChild?: boolean;
358
+ isActive?: boolean;
359
+ } & VariantProps<typeof sidebarMenuButtonVariants>) {
360
+ const Comp = asChild ? Slot : "button";
361
+
362
+ return (
363
+ <Comp
364
+ data-slot="sidebar-menu-button"
365
+ data-sidebar="menu-button"
366
+ data-size={size}
367
+ data-active={isActive}
368
+ className={cn(sidebarMenuButtonVariants({ variant, size }), className)}
369
+ {...props}
370
+ />
371
+ );
372
+ }
373
+
374
+ export {
375
+ Sidebar,
376
+ SidebarContent,
377
+ SidebarFooter,
378
+ SidebarGroup,
379
+ SidebarGroupLabel,
380
+ SidebarHeader,
381
+ SidebarInset,
382
+ SidebarMenu,
383
+ SidebarMenuButton,
384
+ SidebarMenuItem,
385
+ SidebarProvider,
386
+ SidebarRail,
387
+ SidebarTrigger,
388
+ useSidebar,
389
+ };
@@ -0,0 +1,43 @@
1
+ "use client";
2
+
3
+ import { Button } from "#components/ui/button";
4
+ import { cn } from "#utils/cn";
5
+
6
+ export interface StatusFilterOption<T extends string = string> {
7
+ label: string;
8
+ value: T | undefined;
9
+ count?: number;
10
+ }
11
+
12
+ export interface StatusFilterProps<T extends string = string> {
13
+ options: StatusFilterOption<T>[];
14
+ value: T | undefined;
15
+ onChange: (value: T | undefined) => void;
16
+ className?: string;
17
+ }
18
+
19
+ export function StatusFilter<T extends string = string>({
20
+ options,
21
+ value,
22
+ onChange,
23
+ className,
24
+ }: StatusFilterProps<T>) {
25
+ return (
26
+ <div
27
+ data-slot="status-filter"
28
+ className={cn("flex gap-2 flex-wrap", className)}
29
+ >
30
+ {options.map((option) => (
31
+ <Button
32
+ key={option.label}
33
+ variant={value === option.value ? "default" : "outline"}
34
+ size="sm"
35
+ onClick={() => onChange(option.value)}
36
+ >
37
+ {option.label}
38
+ {option.count !== undefined && ` (${option.count})`}
39
+ </Button>
40
+ ))}
41
+ </div>
42
+ );
43
+ }