saasaloy 0.2.0 → 0.3.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "saasaloy",
3
- "version": "0.2.0",
3
+ "version": "0.3.0",
4
4
  "description": "Composable SaaS accelerator kit.",
5
5
  "keywords": [
6
6
  "saas",
@@ -51,19 +51,19 @@
51
51
  },
52
52
  "devDependencies": {
53
53
  "@release-it/conventional-changelog": "12.0.0",
54
- "@types/node": "26.1.1",
55
- "@vitest/coverage-v8": "4.1.10",
54
+ "@types/node": "26.4.1",
55
+ "@vitest/coverage-v8": "5.0.0",
56
56
  "release-it": "21.0.2",
57
57
  "tsup": "8.5.1",
58
- "vitest": "4.1.10"
58
+ "vitest": "5.0.0"
59
59
  },
60
60
  "dependencies": {
61
61
  "@clack/prompts": "1.7.0",
62
- "ajv": "8.17.1",
62
+ "ajv": "8.20.0",
63
63
  "diff": "9.0.0",
64
- "giget": "3.3.0",
64
+ "giget": "3.3.1",
65
65
  "jsonc-parser": "3.3.1",
66
- "magicast": "0.5.3",
66
+ "magicast": "0.5.4",
67
67
  "picocolors": "1.1.1"
68
68
  }
69
69
  }
@@ -97,7 +97,8 @@
97
97
  "package-json-script",
98
98
  "plugin-array",
99
99
  "const-array",
100
- "chained-route"
100
+ "chained-route",
101
+ "drizzle-column"
101
102
  ],
102
103
  "description": "Which codemod applied this patch. Must list exactly the kinds registry-item.schema.json accepts — the applier records here whatever a descriptor authored, so a kind missing from this enum makes the next `loadManifest` refuse a manifest the tool itself wrote (#98). `schema.test.ts` asserts the two enums and the `Patch` union agree."
103
104
  }
@@ -158,9 +158,10 @@
158
158
  "package-json-script",
159
159
  "plugin-array",
160
160
  "const-array",
161
- "chained-route"
161
+ "chained-route",
162
+ "drizzle-column"
162
163
  ],
163
- "description": "Which codemod applies this patch. `const-array`, `plugin-array`, and `chained-route` have removal inverses."
164
+ "description": "Which codemod applies this patch. `const-array`, `plugin-array`, `chained-route`, `wrangler-binding` and `drizzle-column` have removal inverses."
164
165
  }
165
166
  },
166
167
  "allOf": [
@@ -211,6 +212,50 @@
211
212
  }
212
213
  }
213
214
  },
215
+ {
216
+ "if": {
217
+ "properties": { "kind": { "const": "drizzle-column" } },
218
+ "required": ["kind"]
219
+ },
220
+ "then": {
221
+ "required": ["exportName", "column", "value"],
222
+ "properties": {
223
+ "exportName": {
224
+ "type": "string",
225
+ "minLength": 1,
226
+ "description": "Exported binding holding the Drizzle table — the name in `export const <name> = sqliteTable(\"…\", { … })`. The columns object is that call's second argument."
227
+ },
228
+ "column": {
229
+ "type": "string",
230
+ "minLength": 1,
231
+ "description": "Property key added to the columns object (e.g. billingCustomerId). Also the match key: a table already carrying it is left alone, whatever it holds, and `remove` finds the column by it."
232
+ },
233
+ "value": {
234
+ "type": "string",
235
+ "minLength": 1,
236
+ "description": "The column expression, verbatim source (e.g. text(\"billing_customer_id\")). Written as-is and compared as-is on removal, so a column the user has since edited is refused rather than dropped. Ship one patch per dialect when the two builders differ."
237
+ },
238
+ "import": {
239
+ "type": "object",
240
+ "additionalProperties": false,
241
+ "required": ["name", "from"],
242
+ "description": "Named import the value needs, dropped again on `remove` when nothing else references it. Omit it when the table's other columns already import the builder.",
243
+ "properties": {
244
+ "name": {
245
+ "type": "string",
246
+ "minLength": 1,
247
+ "description": "Imported binding name (e.g. text)."
248
+ },
249
+ "from": {
250
+ "type": "string",
251
+ "minLength": 1,
252
+ "description": "Module specifier to import from (e.g. drizzle-orm/sqlite-core)."
253
+ }
254
+ }
255
+ }
256
+ }
257
+ }
258
+ },
214
259
  {
215
260
  "if": {
216
261
  "properties": { "kind": { "const": "chained-route" } },
@@ -147,6 +147,13 @@ pnpm --filter @repo/ui exec shadcn add dialog
147
147
  - Never `npx` (see Never Do).
148
148
  - The CLI writes into `src/components/`. Anything it appends to `package.json` arrives
149
149
  as a range — **re-pin it to an exact version**.
150
+ - **Add the primitive to a `@source` list, or it renders unstyled.** `globals.css` names
151
+ the components it scans one file at a time, because Tailwind scans content rather than
152
+ imports and a blanket glob charges every app for every primitive. A primitive the
153
+ landing page or `src/blocks/` renders gets an `@source "../components/<name>.tsx";` line
154
+ in `globals.css`; a primitive only one app renders gets its `@source` line in that app's
155
+ own stylesheet, the way `apps/admin/src/styles/admin.css` takes the whole directory.
156
+ Nothing errors when you skip this — the classes just never reach the CSS.
150
157
  - `style` is `base-nova` (Base UI) and is fixed at init — the CLI cannot change it later.
151
158
  - `rsc` is `false`, so the CLI strips the `"use client"` directive for you. It means
152
159
  nothing in Astro, and the vendored primitives don't carry it.
@@ -163,7 +170,7 @@ pnpm --filter @repo/ui exec shadcn add https://tweakcn.com/r/themes/modern-minim
163
170
  ```
164
171
 
165
172
  The CLI merges the preset's `:root`, `.dark` and `@theme inline` blocks **into** the
166
- existing ones, so the file's hand-written parts — the three `@source` globs, the
173
+ existing ones, so the file's hand-written parts — the `@source` rules, the
167
174
  `@custom-variant dark`, the `@layer base` rules — stay put, and `components.json` is not
168
175
  touched. It usually *extends* `@theme inline` with mappings the base does not carry
169
176
  (fonts, tracking, shadows); that is expected.
@@ -160,6 +160,10 @@ components:
160
160
 
161
161
  The base uses a neutral, content-first system. It gives a new product a clear structure without choosing a brand palette for the owner. Product identity enters through the theme tokens, the product brief, and the copy.
162
162
 
163
+ ## Apps
164
+
165
+ An app may carry its own token set in its own stylesheet on top of these tokens, importing `packages/ui/src/styles/globals.css` first and reassigning the semantic custom properties after it. The admin app does that, and its skill documents the values.
166
+
163
167
  ## Colors
164
168
 
165
169
  The light theme uses white surfaces, near-black text, and a dark neutral primary action. The dark theme reverses that relationship with near-black surfaces and near-white text. Muted neutrals separate supporting content. The destructive color is the only chromatic semantic token in the seed.
@@ -126,7 +126,7 @@ Use the package-local executable from the project root.
126
126
  pnpm --filter @repo/ui exec shadcn add <registry-style-url>
127
127
  ```
128
128
 
129
- Confirm that the three `@source` rules, `@custom-variant dark`, and `@layer base` remain in `globals.css`. Confirm that `components.json` did not change.
129
+ Confirm that the `@source` rules, `@custom-variant dark`, and `@layer base` remain in `globals.css`. Confirm that `components.json` did not change.
130
130
 
131
131
  Stop and report the failure when the preset removes a required rule or changes `components.json`. Do not repair an unknown preset merge without approval.
132
132
 
@@ -22,7 +22,7 @@
22
22
  "@tailwindcss/vite": "4.3.3",
23
23
  "@types/node": "26.4.1",
24
24
  "@types/react": "19.2.18",
25
- "@types/react-dom": "19.2.5",
25
+ "@types/react-dom": "19.2.7",
26
26
  "rimraf": "6.1.3",
27
27
  "tailwindcss": "4.3.3",
28
28
  "wrangler": "4.129.0"
@@ -3,7 +3,7 @@
3
3
  "version": "0.0.0",
4
4
  "private": true,
5
5
  "type": "module",
6
- "packageManager": "pnpm@11.14.0",
6
+ "packageManager": "pnpm@12.3.4",
7
7
  "engines": {
8
8
  "node": ">=24.13.0",
9
9
  "pnpm": ">=11"
@@ -26,19 +26,19 @@
26
26
  "@commitlint/cli": "21.2.2",
27
27
  "@commitlint/config-conventional": "21.2.2",
28
28
  "husky": "9.1.7",
29
- "lint-staged": "17.4.1",
29
+ "lint-staged": "17.5.0",
30
30
  "oxlint": "1.81.0",
31
31
  "oxlint-tsgolint": "7.0.2001",
32
32
  "prettier": "3.9.6",
33
33
  "prettier-plugin-astro": "0.14.1",
34
34
  "prettier-plugin-tailwindcss": "0.8.1",
35
35
  "rimraf": "6.1.3",
36
- "stylelint": "17.14.1",
36
+ "stylelint": "17.15.0",
37
37
  "stylelint-config-idiomatic-order": "10.0.0",
38
38
  "stylelint-config-standard": "40.0.0",
39
39
  "stylelint-prettier": "5.0.3",
40
40
  "turbo": "2.10.12",
41
41
  "typescript": "7.0.2",
42
- "ultracite": "7.10.7"
42
+ "ultracite": "7.11.0"
43
43
  }
44
44
  }
@@ -17,11 +17,11 @@
17
17
  "typecheck": "tsc --noEmit -p tsconfig.json"
18
18
  },
19
19
  "dependencies": {
20
- "@base-ui/react": "1.7.0",
20
+ "@base-ui/react": "1.8.0",
21
21
  "class-variance-authority": "0.7.1",
22
22
  "clsx": "2.1.1",
23
- "lucide-react": "1.39.0",
24
- "shadcn": "4.20.1",
23
+ "lucide-react": "1.41.0",
24
+ "shadcn": "4.21.0",
25
25
  "tailwind-merge": "3.6.0",
26
26
  "tailwindcss": "4.3.3",
27
27
  "tw-animate-css": "1.4.0"
@@ -56,8 +56,12 @@ export function Footer({
56
56
  siteName = "Acme",
57
57
  tagline = landing.footer.tagline,
58
58
  groups = defaultGroups,
59
- year = new Date().getFullYear(),
59
+ year: yearProp,
60
60
  }: FooterProps) {
61
+ // Read the clock in the body, not as a destructuring default. The React Compiler cannot
62
+ // safely reorder a call expression in a parameter default and bails out of the component.
63
+ const year = yearProp ?? new Date().getFullYear();
64
+
61
65
  return (
62
66
  <footer className="border-border/60 mt-8 border-t">
63
67
  <div className="mx-auto w-full max-w-6xl px-6 py-14">
@@ -0,0 +1,107 @@
1
+ import * as React from "react";
2
+ import { Avatar as AvatarPrimitive } from "@base-ui/react/avatar";
3
+
4
+ import { cn } from "@repo/ui/lib/utils";
5
+
6
+ function Avatar({
7
+ className,
8
+ size = "default",
9
+ ...props
10
+ }: AvatarPrimitive.Root.Props & {
11
+ size?: "default" | "sm" | "lg";
12
+ }) {
13
+ return (
14
+ <AvatarPrimitive.Root
15
+ data-slot="avatar"
16
+ data-size={size}
17
+ className={cn(
18
+ "group/avatar after:border-border relative flex size-8 shrink-0 rounded-full select-none after:absolute after:inset-0 after:rounded-full after:border after:mix-blend-darken data-[size=lg]:size-10 data-[size=sm]:size-6 dark:after:mix-blend-lighten",
19
+ className
20
+ )}
21
+ {...props}
22
+ />
23
+ );
24
+ }
25
+
26
+ function AvatarImage({ className, ...props }: AvatarPrimitive.Image.Props) {
27
+ return (
28
+ <AvatarPrimitive.Image
29
+ data-slot="avatar-image"
30
+ className={cn(
31
+ "aspect-square size-full rounded-full object-cover",
32
+ className
33
+ )}
34
+ {...props}
35
+ />
36
+ );
37
+ }
38
+
39
+ function AvatarFallback({
40
+ className,
41
+ ...props
42
+ }: AvatarPrimitive.Fallback.Props) {
43
+ return (
44
+ <AvatarPrimitive.Fallback
45
+ data-slot="avatar-fallback"
46
+ className={cn(
47
+ "bg-muted text-muted-foreground flex size-full items-center justify-center rounded-full text-sm group-data-[size=sm]/avatar:text-xs",
48
+ className
49
+ )}
50
+ {...props}
51
+ />
52
+ );
53
+ }
54
+
55
+ function AvatarBadge({ className, ...props }: React.ComponentProps<"span">) {
56
+ return (
57
+ <span
58
+ data-slot="avatar-badge"
59
+ className={cn(
60
+ "bg-primary text-primary-foreground ring-background absolute right-0 bottom-0 z-10 inline-flex items-center justify-center rounded-full bg-blend-color ring-2 select-none",
61
+ "group-data-[size=sm]/avatar:size-2 group-data-[size=sm]/avatar:[&>svg]:hidden",
62
+ "group-data-[size=default]/avatar:size-2.5 group-data-[size=default]/avatar:[&>svg]:size-2",
63
+ "group-data-[size=lg]/avatar:size-3 group-data-[size=lg]/avatar:[&>svg]:size-2",
64
+ className
65
+ )}
66
+ {...props}
67
+ />
68
+ );
69
+ }
70
+
71
+ function AvatarGroup({ className, ...props }: React.ComponentProps<"div">) {
72
+ return (
73
+ <div
74
+ data-slot="avatar-group"
75
+ className={cn(
76
+ "group/avatar-group *:data-[slot=avatar]:ring-background flex -space-x-2 *:data-[slot=avatar]:ring-2",
77
+ className
78
+ )}
79
+ {...props}
80
+ />
81
+ );
82
+ }
83
+
84
+ function AvatarGroupCount({
85
+ className,
86
+ ...props
87
+ }: React.ComponentProps<"div">) {
88
+ return (
89
+ <div
90
+ data-slot="avatar-group-count"
91
+ className={cn(
92
+ "bg-muted text-muted-foreground ring-background relative flex size-8 shrink-0 items-center justify-center rounded-full text-sm ring-2 group-has-data-[size=lg]/avatar-group:size-10 group-has-data-[size=sm]/avatar-group:size-6 [&>svg]:size-4 group-has-data-[size=lg]/avatar-group:[&>svg]:size-5 group-has-data-[size=sm]/avatar-group:[&>svg]:size-3",
93
+ className
94
+ )}
95
+ {...props}
96
+ />
97
+ );
98
+ }
99
+
100
+ export {
101
+ Avatar,
102
+ AvatarImage,
103
+ AvatarFallback,
104
+ AvatarGroup,
105
+ AvatarGroupCount,
106
+ AvatarBadge,
107
+ };
@@ -0,0 +1,19 @@
1
+ import { Collapsible as CollapsiblePrimitive } from "@base-ui/react/collapsible";
2
+
3
+ function Collapsible({ ...props }: CollapsiblePrimitive.Root.Props) {
4
+ return <CollapsiblePrimitive.Root data-slot="collapsible" {...props} />;
5
+ }
6
+
7
+ function CollapsibleTrigger({ ...props }: CollapsiblePrimitive.Trigger.Props) {
8
+ return (
9
+ <CollapsiblePrimitive.Trigger data-slot="collapsible-trigger" {...props} />
10
+ );
11
+ }
12
+
13
+ function CollapsibleContent({ ...props }: CollapsiblePrimitive.Panel.Props) {
14
+ return (
15
+ <CollapsiblePrimitive.Panel data-slot="collapsible-content" {...props} />
16
+ );
17
+ }
18
+
19
+ export { Collapsible, CollapsibleTrigger, CollapsibleContent };
@@ -0,0 +1,270 @@
1
+ import * as React from "react";
2
+ import { Menu as MenuPrimitive } from "@base-ui/react/menu";
3
+
4
+ import { cn } from "@repo/ui/lib/utils";
5
+ import { CheckIcon, ChevronRightIcon } from "lucide-react";
6
+
7
+ function DropdownMenu({ ...props }: MenuPrimitive.Root.Props) {
8
+ return <MenuPrimitive.Root data-slot="dropdown-menu" {...props} />;
9
+ }
10
+
11
+ function DropdownMenuPortal({ ...props }: MenuPrimitive.Portal.Props) {
12
+ return <MenuPrimitive.Portal data-slot="dropdown-menu-portal" {...props} />;
13
+ }
14
+
15
+ function DropdownMenuTrigger({ ...props }: MenuPrimitive.Trigger.Props) {
16
+ return <MenuPrimitive.Trigger data-slot="dropdown-menu-trigger" {...props} />;
17
+ }
18
+
19
+ function DropdownMenuContent({
20
+ align = "start",
21
+ alignOffset = 0,
22
+ side = "bottom",
23
+ sideOffset = 4,
24
+ className,
25
+ ...props
26
+ }: MenuPrimitive.Popup.Props &
27
+ Pick<
28
+ MenuPrimitive.Positioner.Props,
29
+ "align" | "alignOffset" | "side" | "sideOffset"
30
+ >) {
31
+ return (
32
+ <MenuPrimitive.Portal>
33
+ <MenuPrimitive.Positioner
34
+ className="isolate z-50 outline-none"
35
+ align={align}
36
+ alignOffset={alignOffset}
37
+ side={side}
38
+ sideOffset={sideOffset}
39
+ >
40
+ <MenuPrimitive.Popup
41
+ data-slot="dropdown-menu-content"
42
+ className={cn(
43
+ "bg-popover text-popover-foreground ring-foreground/10 data-[side=bottom]:slide-in-from-top-2 data-[side=inline-end]:slide-in-from-left-2 data-[side=inline-start]:slide-in-from-right-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 data-open:animate-in data-open:fade-in-0 data-open:zoom-in-95 data-closed:animate-out data-closed:fade-out-0 data-closed:zoom-out-95 z-50 max-h-(--available-height) w-(--anchor-width) min-w-32 origin-(--transform-origin) overflow-x-hidden overflow-y-auto rounded-lg p-1 shadow-md ring-1 duration-100 outline-none data-closed:overflow-hidden",
44
+ className
45
+ )}
46
+ {...props}
47
+ />
48
+ </MenuPrimitive.Positioner>
49
+ </MenuPrimitive.Portal>
50
+ );
51
+ }
52
+
53
+ function DropdownMenuGroup({ ...props }: MenuPrimitive.Group.Props) {
54
+ return <MenuPrimitive.Group data-slot="dropdown-menu-group" {...props} />;
55
+ }
56
+
57
+ function DropdownMenuLabel({
58
+ className,
59
+ inset,
60
+ ...props
61
+ }: MenuPrimitive.GroupLabel.Props & {
62
+ inset?: boolean;
63
+ }) {
64
+ return (
65
+ <MenuPrimitive.GroupLabel
66
+ data-slot="dropdown-menu-label"
67
+ data-inset={inset}
68
+ className={cn(
69
+ "text-muted-foreground px-1.5 py-1 text-xs font-medium data-inset:pl-7",
70
+ className
71
+ )}
72
+ {...props}
73
+ />
74
+ );
75
+ }
76
+
77
+ function DropdownMenuItem({
78
+ className,
79
+ inset,
80
+ variant = "default",
81
+ ...props
82
+ }: MenuPrimitive.Item.Props & {
83
+ inset?: boolean;
84
+ variant?: "default" | "destructive";
85
+ }) {
86
+ return (
87
+ <MenuPrimitive.Item
88
+ data-slot="dropdown-menu-item"
89
+ data-inset={inset}
90
+ data-variant={variant}
91
+ className={cn(
92
+ "group/dropdown-menu-item focus:bg-accent focus:text-accent-foreground not-data-[variant=destructive]:focus:**:text-accent-foreground data-[variant=destructive]:text-destructive data-[variant=destructive]:focus:bg-destructive/10 data-[variant=destructive]:focus:text-destructive dark:data-[variant=destructive]:focus:bg-destructive/20 data-[variant=destructive]:*:[svg]:text-destructive relative flex cursor-default items-center gap-1.5 rounded-md px-1.5 py-1 text-sm outline-hidden select-none data-disabled:pointer-events-none data-disabled:opacity-50 data-inset:pl-7 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
93
+ className
94
+ )}
95
+ {...props}
96
+ />
97
+ );
98
+ }
99
+
100
+ function DropdownMenuSub({ ...props }: MenuPrimitive.SubmenuRoot.Props) {
101
+ return <MenuPrimitive.SubmenuRoot data-slot="dropdown-menu-sub" {...props} />;
102
+ }
103
+
104
+ function DropdownMenuSubTrigger({
105
+ className,
106
+ inset,
107
+ children,
108
+ ...props
109
+ }: MenuPrimitive.SubmenuTrigger.Props & {
110
+ inset?: boolean;
111
+ }) {
112
+ return (
113
+ <MenuPrimitive.SubmenuTrigger
114
+ data-slot="dropdown-menu-sub-trigger"
115
+ data-inset={inset}
116
+ className={cn(
117
+ "focus:bg-accent focus:text-accent-foreground not-data-[variant=destructive]:focus:**:text-accent-foreground data-popup-open:bg-accent data-popup-open:text-accent-foreground data-open:bg-accent data-open:text-accent-foreground flex cursor-default items-center gap-1.5 rounded-md px-1.5 py-1 text-sm outline-hidden select-none data-inset:pl-7 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
118
+ className
119
+ )}
120
+ {...props}
121
+ >
122
+ {children}
123
+ <ChevronRightIcon className="ml-auto" />
124
+ </MenuPrimitive.SubmenuTrigger>
125
+ );
126
+ }
127
+
128
+ function DropdownMenuSubContent({
129
+ align = "start",
130
+ alignOffset = -3,
131
+ side = "right",
132
+ sideOffset = 0,
133
+ className,
134
+ ...props
135
+ }: React.ComponentProps<typeof DropdownMenuContent>) {
136
+ return (
137
+ <DropdownMenuContent
138
+ data-slot="dropdown-menu-sub-content"
139
+ className={cn(
140
+ "bg-popover text-popover-foreground ring-foreground/10 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 data-open:animate-in data-open:fade-in-0 data-open:zoom-in-95 data-closed:animate-out data-closed:fade-out-0 data-closed:zoom-out-95 w-auto min-w-[96px] rounded-lg p-1 shadow-lg ring-1 duration-100",
141
+ className
142
+ )}
143
+ align={align}
144
+ alignOffset={alignOffset}
145
+ side={side}
146
+ sideOffset={sideOffset}
147
+ {...props}
148
+ />
149
+ );
150
+ }
151
+
152
+ function DropdownMenuCheckboxItem({
153
+ className,
154
+ children,
155
+ checked,
156
+ inset,
157
+ ...props
158
+ }: MenuPrimitive.CheckboxItem.Props & {
159
+ inset?: boolean;
160
+ }) {
161
+ return (
162
+ <MenuPrimitive.CheckboxItem
163
+ data-slot="dropdown-menu-checkbox-item"
164
+ data-inset={inset}
165
+ className={cn(
166
+ "focus:bg-accent focus:text-accent-foreground focus:**:text-accent-foreground relative flex cursor-default items-center gap-1.5 rounded-md py-1 pr-8 pl-1.5 text-sm outline-hidden select-none data-disabled:pointer-events-none data-disabled:opacity-50 data-inset:pl-7 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
167
+ className
168
+ )}
169
+ checked={checked}
170
+ {...props}
171
+ >
172
+ <span
173
+ className="pointer-events-none absolute right-2 flex items-center justify-center"
174
+ data-slot="dropdown-menu-checkbox-item-indicator"
175
+ >
176
+ <MenuPrimitive.CheckboxItemIndicator>
177
+ <CheckIcon />
178
+ </MenuPrimitive.CheckboxItemIndicator>
179
+ </span>
180
+ {children}
181
+ </MenuPrimitive.CheckboxItem>
182
+ );
183
+ }
184
+
185
+ function DropdownMenuRadioGroup({ ...props }: MenuPrimitive.RadioGroup.Props) {
186
+ return (
187
+ <MenuPrimitive.RadioGroup
188
+ data-slot="dropdown-menu-radio-group"
189
+ {...props}
190
+ />
191
+ );
192
+ }
193
+
194
+ function DropdownMenuRadioItem({
195
+ className,
196
+ children,
197
+ inset,
198
+ ...props
199
+ }: MenuPrimitive.RadioItem.Props & {
200
+ inset?: boolean;
201
+ }) {
202
+ return (
203
+ <MenuPrimitive.RadioItem
204
+ data-slot="dropdown-menu-radio-item"
205
+ data-inset={inset}
206
+ className={cn(
207
+ "focus:bg-accent focus:text-accent-foreground focus:**:text-accent-foreground relative flex cursor-default items-center gap-1.5 rounded-md py-1 pr-8 pl-1.5 text-sm outline-hidden select-none data-disabled:pointer-events-none data-disabled:opacity-50 data-inset:pl-7 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
208
+ className
209
+ )}
210
+ {...props}
211
+ >
212
+ <span
213
+ className="pointer-events-none absolute right-2 flex items-center justify-center"
214
+ data-slot="dropdown-menu-radio-item-indicator"
215
+ >
216
+ <MenuPrimitive.RadioItemIndicator>
217
+ <CheckIcon />
218
+ </MenuPrimitive.RadioItemIndicator>
219
+ </span>
220
+ {children}
221
+ </MenuPrimitive.RadioItem>
222
+ );
223
+ }
224
+
225
+ function DropdownMenuSeparator({
226
+ className,
227
+ ...props
228
+ }: MenuPrimitive.Separator.Props) {
229
+ return (
230
+ <MenuPrimitive.Separator
231
+ data-slot="dropdown-menu-separator"
232
+ className={cn("bg-border -mx-1 my-1 h-px", className)}
233
+ {...props}
234
+ />
235
+ );
236
+ }
237
+
238
+ function DropdownMenuShortcut({
239
+ className,
240
+ ...props
241
+ }: React.ComponentProps<"span">) {
242
+ return (
243
+ <span
244
+ data-slot="dropdown-menu-shortcut"
245
+ className={cn(
246
+ "text-muted-foreground group-focus/dropdown-menu-item:text-accent-foreground ml-auto text-xs tracking-widest",
247
+ className
248
+ )}
249
+ {...props}
250
+ />
251
+ );
252
+ }
253
+
254
+ export {
255
+ DropdownMenu,
256
+ DropdownMenuPortal,
257
+ DropdownMenuTrigger,
258
+ DropdownMenuContent,
259
+ DropdownMenuGroup,
260
+ DropdownMenuLabel,
261
+ DropdownMenuItem,
262
+ DropdownMenuCheckboxItem,
263
+ DropdownMenuRadioGroup,
264
+ DropdownMenuRadioItem,
265
+ DropdownMenuSeparator,
266
+ DropdownMenuShortcut,
267
+ DropdownMenuSub,
268
+ DropdownMenuSubTrigger,
269
+ DropdownMenuSubContent,
270
+ };
@@ -0,0 +1,52 @@
1
+ import { ScrollArea as ScrollAreaPrimitive } from "@base-ui/react/scroll-area";
2
+
3
+ import { cn } from "@repo/ui/lib/utils";
4
+
5
+ function ScrollArea({
6
+ className,
7
+ children,
8
+ ...props
9
+ }: ScrollAreaPrimitive.Root.Props) {
10
+ return (
11
+ <ScrollAreaPrimitive.Root
12
+ data-slot="scroll-area"
13
+ className={cn("relative", className)}
14
+ {...props}
15
+ >
16
+ <ScrollAreaPrimitive.Viewport
17
+ data-slot="scroll-area-viewport"
18
+ className="focus-visible:ring-ring/50 size-full rounded-[inherit] transition-[color,box-shadow] outline-none focus-visible:ring-[3px] focus-visible:outline-1"
19
+ >
20
+ {children}
21
+ </ScrollAreaPrimitive.Viewport>
22
+ <ScrollBar />
23
+ <ScrollAreaPrimitive.Corner />
24
+ </ScrollAreaPrimitive.Root>
25
+ );
26
+ }
27
+
28
+ function ScrollBar({
29
+ className,
30
+ orientation = "vertical",
31
+ ...props
32
+ }: ScrollAreaPrimitive.Scrollbar.Props) {
33
+ return (
34
+ <ScrollAreaPrimitive.Scrollbar
35
+ data-slot="scroll-area-scrollbar"
36
+ data-orientation={orientation}
37
+ orientation={orientation}
38
+ className={cn(
39
+ "flex touch-none p-px transition-colors select-none data-horizontal:h-2.5 data-horizontal:flex-col data-horizontal:border-t data-horizontal:border-t-transparent data-vertical:h-full data-vertical:w-2.5 data-vertical:border-l data-vertical:border-l-transparent",
40
+ className
41
+ )}
42
+ {...props}
43
+ >
44
+ <ScrollAreaPrimitive.Thumb
45
+ data-slot="scroll-area-thumb"
46
+ className="bg-border relative flex-1 rounded-full"
47
+ />
48
+ </ScrollAreaPrimitive.Scrollbar>
49
+ );
50
+ }
51
+
52
+ export { ScrollArea, ScrollBar };