shapes-ui 0.5.0 → 0.6.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/.github/workflows/pr-preview.yml +9 -2
- package/CHANGELOG.md +11 -0
- package/README.md +13 -0
- package/content/components/accordion.mdx +13 -0
- package/content/components/alert-dialog.mdx +34 -0
- package/content/components/autocomplete.mdx +62 -0
- package/content/components/avatar.mdx +11 -0
- package/content/components/button.mdx +8 -0
- package/content/components/checkbox.mdx +11 -0
- package/content/components/collapsible.mdx +11 -0
- package/content/components/combobox.mdx +33 -0
- package/content/components/context-menu.mdx +29 -0
- package/content/components/dialog.mdx +33 -0
- package/content/components/drawer.mdx +38 -0
- package/content/components/field.mdx +21 -0
- package/content/components/fieldset.mdx +10 -0
- package/content/components/form.mdx +8 -0
- package/content/components/input.mdx +4 -0
- package/content/components/menu.mdx +27 -0
- package/content/components/menubar.mdx +31 -0
- package/content/components/meter.mdx +14 -0
- package/content/components/navigation-menu.mdx +28 -0
- package/content/components/number-field.mdx +25 -0
- package/content/components/popover.mdx +22 -0
- package/content/components/preview-card.mdx +14 -2
- package/content/components/progress.mdx +15 -1
- package/content/components/radio.mdx +11 -0
- package/content/components/scroll-area.mdx +23 -0
- package/content/components/select.mdx +27 -0
- package/content/components/separator.mdx +29 -0
- package/content/components/slider.mdx +4 -0
- package/content/components/switch.mdx +4 -0
- package/content/components/tabs.mdx +15 -0
- package/content/components/toast.mdx +10 -0
- package/content/components/toggle-group.mdx +37 -0
- package/content/components/toggle.mdx +12 -0
- package/content/components/toolbar.mdx +22 -0
- package/content/components/tooltip.mdx +13 -0
- package/content/docs/installation.mdx +30 -0
- package/content-collections.ts +65 -1
- package/dist/cli.js +947 -101
- package/examples/__index.tsx +136 -68
- package/examples/autocomplete-align.tsx +39 -0
- package/examples/autocomplete-controlled.tsx +44 -0
- package/examples/autocomplete-groups.tsx +65 -0
- package/examples/autocomplete-no-clear.tsx +40 -0
- package/examples/avatar-demo.tsx +3 -3
- package/examples/input-group-with-button.tsx +1 -1
- package/examples/separator-demo.tsx +13 -0
- package/examples/separator-horizontal.tsx +18 -0
- package/package.json +19 -18
- package/public/base-ui.svg +1 -0
- package/src/assets/base-ui.svg +1 -0
- package/src/commands/add.ts +79 -38
- package/src/commands/cli.ts +50 -3
- package/src/commands/create.ts +262 -0
- package/src/commands/init.ts +45 -12
- package/src/commands/palette.ts +55 -0
- package/src/components/docs/layout/footer.tsx +2 -2
- package/src/components/docs/layout/header.tsx +7 -9
- package/src/components/docs/layout/mobile-menu.tsx +0 -1
- package/src/components/docs/layout/nav-list.tsx +2 -2
- package/src/components/docs/layout/page-header.tsx +52 -7
- package/src/components/docs/layout/split-layout.tsx +9 -10
- package/src/components/docs/layout/table-of-content.tsx +145 -0
- package/src/components/docs/markdown/components.tsx +142 -29
- package/src/components/docs/markdown/copy-button.tsx +41 -0
- package/src/components/docs/markdown/installation-block.tsx +5 -24
- package/src/components/docs/markdown/render-preview.tsx +1 -1
- package/src/components/ui/button-group.tsx +1 -1
- package/src/components/ui/scroll-area.tsx +11 -2
- package/src/lib/docs-headings.ts +72 -0
- package/src/routeTree.gen.ts +60 -3
- package/src/routes/__root.tsx +2 -2
- package/src/routes/components.$slug.tsx +20 -4
- package/src/routes/docs.$slug.tsx +78 -0
- package/src/routes/docs.tsx +15 -0
- package/src/styles/styles.css +1 -1
- package/src/utils/cli-utils.ts +8 -8
- package/src/utils/dependency-installer.ts +30 -0
- package/src/utils/package-manager.ts +4 -4
- package/src/utils/palette.ts +666 -0
- package/src/utils/schema.ts +6 -0
package/examples/avatar-demo.tsx
CHANGED
|
@@ -4,15 +4,15 @@ export default function AvatarDemo() {
|
|
|
4
4
|
return (
|
|
5
5
|
<div className="flex items-center gap-4">
|
|
6
6
|
<Avatar>
|
|
7
|
-
<AvatarImage src="
|
|
7
|
+
<AvatarImage src="/shps_black.svg" alt="Shapes UI" className={"bg-muted"} />
|
|
8
8
|
<AvatarFallback>CN</AvatarFallback>
|
|
9
9
|
</Avatar>
|
|
10
10
|
<Avatar size="sm">
|
|
11
|
-
<AvatarImage src="
|
|
11
|
+
<AvatarImage src="/shps_black.svg" alt="Shapes UI" className={"bg-muted"} />
|
|
12
12
|
<AvatarFallback>CN</AvatarFallback>
|
|
13
13
|
</Avatar>
|
|
14
14
|
<Avatar size="lg">
|
|
15
|
-
<AvatarImage src="
|
|
15
|
+
<AvatarImage src="/shps_black.svg" alt="Shapes UI" className={"bg-muted"} />
|
|
16
16
|
<AvatarFallback>CN</AvatarFallback>
|
|
17
17
|
</Avatar>
|
|
18
18
|
<Avatar>
|
|
@@ -16,7 +16,7 @@ export default function InputGroupButtonExample() {
|
|
|
16
16
|
return (
|
|
17
17
|
<div className="grid w-full max-w-sm gap-6">
|
|
18
18
|
<InputGroup>
|
|
19
|
-
<InputGroupInput placeholder="https://
|
|
19
|
+
<InputGroupInput placeholder="https://shapes-ui.com" readOnly />
|
|
20
20
|
<InputGroupAddon align="inline-end">
|
|
21
21
|
<InputGroupButton aria-label="Copy" title="Copy" size="icon-xs">
|
|
22
22
|
<CopyIcon />
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { Separator } from "@/components/ui/separator";
|
|
2
|
+
|
|
3
|
+
export default function SeparatorDemo() {
|
|
4
|
+
return (
|
|
5
|
+
<div className="flex h-5 items-center space-x-4 text-sm">
|
|
6
|
+
<div>Blog</div>
|
|
7
|
+
<Separator orientation="vertical" />
|
|
8
|
+
<div>Docs</div>
|
|
9
|
+
<Separator orientation="vertical" />
|
|
10
|
+
<div>Source</div>
|
|
11
|
+
</div>
|
|
12
|
+
);
|
|
13
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { Separator } from "@/components/ui/separator";
|
|
2
|
+
|
|
3
|
+
export default function SeparatorHorizontal() {
|
|
4
|
+
return (
|
|
5
|
+
<div className="w-full max-w-sm space-y-4">
|
|
6
|
+
<div className="space-y-1">
|
|
7
|
+
<h4 className="text-sm leading-none font-medium">Shapes UI</h4>
|
|
8
|
+
<p className="text-sm text-muted-foreground">Headless components and docs</p>
|
|
9
|
+
</div>
|
|
10
|
+
<Separator />
|
|
11
|
+
<div className="flex h-5 items-center gap-4 text-sm">
|
|
12
|
+
<div>Docs</div>
|
|
13
|
+
<Separator orientation="vertical" />
|
|
14
|
+
<div>Examples</div>
|
|
15
|
+
</div>
|
|
16
|
+
</div>
|
|
17
|
+
);
|
|
18
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "shapes-ui",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.6.0",
|
|
4
4
|
"repository": {
|
|
5
5
|
"type": "git",
|
|
6
6
|
"url": "https://github.com/abbbdab/shapes-ui.git"
|
|
@@ -16,13 +16,14 @@
|
|
|
16
16
|
"dependencies": {
|
|
17
17
|
"@base-ui/react": "^1.2.0",
|
|
18
18
|
"@clack/prompts": "^1.0.1",
|
|
19
|
-
"@
|
|
20
|
-
"@
|
|
21
|
-
"@tanstack/react-
|
|
22
|
-
"@tanstack/react-router
|
|
23
|
-
"@tanstack/react-router-
|
|
24
|
-
"@tanstack/react-
|
|
25
|
-
"@tanstack/
|
|
19
|
+
"@tabler/icons-react": "^3.37.1",
|
|
20
|
+
"@tailwindcss/vite": "^4.2.1",
|
|
21
|
+
"@tanstack/react-devtools": "^0.9.6",
|
|
22
|
+
"@tanstack/react-router": "^1.163.3",
|
|
23
|
+
"@tanstack/react-router-devtools": "^1.163.3",
|
|
24
|
+
"@tanstack/react-router-ssr-query": "^1.163.3",
|
|
25
|
+
"@tanstack/react-start": "^1.163.3",
|
|
26
|
+
"@tanstack/router-plugin": "^1.163.3",
|
|
26
27
|
"class-variance-authority": "^0.7.1",
|
|
27
28
|
"clsx": "^2.1.1",
|
|
28
29
|
"commander": "^14.0.3",
|
|
@@ -36,42 +37,42 @@
|
|
|
36
37
|
"react": "^19.2.4",
|
|
37
38
|
"react-dom": "^19.2.4",
|
|
38
39
|
"rehype-pretty-code": "^0.14.1",
|
|
39
|
-
"tailwind-merge": "^3.
|
|
40
|
-
"tailwindcss": "^4.1
|
|
40
|
+
"tailwind-merge": "^3.5.0",
|
|
41
|
+
"tailwindcss": "^4.2.1",
|
|
41
42
|
"tw-animate-css": "^1.4.0",
|
|
42
43
|
"vite-tsconfig-paths": "^6.1.1",
|
|
43
44
|
"zod-to-json-schema": "^3.25.1"
|
|
44
45
|
},
|
|
45
46
|
"devDependencies": {
|
|
46
47
|
"@changesets/cli": "^2.29.8",
|
|
47
|
-
"@cloudflare/vite-plugin": "^1.25.
|
|
48
|
-
"@content-collections/core": "^0.14.
|
|
48
|
+
"@cloudflare/vite-plugin": "^1.25.6",
|
|
49
|
+
"@content-collections/core": "^0.14.1",
|
|
49
50
|
"@content-collections/mdx": "^0.2.2",
|
|
50
51
|
"@content-collections/vite": "^0.2.9",
|
|
51
|
-
"@shikijs/rehype": "^3.
|
|
52
|
-
"@tanstack/devtools-vite": "^0.5.
|
|
52
|
+
"@shikijs/rehype": "^3.23.0",
|
|
53
|
+
"@tanstack/devtools-vite": "^0.5.2",
|
|
53
54
|
"@testing-library/dom": "^10.4.1",
|
|
54
55
|
"@testing-library/react": "^16.3.2",
|
|
55
56
|
"@types/figlet": "^1.7.0",
|
|
56
57
|
"@types/fs-extra": "^11.0.4",
|
|
57
58
|
"@types/gradient-string": "^1.1.6",
|
|
58
|
-
"@types/node": "^25.2
|
|
59
|
+
"@types/node": "^25.3.2",
|
|
59
60
|
"@types/react": "^19.2.14",
|
|
60
61
|
"@types/react-dom": "^19.2.3",
|
|
61
62
|
"@vitejs/plugin-react": "^5.1.4",
|
|
62
63
|
"add": "^2.0.6",
|
|
63
64
|
"jsdom": "^28.1.0",
|
|
64
65
|
"oxfmt": "^0.32.0",
|
|
65
|
-
"oxlint": "^1.
|
|
66
|
+
"oxlint": "^1.50.0",
|
|
66
67
|
"remark-gfm": "^4.0.1",
|
|
67
|
-
"shiki": "^3.
|
|
68
|
+
"shiki": "^3.23.0",
|
|
68
69
|
"tsup": "^8.5.1",
|
|
69
70
|
"tsx": "^4.21.0",
|
|
70
71
|
"typescript": "^5.9.3",
|
|
71
72
|
"vite": "^7.3.1",
|
|
72
73
|
"vitest": "^4.0.18",
|
|
73
74
|
"web-vitals": "^5.1.0",
|
|
74
|
-
"wrangler": "^4.
|
|
75
|
+
"wrangler": "^4.69.0",
|
|
75
76
|
"zod": "^4.3.6"
|
|
76
77
|
},
|
|
77
78
|
"scripts": {
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
<svg width="24" height="24" viewBox="0 0 17 24" xmlns="http://www.w3.org/2000/svg"><path fill="#737373" d="M9.5001 7.01537C9.2245 6.99837 9 7.22385 9 7.49999V23C13.4183 23 17 19.4183 17 15C17 10.7497 13.6854 7.27351 9.5001 7.01537Z"></path><path fill="#737373" d="M8 9.8V12V23C3.58172 23 0 19.0601 0 14.2V12V1C4.41828 1 8 4.93989 8 9.8Z"></path></svg>
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
<svg width="24" height="24" viewBox="0 0 17 24" xmlns="http://www.w3.org/2000/svg"><path fill="#737373" d="M9.5001 7.01537C9.2245 6.99837 9 7.22385 9 7.49999V23C13.4183 23 17 19.4183 17 15C17 10.7497 13.6854 7.27351 9.5001 7.01537Z"></path><path fill="#737373" d="M8 9.8V12V23C3.58172 23 0 19.0601 0 14.2V12V1C4.41828 1 8 4.93989 8 9.8Z"></path></svg>
|
package/src/commands/add.ts
CHANGED
|
@@ -1,18 +1,17 @@
|
|
|
1
1
|
import path from "node:path";
|
|
2
2
|
|
|
3
3
|
import { cancel, multiselect, note, spinner } from "@clack/prompts";
|
|
4
|
-
import { execa } from "execa";
|
|
5
4
|
import fs from "fs-extra";
|
|
6
5
|
|
|
7
6
|
import type { RegistryItem } from "@/types/registry-item";
|
|
8
7
|
import { exitIfCancelled } from "@/utils/cli-utils";
|
|
9
|
-
import {
|
|
8
|
+
import { installDependencies } from "@/utils/dependency-installer";
|
|
10
9
|
import type { Config } from "@/utils/schema";
|
|
11
10
|
|
|
12
11
|
const REGISTRY_URL = "https://shapes-ui.com/r";
|
|
13
12
|
|
|
14
|
-
export async function loadRegistryIndex() {
|
|
15
|
-
const localRegistryDir = path.resolve(
|
|
13
|
+
export async function loadRegistryIndex(cwd = process.cwd()) {
|
|
14
|
+
const localRegistryDir = path.resolve(cwd, "public/r");
|
|
16
15
|
if (await fs.pathExists(localRegistryDir)) {
|
|
17
16
|
const files = await fs.readdir(localRegistryDir);
|
|
18
17
|
const entries: RegistryItem[] = [];
|
|
@@ -47,7 +46,7 @@ export async function loadRegistryIndex() {
|
|
|
47
46
|
export async function pickComponents() {
|
|
48
47
|
let components: string[] = [];
|
|
49
48
|
try {
|
|
50
|
-
components = await loadRegistryIndex();
|
|
49
|
+
components = await loadRegistryIndex(process.cwd());
|
|
51
50
|
} catch (error) {
|
|
52
51
|
cancel(error instanceof Error ? error.message : "Failed to load registry.");
|
|
53
52
|
process.exit(1);
|
|
@@ -68,49 +67,90 @@ export async function pickComponents() {
|
|
|
68
67
|
return selected as string[];
|
|
69
68
|
}
|
|
70
69
|
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
70
|
+
type InstallContext = {
|
|
71
|
+
installing: Set<string>;
|
|
72
|
+
installed: Set<string>;
|
|
73
|
+
};
|
|
74
74
|
|
|
75
|
-
|
|
75
|
+
function createInstallContext(): InstallContext {
|
|
76
|
+
return {
|
|
77
|
+
installing: new Set<string>(),
|
|
78
|
+
installed: new Set<string>(),
|
|
79
|
+
};
|
|
80
|
+
}
|
|
76
81
|
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
}
|
|
86
|
-
|
|
82
|
+
export async function installComponent(
|
|
83
|
+
name: string,
|
|
84
|
+
config: Config,
|
|
85
|
+
context = createInstallContext(),
|
|
86
|
+
cwd = process.cwd(),
|
|
87
|
+
) {
|
|
88
|
+
if (context.installed.has(name)) return;
|
|
89
|
+
if (context.installing.has(name)) {
|
|
90
|
+
note(`Skipped circular dependency while resolving ${name}`);
|
|
91
|
+
return;
|
|
87
92
|
}
|
|
88
93
|
|
|
89
|
-
|
|
94
|
+
context.installing.add(name);
|
|
95
|
+
try {
|
|
96
|
+
const spin = spinner();
|
|
97
|
+
spin.start(`Fetching ${name}`);
|
|
98
|
+
|
|
99
|
+
let data: RegistryItem;
|
|
100
|
+
|
|
101
|
+
const localFile = path.resolve(cwd, `public/r/${name}.json`);
|
|
102
|
+
if (await fs.pathExists(localFile)) {
|
|
103
|
+
data = await fs.readJSON(localFile);
|
|
104
|
+
} else {
|
|
105
|
+
const res = await fetch(`${REGISTRY_URL}/${name}.json`);
|
|
106
|
+
if (!res.ok) {
|
|
107
|
+
spin.stop("Fetch failed");
|
|
108
|
+
throw new Error(`Component ${name} not found in registry.`);
|
|
109
|
+
}
|
|
110
|
+
data = await res.json();
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
spin.stop(`Fetched ${name}`);
|
|
90
114
|
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
115
|
+
// 1. Recursive Install of Registry Dependencies
|
|
116
|
+
if (data.registryDependencies) {
|
|
117
|
+
for (const dep of data.registryDependencies) {
|
|
118
|
+
const depPath = path.join(cwd, config.paths.ui, `${dep}.tsx`);
|
|
119
|
+
if (!fs.existsSync(depPath)) {
|
|
120
|
+
await installComponent(dep, config, context, cwd);
|
|
121
|
+
}
|
|
97
122
|
}
|
|
98
123
|
}
|
|
99
|
-
}
|
|
100
124
|
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
125
|
+
// 2. Install NPM Dependencies
|
|
126
|
+
if (data.dependencies?.length) {
|
|
127
|
+
await installDependencies(data.dependencies, {
|
|
128
|
+
label: `Installing dependencies for ${name}`,
|
|
129
|
+
successMessage: `Dependencies installed for ${name}`,
|
|
130
|
+
cwd,
|
|
131
|
+
});
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// 3. Write Files
|
|
135
|
+
for (const file of data.files) {
|
|
136
|
+
const target = path.join(cwd, config.paths.ui, file.path);
|
|
137
|
+
await fs.ensureDir(path.dirname(target));
|
|
138
|
+
await fs.writeFile(target, file.content);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
context.installed.add(name);
|
|
142
|
+
note(`Added ${name}`);
|
|
143
|
+
} finally {
|
|
144
|
+
context.installing.delete(name);
|
|
105
145
|
}
|
|
146
|
+
}
|
|
106
147
|
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
await
|
|
148
|
+
export async function addAllComponents(config: Config, cwd = process.cwd()) {
|
|
149
|
+
const all = await loadRegistryIndex(cwd);
|
|
150
|
+
const context = createInstallContext();
|
|
151
|
+
for (const name of all) {
|
|
152
|
+
await installComponent(name, config, context, cwd);
|
|
112
153
|
}
|
|
113
|
-
note(`Added ${name}`);
|
|
114
154
|
}
|
|
115
155
|
|
|
116
156
|
export async function addCommand(components: string[], config: Config) {
|
|
@@ -119,7 +159,8 @@ export async function addCommand(components: string[], config: Config) {
|
|
|
119
159
|
selections = await pickComponents();
|
|
120
160
|
}
|
|
121
161
|
|
|
162
|
+
const context = createInstallContext();
|
|
122
163
|
for (const name of selections) {
|
|
123
|
-
await installComponent(name, config);
|
|
164
|
+
await installComponent(name, config, context, process.cwd());
|
|
124
165
|
}
|
|
125
166
|
}
|
package/src/commands/cli.ts
CHANGED
|
@@ -3,25 +3,72 @@ import { Command } from "commander";
|
|
|
3
3
|
|
|
4
4
|
import { getConfig } from "@/utils/cli-utils";
|
|
5
5
|
|
|
6
|
-
import { addCommand } from "./add";
|
|
6
|
+
import { addAllComponents, addCommand } from "./add";
|
|
7
|
+
import { createCommand } from "./create";
|
|
7
8
|
import { initCommand } from "./init";
|
|
9
|
+
import { paletteSetCommand } from "./palette";
|
|
8
10
|
|
|
9
11
|
const program = new Command();
|
|
10
12
|
|
|
11
|
-
program.name("shapes").description("Shapes UI CLI").version("0.0.1");
|
|
13
|
+
program.name("shapes-ui").description("Shapes UI CLI").version("0.0.1");
|
|
12
14
|
|
|
13
15
|
program.command("init").description("Configure Shapes UI for your project").action(initCommand);
|
|
14
16
|
|
|
17
|
+
program
|
|
18
|
+
.command("create [project-name]")
|
|
19
|
+
.description("Create a new Vite React TypeScript app ready for Shapes UI")
|
|
20
|
+
.option("-i, --install", "Install project dependencies")
|
|
21
|
+
.option("--full", "Configure Shapes UI and install all components")
|
|
22
|
+
.option("--style <style>", "Shapes style for --full (default|brutalist|minimal)")
|
|
23
|
+
.option("--palette <name>", "Brand palette for --full (e.g. blue, emerald, rose)")
|
|
24
|
+
.option("--contrast-mode <mode>", "Foreground contrast mode for --full (deterministic|dynamic)")
|
|
25
|
+
.option("--ui-path <path>", "UI components path for --full (e.g. ./src/components/ui)")
|
|
26
|
+
.option("--css-path <path>", "Tailwind CSS file path for --full (e.g. src/index.css)")
|
|
27
|
+
.option("-f, --force", "Overwrite target directory if it already exists")
|
|
28
|
+
.action(async (projectName, options) => {
|
|
29
|
+
await createCommand(projectName, {
|
|
30
|
+
install: options.install,
|
|
31
|
+
full: options.full,
|
|
32
|
+
style: options.style,
|
|
33
|
+
palette: options.palette,
|
|
34
|
+
contrastMode: options.contrastMode,
|
|
35
|
+
uiPath: options.uiPath,
|
|
36
|
+
cssPath: options.cssPath,
|
|
37
|
+
force: options.force,
|
|
38
|
+
});
|
|
39
|
+
});
|
|
40
|
+
|
|
15
41
|
program
|
|
16
42
|
.command("add [components...]")
|
|
17
43
|
.description("Add components to your project")
|
|
18
|
-
.
|
|
44
|
+
.option("-a, --all", "Add all available components")
|
|
45
|
+
.action(async (components, options) => {
|
|
19
46
|
const config = await getConfig();
|
|
20
47
|
if (!config) {
|
|
21
48
|
console.error("Please run 'init' first.");
|
|
22
49
|
return;
|
|
23
50
|
}
|
|
51
|
+
if (options.all) {
|
|
52
|
+
await addAllComponents(config);
|
|
53
|
+
return;
|
|
54
|
+
}
|
|
24
55
|
await addCommand(components, config);
|
|
25
56
|
});
|
|
26
57
|
|
|
58
|
+
program
|
|
59
|
+
.command("palette")
|
|
60
|
+
.description("Manage color palettes")
|
|
61
|
+
.command("set <name>")
|
|
62
|
+
.description("Set brand palette and regenerate semantic tokens")
|
|
63
|
+
.option("--css-path <path>", "Override Tailwind CSS file path")
|
|
64
|
+
.option("--contrast-mode <mode>", "Foreground contrast mode (deterministic|dynamic)")
|
|
65
|
+
.option("--refresh", "Refresh Tailwind palette data from docs")
|
|
66
|
+
.action(async (name, options) => {
|
|
67
|
+
await paletteSetCommand(name, {
|
|
68
|
+
cssPath: options.cssPath,
|
|
69
|
+
contrastMode: options.contrastMode,
|
|
70
|
+
refresh: options.refresh,
|
|
71
|
+
});
|
|
72
|
+
});
|
|
73
|
+
|
|
27
74
|
program.parse();
|
|
@@ -0,0 +1,262 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
|
|
3
|
+
import { confirm, intro, note, outro, spinner, text } from "@clack/prompts";
|
|
4
|
+
import { execa } from "execa";
|
|
5
|
+
import fs from "fs-extra";
|
|
6
|
+
import pc from "picocolors";
|
|
7
|
+
|
|
8
|
+
import { addAllComponents } from "@/commands/add";
|
|
9
|
+
import {
|
|
10
|
+
exitIfCancelled,
|
|
11
|
+
getMissingDeps,
|
|
12
|
+
isTailwindV4Installed,
|
|
13
|
+
isViteProject,
|
|
14
|
+
readPackageJson,
|
|
15
|
+
} from "@/utils/cli-utils";
|
|
16
|
+
import { installDependencies } from "@/utils/dependency-installer";
|
|
17
|
+
import { getPackageManager } from "@/utils/package-manager";
|
|
18
|
+
import {
|
|
19
|
+
type ContrastMode,
|
|
20
|
+
getDefaultBrandPaletteOptions,
|
|
21
|
+
normalizeContrastMode,
|
|
22
|
+
writePaletteTokens,
|
|
23
|
+
} from "@/utils/palette";
|
|
24
|
+
import type { Config } from "@/utils/schema";
|
|
25
|
+
|
|
26
|
+
type CreateOptions = {
|
|
27
|
+
install?: boolean;
|
|
28
|
+
force?: boolean;
|
|
29
|
+
full?: boolean;
|
|
30
|
+
style?: Config["style"];
|
|
31
|
+
palette?: string;
|
|
32
|
+
contrastMode?: ContrastMode | string;
|
|
33
|
+
uiPath?: string;
|
|
34
|
+
cssPath?: string;
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
const BASE_DEPS = [
|
|
38
|
+
"@base-ui/react",
|
|
39
|
+
"class-variance-authority",
|
|
40
|
+
"clsx",
|
|
41
|
+
"lucide-react",
|
|
42
|
+
"tailwind-merge",
|
|
43
|
+
"tw-animate-css",
|
|
44
|
+
];
|
|
45
|
+
|
|
46
|
+
async function ensureTailwindStyles(cwd: string, cssPath: string) {
|
|
47
|
+
const template = '@import "tailwindcss";\n';
|
|
48
|
+
const absPath = path.join(cwd, cssPath);
|
|
49
|
+
await fs.ensureDir(path.dirname(absPath));
|
|
50
|
+
|
|
51
|
+
if (await fs.pathExists(absPath)) {
|
|
52
|
+
const current = await fs.readFile(absPath, "utf-8");
|
|
53
|
+
if (current.includes("tailwindcss") || current.includes("@tailwind")) return;
|
|
54
|
+
await fs.writeFile(absPath, `${current.trimEnd()}\n\n${template}`);
|
|
55
|
+
return;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
await fs.writeFile(absPath, template);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
async function setupShapesFull(projectDir: string, options: CreateOptions) {
|
|
62
|
+
const setupSpin = spinner();
|
|
63
|
+
setupSpin.start("Configuring Shapes UI");
|
|
64
|
+
|
|
65
|
+
const cssPath = options.cssPath ?? "src/index.css";
|
|
66
|
+
const uiPath = options.uiPath ?? "./src/components/ui";
|
|
67
|
+
const style = options.style ?? "default";
|
|
68
|
+
const palette = options.palette ?? "blue";
|
|
69
|
+
const contrastMode = options.contrastMode ?? "deterministic";
|
|
70
|
+
const pkg = await readPackageJson(projectDir);
|
|
71
|
+
const hasTailwindV4 = isTailwindV4Installed(pkg);
|
|
72
|
+
const isVite = await isViteProject(projectDir);
|
|
73
|
+
|
|
74
|
+
if (!hasTailwindV4) {
|
|
75
|
+
const tailwindDeps = ["tailwindcss"];
|
|
76
|
+
if (isVite) {
|
|
77
|
+
tailwindDeps.push("@tailwindcss/vite");
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
await installDependencies(tailwindDeps, {
|
|
81
|
+
dev: true,
|
|
82
|
+
label: "Installing Tailwind dependencies",
|
|
83
|
+
successMessage: "Tailwind dependencies installed",
|
|
84
|
+
cwd: projectDir,
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
const latestPkg = await readPackageJson(projectDir);
|
|
89
|
+
const missingBaseDeps = getMissingDeps(latestPkg, BASE_DEPS);
|
|
90
|
+
await installDependencies(missingBaseDeps, {
|
|
91
|
+
label: "Installing Shapes base dependencies",
|
|
92
|
+
successMessage: "Shapes base dependencies installed",
|
|
93
|
+
cwd: projectDir,
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
await ensureTailwindStyles(projectDir, cssPath);
|
|
97
|
+
|
|
98
|
+
const paletteResult = await writePaletteTokens({
|
|
99
|
+
cwd: projectDir,
|
|
100
|
+
cssPath,
|
|
101
|
+
paletteName: palette,
|
|
102
|
+
neutralPalette: "zinc",
|
|
103
|
+
contrastMode,
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
const config: Config = {
|
|
107
|
+
$schema: "https://shapes-ui.com/schema.json",
|
|
108
|
+
style,
|
|
109
|
+
palette: {
|
|
110
|
+
name: paletteResult.paletteName,
|
|
111
|
+
contrastMode: paletteResult.contrastMode,
|
|
112
|
+
},
|
|
113
|
+
tailwind: { css: cssPath, baseColor: "zinc" },
|
|
114
|
+
paths: { ui: uiPath, lib: "./src/lib" },
|
|
115
|
+
};
|
|
116
|
+
|
|
117
|
+
await fs.writeJSON(path.join(projectDir, "shapes.json"), config, { spaces: 2 });
|
|
118
|
+
await addAllComponents(config, projectDir);
|
|
119
|
+
|
|
120
|
+
setupSpin.stop("Shapes UI configured with all components");
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function getCreateCommand(packageManager: string, projectName: string) {
|
|
124
|
+
if (packageManager === "pnpm") {
|
|
125
|
+
return ["pnpm", ["create", "vite", projectName, "--template", "react-ts"]] as const;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
if (packageManager === "yarn") {
|
|
129
|
+
return ["yarn", ["create", "vite", projectName, "--template", "react-ts"]] as const;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
if (packageManager === "bun") {
|
|
133
|
+
return ["bun", ["create", "vite", projectName, "--template", "react-ts"]] as const;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
return ["npm", ["create", "vite@latest", projectName, "--", "--template", "react-ts"]] as const;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
function getProjectInstallCommand(packageManager: string) {
|
|
140
|
+
if (packageManager === "pnpm") {
|
|
141
|
+
return ["pnpm", ["install"]] as const;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
if (packageManager === "yarn") {
|
|
145
|
+
return ["yarn", ["install"]] as const;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
if (packageManager === "bun") {
|
|
149
|
+
return ["bun", ["install"]] as const;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
return ["npm", ["install"]] as const;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
export async function createCommand(projectNameArg?: string, options: CreateOptions = {}) {
|
|
156
|
+
intro(pc.bgCyan(pc.black(" Create a Shapes UI app ")));
|
|
157
|
+
|
|
158
|
+
const allowedStyles = new Set<Config["style"]>(["default", "brutalist", "minimal"]);
|
|
159
|
+
if (options.style && !allowedStyles.has(options.style)) {
|
|
160
|
+
throw new Error(`Invalid style "${options.style}". Use one of: default, brutalist, minimal.`);
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
if (options.palette) {
|
|
164
|
+
const normalizedPalette = options.palette.toLowerCase();
|
|
165
|
+
const allowedPalettes = new Set<string>(getDefaultBrandPaletteOptions());
|
|
166
|
+
if (!allowedPalettes.has(normalizedPalette)) {
|
|
167
|
+
throw new Error(
|
|
168
|
+
`Invalid palette "${options.palette}". Use one of: ${getDefaultBrandPaletteOptions().join(", ")}.`,
|
|
169
|
+
);
|
|
170
|
+
}
|
|
171
|
+
options.palette = normalizedPalette;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
if (options.contrastMode) {
|
|
175
|
+
options.contrastMode = normalizeContrastMode(options.contrastMode);
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
if (options.uiPath !== undefined && !options.uiPath.trim()) {
|
|
179
|
+
throw new Error("Invalid ui path. Please provide a non-empty value.");
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
if (options.cssPath !== undefined && !options.cssPath.trim()) {
|
|
183
|
+
throw new Error("Invalid css path. Please provide a non-empty value.");
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
const projectName =
|
|
187
|
+
projectNameArg && projectNameArg.trim().length > 0
|
|
188
|
+
? projectNameArg.trim()
|
|
189
|
+
: (
|
|
190
|
+
exitIfCancelled(
|
|
191
|
+
await text({
|
|
192
|
+
message: "Project name",
|
|
193
|
+
initialValue: "my-shapes-app",
|
|
194
|
+
validate(value) {
|
|
195
|
+
if (!value || !value.trim()) return "Project name is required.";
|
|
196
|
+
return undefined;
|
|
197
|
+
},
|
|
198
|
+
}),
|
|
199
|
+
) as string
|
|
200
|
+
).trim();
|
|
201
|
+
|
|
202
|
+
const projectDir = path.resolve(process.cwd(), projectName);
|
|
203
|
+
const exists = await fs.pathExists(projectDir);
|
|
204
|
+
|
|
205
|
+
if (exists) {
|
|
206
|
+
const overwrite =
|
|
207
|
+
options.force ??
|
|
208
|
+
exitIfCancelled(
|
|
209
|
+
await confirm({
|
|
210
|
+
message: `${projectName} already exists. Overwrite it?`,
|
|
211
|
+
initialValue: false,
|
|
212
|
+
}),
|
|
213
|
+
);
|
|
214
|
+
|
|
215
|
+
if (!overwrite) {
|
|
216
|
+
outro(pc.yellow("Create cancelled."));
|
|
217
|
+
return;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
await fs.remove(projectDir);
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
const packageManager = await getPackageManager();
|
|
224
|
+
const [createCommandName, createArgs] = getCreateCommand(packageManager, projectName);
|
|
225
|
+
|
|
226
|
+
const createSpin = spinner();
|
|
227
|
+
createSpin.start(`Scaffolding ${projectName}`);
|
|
228
|
+
|
|
229
|
+
await execa(createCommandName, createArgs, { stdio: "ignore" });
|
|
230
|
+
createSpin.stop(`Created ${projectName}`);
|
|
231
|
+
|
|
232
|
+
note(`Project scaffolded with ${packageManager} + Vite (React + TypeScript).`, "Create");
|
|
233
|
+
|
|
234
|
+
const shouldInstallProjectDeps = options.install || options.full;
|
|
235
|
+
|
|
236
|
+
if (shouldInstallProjectDeps) {
|
|
237
|
+
const [installCommand, installArgs] = getProjectInstallCommand(packageManager);
|
|
238
|
+
const installSpin = spinner();
|
|
239
|
+
installSpin.start("Installing project dependencies");
|
|
240
|
+
|
|
241
|
+
await execa(installCommand, installArgs, {
|
|
242
|
+
cwd: projectDir,
|
|
243
|
+
stdio: "ignore",
|
|
244
|
+
});
|
|
245
|
+
|
|
246
|
+
installSpin.stop("Dependencies installed");
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
if (options.full) {
|
|
250
|
+
await setupShapesFull(projectDir, options);
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
const runShapesInit = `${packageManager === "npm" ? "npx" : packageManager === "pnpm" ? "pnpm dlx" : packageManager === "yarn" ? "yarn dlx" : "bunx"} shapes-ui init`;
|
|
254
|
+
|
|
255
|
+
outro(
|
|
256
|
+
`${pc.green(options.full ? "Project ready with Shapes UI." : "Project ready.")}\n\n` +
|
|
257
|
+
`${pc.bold("Next steps:")}\n` +
|
|
258
|
+
` cd ${projectName}\n` +
|
|
259
|
+
` ${shouldInstallProjectDeps ? "" : `${packageManager} install\n `}${options.full ? "" : `${runShapesInit}\n `}` +
|
|
260
|
+
` ${packageManager} run dev`,
|
|
261
|
+
);
|
|
262
|
+
}
|