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/README.md +5 -4
- package/dist/index.js +413 -21
- package/dist/index.js.map +1 -1
- package/package.json +7 -7
- package/schemas/manifest.schema.json +2 -1
- package/schemas/registry-item.schema.json +47 -2
- package/templates/base/AGENTS.md +8 -1
- package/templates/base/DESIGN.md +4 -0
- package/templates/base/_agents/skills/saasaloy-design/SKILL.md +1 -1
- package/templates/base/apps/web/package.json +1 -1
- package/templates/base/package.json +4 -4
- package/templates/base/packages/ui/package.json +3 -3
- package/templates/base/packages/ui/src/blocks/footer.tsx +5 -1
- package/templates/base/packages/ui/src/components/avatar.tsx +107 -0
- package/templates/base/packages/ui/src/components/collapsible.tsx +19 -0
- package/templates/base/packages/ui/src/components/dropdown-menu.tsx +270 -0
- package/templates/base/packages/ui/src/components/scroll-area.tsx +52 -0
- package/templates/base/packages/ui/src/components/sheet.tsx +132 -0
- package/templates/base/packages/ui/src/components/table.tsx +114 -0
- package/templates/base/packages/ui/src/components/tabs.tsx +81 -0
- package/templates/base/packages/ui/src/components/tooltip.tsx +64 -0
- package/templates/base/packages/ui/src/lib/sentinel.ts +1 -1
- package/templates/base/packages/ui/src/lib/theme.ts +124 -1
- package/templates/base/packages/ui/src/styles/globals.css +28 -1
- package/templates/base/stylelint.config.js +8 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "saasaloy",
|
|
3
|
-
"version": "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.
|
|
55
|
-
"@vitest/coverage-v8": "
|
|
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": "
|
|
58
|
+
"vitest": "5.0.0"
|
|
59
59
|
},
|
|
60
60
|
"dependencies": {
|
|
61
61
|
"@clack/prompts": "1.7.0",
|
|
62
|
-
"ajv": "8.
|
|
62
|
+
"ajv": "8.20.0",
|
|
63
63
|
"diff": "9.0.0",
|
|
64
|
-
"giget": "3.3.
|
|
64
|
+
"giget": "3.3.1",
|
|
65
65
|
"jsonc-parser": "3.3.1",
|
|
66
|
-
"magicast": "0.5.
|
|
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`,
|
|
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" } },
|
package/templates/base/AGENTS.md
CHANGED
|
@@ -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
|
|
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.
|
package/templates/base/DESIGN.md
CHANGED
|
@@ -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
|
|
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
|
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
"version": "0.0.0",
|
|
4
4
|
"private": true,
|
|
5
5
|
"type": "module",
|
|
6
|
-
"packageManager": "pnpm@
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
24
|
-
"shadcn": "4.
|
|
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
|
|
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 };
|