@withmata/blueprints 0.3.4 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude/skills/audit/SKILL.md +4 -4
- package/.claude/skills/blueprint-catalog/SKILL.md +17 -7
- package/.claude/skills/copywrite/SKILL.md +187 -0
- package/.claude/skills/copywrite-landing/SKILL.md +489 -0
- package/.claude/skills/design-system/SKILL.md +970 -0
- package/.claude/skills/new-project/SKILL.md +168 -112
- package/.claude/skills/scaffold-auth/SKILL.md +9 -9
- package/.claude/skills/scaffold-db/SKILL.md +14 -14
- package/.claude/skills/scaffold-env/SKILL.md +4 -4
- package/.claude/skills/scaffold-foundation/SKILL.md +15 -15
- package/.claude/skills/scaffold-tailwind/SKILL.md +17 -3
- package/.claude/skills/scaffold-ui/SKILL.md +155 -36
- package/ENGINEERING.md +2 -2
- package/blueprints/discovery/design-system/BLUEPRINT.md +1479 -0
- package/blueprints/discovery/marketing-copywriting/BLUEPRINT.md +664 -0
- package/blueprints/features/auth-better-auth/BLUEPRINT.md +20 -22
- package/blueprints/features/db-drizzle-postgres/BLUEPRINT.md +12 -12
- package/blueprints/features/db-drizzle-postgres/files/db/src/example-entity.ts +1 -1
- package/blueprints/features/db-drizzle-postgres/files/db/src/scripts/seed.ts +1 -1
- package/blueprints/features/env-t3/BLUEPRINT.md +1 -1
- package/blueprints/features/tailwind-v4/BLUEPRINT.md +9 -2
- package/blueprints/features/tailwind-v4/files/tailwind-config/shared-styles.css +80 -1
- package/blueprints/features/ui-shared-components/BLUEPRINT.md +411 -78
- package/blueprints/features/ui-shared-components/files/ui/components/ui/alert-dialog.tsx +192 -0
- package/blueprints/features/ui-shared-components/files/ui/components/ui/avatar.tsx +71 -0
- package/blueprints/features/ui-shared-components/files/ui/components/ui/badge.tsx +52 -0
- package/blueprints/features/ui-shared-components/files/ui/components/ui/breadcrumb.tsx +122 -0
- package/blueprints/features/ui-shared-components/files/ui/components/ui/button.tsx +56 -0
- package/blueprints/features/ui-shared-components/files/ui/components/ui/card-select.tsx +72 -0
- package/blueprints/features/ui-shared-components/files/ui/components/ui/card.tsx +100 -0
- package/blueprints/features/ui-shared-components/files/ui/components/ui/collapsible.tsx +34 -0
- package/blueprints/features/ui-shared-components/files/ui/components/ui/combobox.tsx +301 -0
- package/blueprints/features/ui-shared-components/files/ui/components/ui/dropdown-menu.tsx +264 -0
- package/blueprints/features/ui-shared-components/files/ui/components/ui/empty-state.tsx +43 -0
- package/blueprints/features/ui-shared-components/files/ui/components/ui/entity-select.tsx +110 -0
- package/blueprints/features/ui-shared-components/files/ui/components/ui/field.tsx +237 -0
- package/blueprints/features/ui-shared-components/files/ui/components/ui/form-field.tsx +217 -0
- package/blueprints/features/ui-shared-components/files/ui/components/ui/input-group.tsx +161 -0
- package/blueprints/features/ui-shared-components/files/ui/components/ui/input.tsx +20 -0
- package/blueprints/features/ui-shared-components/files/ui/components/ui/label.tsx +20 -0
- package/blueprints/features/ui-shared-components/files/ui/components/ui/org-switcher.tsx +114 -0
- package/blueprints/features/ui-shared-components/files/ui/components/ui/page-header.tsx +45 -0
- package/blueprints/features/ui-shared-components/files/ui/components/ui/pagination.tsx +52 -0
- package/blueprints/features/ui-shared-components/files/ui/components/ui/pill-select.tsx +151 -0
- package/blueprints/features/ui-shared-components/files/ui/components/ui/popover.tsx +41 -0
- package/blueprints/features/ui-shared-components/files/ui/components/ui/search-input.tsx +49 -0
- package/blueprints/features/ui-shared-components/files/ui/components/ui/select.tsx +205 -0
- package/blueprints/features/ui-shared-components/files/ui/components/ui/selected-entity-card.tsx +47 -0
- package/blueprints/features/ui-shared-components/files/ui/components/ui/separator.tsx +25 -0
- package/blueprints/features/ui-shared-components/files/ui/components/ui/sidebar.tsx +389 -0
- package/blueprints/features/ui-shared-components/files/ui/components/ui/status-filter.tsx +43 -0
- package/blueprints/features/ui-shared-components/files/ui/components/ui/tag-input.tsx +131 -0
- package/blueprints/features/ui-shared-components/files/ui/components/ui/textarea.tsx +18 -0
- package/blueprints/features/ui-shared-components/files/ui/components/ui/user-menu.tsx +149 -0
- package/blueprints/features/ui-shared-components/files/ui/components.json +11 -8
- package/blueprints/features/ui-shared-components/files/ui/package.json +20 -11
- package/blueprints/foundation/monorepo-turbo/BLUEPRINT.md +19 -20
- package/blueprints/foundation/monorepo-turbo/files/root/package.json +1 -1
- package/dist/index.js +241 -100
- package/package.json +1 -1
- package/blueprints/features/tailwind-v4/files/tailwind-config/package.json +0 -20
- 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
|
+
};
|
package/blueprints/features/ui-shared-components/files/ui/components/ui/selected-entity-card.tsx
ADDED
|
@@ -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
|
+
}
|