shapes-ui 0.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (117) hide show
  1. package/.changeset/README.md +8 -0
  2. package/.changeset/config.json +11 -0
  3. package/.cta.json +12 -0
  4. package/.github/workflows/release.yml +44 -0
  5. package/.oxfmtrc.json +21 -0
  6. package/.oxlintrc.json +40 -0
  7. package/.vscode/settings.json +11 -0
  8. package/CHANGELOG.md +22 -0
  9. package/LICENSE +21 -0
  10. package/README.md +1 -0
  11. package/content/components/accordion.mdx +41 -0
  12. package/content/components/alert-dialog.mdx +25 -0
  13. package/content/components/alert.mdx +53 -0
  14. package/content/components/badge.mdx +27 -0
  15. package/content/components/button.mdx +55 -0
  16. package/content-collections.ts +67 -0
  17. package/dist/cli.d.ts +1 -0
  18. package/dist/cli.js +15255 -0
  19. package/dist/client/.assetsignore +2 -0
  20. package/dist/server/.vite/manifest.json +2435 -0
  21. package/examples/__index.tsx +201 -0
  22. package/examples/accordion-demo.tsx +30 -0
  23. package/examples/accordion-icon.tsx +40 -0
  24. package/examples/accordion-multiple.tsx +27 -0
  25. package/examples/accordion-small.tsx +30 -0
  26. package/examples/accordion-surface.tsx +30 -0
  27. package/examples/alert-action.tsx +18 -0
  28. package/examples/alert-demo.tsx +10 -0
  29. package/examples/alert-description.tsx +16 -0
  30. package/examples/alert-destructive.tsx +13 -0
  31. package/examples/alert-dialog-demo.tsx +33 -0
  32. package/examples/alert-dialog-destructive.tsx +33 -0
  33. package/examples/alert-dialog-icon.tsx +38 -0
  34. package/examples/alert-info.tsx +13 -0
  35. package/examples/alert-link.tsx +21 -0
  36. package/examples/alert-success.tsx +13 -0
  37. package/examples/alert-title.tsx +12 -0
  38. package/examples/alert-warning.tsx +13 -0
  39. package/examples/badge-demo.tsx +5 -0
  40. package/examples/badge-status-icon.tsx +26 -0
  41. package/examples/badge-status.tsx +12 -0
  42. package/examples/badge-variants.tsx +12 -0
  43. package/examples/button-default.tsx +15 -0
  44. package/examples/button-demo.tsx +14 -0
  45. package/examples/button-destructive.tsx +15 -0
  46. package/examples/button-ghost.tsx +15 -0
  47. package/examples/button-info.tsx +15 -0
  48. package/examples/button-link.tsx +15 -0
  49. package/examples/button-loading.tsx +14 -0
  50. package/examples/button-outline.tsx +15 -0
  51. package/examples/button-sizes.tsx +34 -0
  52. package/examples/button-success.tsx +15 -0
  53. package/examples/button-warning.tsx +15 -0
  54. package/package.json +81 -0
  55. package/public/apple-touch-icon.png +0 -0
  56. package/public/circuit-board.svg +1 -0
  57. package/public/favicon-16x16.png +0 -0
  58. package/public/favicon-32x32.png +0 -0
  59. package/public/favicon.ico +0 -0
  60. package/public/favicon.svg +3 -0
  61. package/public/logo192.png +0 -0
  62. package/public/logo512.png +0 -0
  63. package/public/manifest.json +25 -0
  64. package/public/r/accordion.json +16 -0
  65. package/public/r/alert-dialog.json +17 -0
  66. package/public/r/alert.json +15 -0
  67. package/public/r/badge.json +15 -0
  68. package/public/r/button.json +16 -0
  69. package/public/r/index.json +7 -0
  70. package/public/robots.txt +3 -0
  71. package/public/shps_black.png +0 -0
  72. package/public/shps_black.svg +3 -0
  73. package/public/shps_white.png +0 -0
  74. package/public/shps_white.svg +3 -0
  75. package/scripts/generate-examples.mts +118 -0
  76. package/scripts/generate-registry.mts +129 -0
  77. package/src/commands/add.ts +125 -0
  78. package/src/commands/cli.ts +27 -0
  79. package/src/commands/init.ts +128 -0
  80. package/src/components/docs/docs-button.tsx +60 -0
  81. package/src/components/docs/layout/circuit-board.tsx +22 -0
  82. package/src/components/docs/layout/footer.tsx +11 -0
  83. package/src/components/docs/layout/header.tsx +49 -0
  84. package/src/components/docs/layout/mobile-menu.tsx +86 -0
  85. package/src/components/docs/layout/nav-list.tsx +43 -0
  86. package/src/components/docs/layout/page-header.tsx +33 -0
  87. package/src/components/docs/layout/split-layout.tsx +21 -0
  88. package/src/components/docs/layout/suspense-fallback.tsx +12 -0
  89. package/src/components/docs/markdown/components.tsx +324 -0
  90. package/src/components/docs/markdown/installation-block.tsx +146 -0
  91. package/src/components/docs/markdown/render-preview.tsx +152 -0
  92. package/src/components/docs/theme-provider.tsx +48 -0
  93. package/src/components/ui/accordion.tsx +83 -0
  94. package/src/components/ui/alert-dialog.tsx +162 -0
  95. package/src/components/ui/alert.tsx +72 -0
  96. package/src/components/ui/badge.tsx +34 -0
  97. package/src/components/ui/button.tsx +64 -0
  98. package/src/lib/utils.ts +6 -0
  99. package/src/routeTree.gen.ts +131 -0
  100. package/src/router.tsx +17 -0
  101. package/src/routes/__root.tsx +72 -0
  102. package/src/routes/components.$slug.tsx +62 -0
  103. package/src/routes/components.index.tsx +55 -0
  104. package/src/routes/components.tsx +15 -0
  105. package/src/routes/index.tsx +16 -0
  106. package/src/styles/globals.css +66 -0
  107. package/src/styles/styles.css +113 -0
  108. package/src/types/registry-item.ts +13 -0
  109. package/src/utils/cli-utils.ts +46 -0
  110. package/src/utils/package-manager.ts +27 -0
  111. package/src/utils/schema.ts +25 -0
  112. package/tests/generate-registry.test.ts +60 -0
  113. package/tsconfig.json +29 -0
  114. package/tsup.config.ts +11 -0
  115. package/vite.config.ts +31 -0
  116. package/vitest.config.ts +8 -0
  117. package/wrangler.jsonc +7 -0
@@ -0,0 +1,129 @@
1
+ import fs from "node:fs/promises";
2
+ import path from "node:path";
3
+ import { fileURLToPath } from "node:url";
4
+
5
+ import ts from "typescript";
6
+
7
+ import type { RegistryItem } from "../src/types/registry-item";
8
+
9
+ const __filename = fileURLToPath(import.meta.url);
10
+ const __dirname = path.dirname(__filename);
11
+
12
+ const workspaceRoot = path.resolve(__dirname, "..");
13
+ const uiDir = path.join(workspaceRoot, "src", "components", "ui");
14
+ const outputDir = path.join(workspaceRoot, "public", "r");
15
+
16
+ type DependencyInfo = {
17
+ dependencies: string[];
18
+ registryDependencies: string[];
19
+ };
20
+
21
+ export function inferDependencies(options: {
22
+ name: string;
23
+ content: string;
24
+ filePath: string;
25
+ uiDir: string;
26
+ }): DependencyInfo {
27
+ const { name, content, filePath, uiDir } = options;
28
+ const sourceFile = ts.createSourceFile(
29
+ filePath,
30
+ content,
31
+ ts.ScriptTarget.ESNext,
32
+ true,
33
+ ts.ScriptKind.TSX,
34
+ );
35
+
36
+ const importSpecifiers = new Set<string>();
37
+ sourceFile.forEachChild((node) => {
38
+ if (ts.isImportDeclaration(node) && ts.isStringLiteral(node.moduleSpecifier)) {
39
+ importSpecifiers.add(node.moduleSpecifier.text);
40
+ }
41
+ });
42
+
43
+ const dependencies = new Set<string>();
44
+ const registryDependencies = new Set<string>();
45
+
46
+ for (const spec of importSpecifiers) {
47
+ if (spec.startsWith("@/")) {
48
+ if (spec.startsWith("@/components/ui/")) {
49
+ const depName = spec.replace("@/components/ui/", "").split("/")[0];
50
+ if (depName && depName !== name) registryDependencies.add(depName);
51
+ }
52
+ continue;
53
+ }
54
+
55
+ if (spec.startsWith("./") || spec.startsWith("../")) {
56
+ const resolved = path.resolve(path.dirname(filePath), spec);
57
+ if (resolved.startsWith(uiDir)) {
58
+ const depName = path.basename(resolved).replace(/\.(t|j)sx?$/, "");
59
+ if (depName && depName !== name && depName !== "index") {
60
+ registryDependencies.add(depName);
61
+ }
62
+ }
63
+ continue;
64
+ }
65
+
66
+ const packageName = spec.startsWith("@")
67
+ ? spec.split("/").slice(0, 2).join("/")
68
+ : spec.split("/")[0];
69
+
70
+ if (packageName && packageName !== "react" && packageName !== "react-dom") {
71
+ dependencies.add(packageName);
72
+ }
73
+ }
74
+
75
+ return {
76
+ dependencies: Array.from(dependencies),
77
+ registryDependencies: Array.from(registryDependencies),
78
+ };
79
+ }
80
+
81
+ async function generate() {
82
+ await fs.mkdir(outputDir, { recursive: true });
83
+
84
+ const files = await fs.readdir(uiDir);
85
+ const componentFiles = files.filter((f) => f.endsWith(".tsx"));
86
+
87
+ const registryIndex: string[] = [];
88
+
89
+ for (const file of componentFiles) {
90
+ const name = file.replace(".tsx", "");
91
+ const content = await fs.readFile(path.join(uiDir, file), "utf-8");
92
+
93
+ const { dependencies, registryDependencies } = inferDependencies({
94
+ name,
95
+ content,
96
+ filePath: path.join(uiDir, file),
97
+ uiDir,
98
+ });
99
+
100
+ const registryItem: RegistryItem = {
101
+ name,
102
+ type: "registry:ui",
103
+ dependencies,
104
+ registryDependencies,
105
+ files: [
106
+ {
107
+ path: `${name}.tsx`,
108
+ content,
109
+ type: "registry:ui",
110
+ },
111
+ ],
112
+ };
113
+
114
+ await fs.writeFile(path.join(outputDir, `${name}.json`), JSON.stringify(registryItem, null, 2));
115
+
116
+ registryIndex.push(name);
117
+ console.log(`✅ Generated ${name}.json`);
118
+ }
119
+
120
+ await fs.writeFile(path.join(outputDir, "index.json"), JSON.stringify(registryIndex, null, 2));
121
+ console.log("✅ Generated index.json");
122
+ }
123
+
124
+ const entryFile = process.argv[1] ? path.resolve(process.argv[1]) : "";
125
+ const isDirectRun = fileURLToPath(import.meta.url) === entryFile;
126
+
127
+ if (isDirectRun) {
128
+ generate().catch(console.error);
129
+ }
@@ -0,0 +1,125 @@
1
+ import path from "node:path";
2
+
3
+ import { cancel, multiselect, note, spinner } from "@clack/prompts";
4
+ import { execa } from "execa";
5
+ import fs from "fs-extra";
6
+
7
+ import type { RegistryItem } from "@/types/registry-item";
8
+ import { exitIfCancelled } from "@/utils/cli-utils";
9
+ import { getInstallCommand } from "@/utils/package-manager";
10
+ import type { Config } from "@/utils/schema";
11
+
12
+ const REGISTRY_URL = "https://shapes-ui.com/r";
13
+
14
+ export async function loadRegistryIndex() {
15
+ const localRegistryDir = path.resolve(process.cwd(), "public/r");
16
+ if (await fs.pathExists(localRegistryDir)) {
17
+ const files = await fs.readdir(localRegistryDir);
18
+ const entries: RegistryItem[] = [];
19
+
20
+ for (const file of files) {
21
+ if (!file.endsWith(".json") || file === "index.json") continue;
22
+ const data = await fs.readJSON(path.join(localRegistryDir, file));
23
+ if (data?.name) entries.push(data);
24
+ }
25
+
26
+ return entries.map((entry) => entry.name).sort();
27
+ }
28
+
29
+ const res = await fetch(`${REGISTRY_URL}/index.json`);
30
+ if (!res.ok) throw new Error("Registry index not found.");
31
+ const data = await res.json();
32
+
33
+ if (Array.isArray(data) && data.every((item) => typeof item === "string")) {
34
+ return data.sort();
35
+ }
36
+
37
+ if (Array.isArray(data)) {
38
+ return data
39
+ .map((item) => item?.name)
40
+ .filter((name) => typeof name === "string")
41
+ .sort();
42
+ }
43
+
44
+ return [];
45
+ }
46
+
47
+ export async function pickComponents() {
48
+ let components: string[] = [];
49
+ try {
50
+ components = await loadRegistryIndex();
51
+ } catch (error) {
52
+ cancel(error instanceof Error ? error.message : "Failed to load registry.");
53
+ process.exit(1);
54
+ }
55
+
56
+ if (!components.length) {
57
+ note("No components found in the registry.");
58
+ return [] as string[];
59
+ }
60
+
61
+ const selected = exitIfCancelled(
62
+ await multiselect({
63
+ message: "Select components to add",
64
+ options: components.map((name) => ({ label: name, value: name })),
65
+ }),
66
+ );
67
+
68
+ return selected as string[];
69
+ }
70
+
71
+ export async function installComponent(name: string, config: Config) {
72
+ const spin = spinner();
73
+ spin.start(`Fetching ${name}`);
74
+
75
+ let data: RegistryItem;
76
+
77
+ const localFile = path.resolve(process.cwd(), `public/r/${name}.json`);
78
+ if (await fs.pathExists(localFile)) {
79
+ data = await fs.readJSON(localFile);
80
+ } else {
81
+ const res = await fetch(`${REGISTRY_URL}/${name}.json`);
82
+ if (!res.ok) {
83
+ spin.stop("Fetch failed");
84
+ throw new Error(`Component ${name} not found in registry.`);
85
+ }
86
+ data = await res.json();
87
+ }
88
+
89
+ spin.stop(`Fetched ${name}`);
90
+
91
+ // 1. Recursive Install of Registry Dependencies
92
+ if (data.registryDependencies) {
93
+ for (const dep of data.registryDependencies) {
94
+ const depPath = path.join(process.cwd(), config.paths.ui, `${dep}.tsx`);
95
+ if (!fs.existsSync(depPath)) {
96
+ await installComponent(dep, config);
97
+ }
98
+ }
99
+ }
100
+
101
+ // 2. Install NPM Dependencies
102
+ if (data.dependencies?.length) {
103
+ const [command, ...args] = await getInstallCommand(data.dependencies);
104
+ await execa(command, args);
105
+ }
106
+
107
+ // 3. Write Files
108
+ for (const file of data.files) {
109
+ const target = path.join(process.cwd(), config.paths.ui, file.path);
110
+ await fs.ensureDir(path.dirname(target));
111
+ await fs.writeFile(target, file.content);
112
+ }
113
+ note(`Added ${name}`);
114
+ }
115
+
116
+ export async function addCommand(components: string[], config: Config) {
117
+ let selections = components;
118
+ if (!selections?.length) {
119
+ selections = await pickComponents();
120
+ }
121
+
122
+ for (const name of selections) {
123
+ await installComponent(name, config);
124
+ }
125
+ }
@@ -0,0 +1,27 @@
1
+ #!/usr/bin/env node
2
+ import { Command } from "commander";
3
+
4
+ import { getConfig } from "@/utils/cli-utils";
5
+
6
+ import { addCommand } from "./add";
7
+ import { initCommand } from "./init";
8
+
9
+ const program = new Command();
10
+
11
+ program.name("shapes").description("Shapes UI CLI").version("0.0.1");
12
+
13
+ program.command("init").description("Configure Shapes UI for your project").action(initCommand);
14
+
15
+ program
16
+ .command("add [components...]")
17
+ .description("Add components to your project")
18
+ .action(async (components) => {
19
+ const config = await getConfig();
20
+ if (!config) {
21
+ console.error("Please run 'init' first.");
22
+ return;
23
+ }
24
+ await addCommand(components, config);
25
+ });
26
+
27
+ program.parse();
@@ -0,0 +1,128 @@
1
+ import path from "node:path";
2
+
3
+ import { confirm, intro, note, outro, select, text, spinner } from "@clack/prompts";
4
+ import { execa } from "execa";
5
+ import fs from "fs-extra";
6
+
7
+ import { pickComponents, installComponent } from "@/commands/add";
8
+ import {
9
+ exitIfCancelled,
10
+ getMissingDeps,
11
+ isTailwindV4Installed,
12
+ readPackageJson,
13
+ } from "@/utils/cli-utils";
14
+ import { getInstallCommand } from "@/utils/package-manager";
15
+ import { type Config } from "@/utils/schema";
16
+
17
+ const BASE_DEPS = [
18
+ "@base-ui/react",
19
+ "class-variance-authority",
20
+ "clsx",
21
+ "lucide-react",
22
+ "tailwind-merge",
23
+ "tw-animate-css",
24
+ ];
25
+ const TAILWIND_DEV_DEPS = ["tailwindcss", "@tailwindcss/vite"];
26
+
27
+ async function installDeps(deps: string[], dev = false) {
28
+ if (!deps.length) return;
29
+ const spin = spinner();
30
+ const label = dev ? "Installing Tailwind v4" : "Installing base dependencies";
31
+ spin.start(label);
32
+
33
+ const [command, ...args] = await getInstallCommand(deps, dev);
34
+ await execa(command, args);
35
+
36
+ spin.stop("Dependencies installed");
37
+ }
38
+
39
+ async function ensureTailwindStyles(cssPath: string) {
40
+ const template = '@import "tailwindcss";\n';
41
+ const absPath = path.join(process.cwd(), cssPath);
42
+ await fs.ensureDir(path.dirname(absPath));
43
+
44
+ if (await fs.pathExists(absPath)) {
45
+ const current = await fs.readFile(absPath, "utf-8");
46
+ if (current.includes("tailwindcss") || current.includes("@tailwind")) return;
47
+ await fs.writeFile(absPath, `${current.trimEnd()}\n\n${template}`);
48
+ return;
49
+ }
50
+
51
+ await fs.writeFile(absPath, template);
52
+ }
53
+
54
+ export async function initCommand() {
55
+ intro("Shapes UI");
56
+
57
+ const configPath = path.join(process.cwd(), "shapes.json");
58
+ if (await fs.pathExists(configPath)) {
59
+ const overwrite = exitIfCancelled(
60
+ await confirm({ message: "shapes.json already exists. Overwrite it?" }),
61
+ );
62
+ if (!overwrite) {
63
+ outro("Init cancelled.");
64
+ return;
65
+ }
66
+ }
67
+
68
+ const style = exitIfCancelled(
69
+ await select({
70
+ message: "Which style do you want to use?",
71
+ options: [
72
+ { label: "Default", value: "default" },
73
+ { label: "Brutalist", value: "brutalist" },
74
+ ],
75
+ }),
76
+ );
77
+
78
+ const uiPath = exitIfCancelled(
79
+ await text({
80
+ message: "Where should we install components?",
81
+ initialValue: "./src/components/ui",
82
+ }),
83
+ );
84
+
85
+ const pkg = await readPackageJson();
86
+ const hasTailwindV4 = isTailwindV4Installed(pkg);
87
+
88
+ const hasExistingCss = exitIfCancelled(
89
+ await confirm({ message: "Do you already have a CSS file?" }),
90
+ );
91
+
92
+ const cssPath = hasExistingCss
93
+ ? exitIfCancelled(
94
+ await text({
95
+ message: "Path to your CSS file",
96
+ initialValue: "src/styles/globals.css",
97
+ }),
98
+ )
99
+ : "src/styles/globals.css";
100
+
101
+ if (!hasTailwindV4) {
102
+ await installDeps(TAILWIND_DEV_DEPS, true);
103
+ }
104
+
105
+ const missingBaseDeps = getMissingDeps(pkg, BASE_DEPS);
106
+ await installDeps(missingBaseDeps, false);
107
+ await ensureTailwindStyles(cssPath);
108
+
109
+ const config: Config = {
110
+ $schema: "https://shapes-ui.com/schema.json",
111
+ style: style as Config["style"],
112
+ tailwind: { css: cssPath as string, baseColor: "zinc" },
113
+ paths: { ui: uiPath as string, lib: "./src/lib" },
114
+ };
115
+
116
+ await fs.writeJSON("shapes.json", config, { spaces: 2 });
117
+ note("Created shapes.json");
118
+
119
+ const addNow = exitIfCancelled(await confirm({ message: "Add components now?" }));
120
+ if (addNow) {
121
+ const selected = await pickComponents();
122
+ for (const name of selected) {
123
+ await installComponent(name, config);
124
+ }
125
+ }
126
+
127
+ outro("Shapes UI is ready.");
128
+ }
@@ -0,0 +1,60 @@
1
+ import { Button as ButtonPrimitive } from "@base-ui/react/button";
2
+ import { cva, type VariantProps } from "class-variance-authority";
3
+
4
+ import { cn } from "@/lib/utils";
5
+
6
+ const buttonVariants = cva(
7
+ "group/button inline-flex shrink-0 items-center justify-center border border-transparent bg-clip-padding text-sm font-medium whitespace-nowrap transition-all outline-none select-none focus-visible:border-ring focus-visible:ring-3 focus-visible:ring-ring/50 disabled:pointer-events-none disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-3 aria-invalid:ring-destructive/20 dark:aria-invalid:border-destructive/50 dark:aria-invalid:ring-destructive/40 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
8
+ {
9
+ variants: {
10
+ variant: {
11
+ default: "bg-primary text-primary-foreground [a]:hover:bg-primary/80",
12
+ outline:
13
+ "border-border bg-background hover:bg-muted hover:text-foreground aria-expanded:bg-muted aria-expanded:text-foreground dark:border-input dark:bg-input/30 dark:hover:bg-input/50",
14
+ secondary:
15
+ "bg-secondary text-secondary-foreground hover:bg-secondary/80 aria-expanded:bg-secondary aria-expanded:text-secondary-foreground",
16
+ ghost:
17
+ "hover:bg-muted hover:text-foreground aria-expanded:bg-muted aria-expanded:text-foreground dark:hover:bg-muted/50",
18
+ destructive:
19
+ "bg-destructive/10 text-destructive hover:bg-destructive/20 focus-visible:border-destructive/40 focus-visible:ring-destructive/20 dark:bg-destructive/20 dark:hover:bg-destructive/30 dark:focus-visible:ring-destructive/40",
20
+ success:
21
+ "bg-success/10 text-success hover:bg-success/20 focus-visible:border-success/40 focus-visible:ring-success/20 dark:bg-success/20 dark:hover:bg-success/30 dark:focus-visible:ring-success/40",
22
+ warning:
23
+ "bg-warning/10 text-warning hover:bg-warning/20 focus-visible:border-warning/40 focus-visible:ring-warning/20 dark:bg-warning/20 dark:hover:bg-warning/30 dark:focus-visible:ring-warning/40",
24
+ info: "bg-info/10 text-info hover:bg-info/20 focus-visible:border-info/40 focus-visible:ring-info/20 dark:bg-info/20 dark:hover:bg-info/30 dark:focus-visible:ring-info/40",
25
+ link: "text-primary underline-offset-4 hover:underline",
26
+ },
27
+ size: {
28
+ default: "h-8 gap-1.5 px-2.5 has-data-[icon=end]:pr-2 has-data-[icon=start]:pl-2",
29
+ xs: "r h-6 gap-1 px-2 text-xs has-data-[icon=end]:pr-1.5 has-data-[icon=start]:pl-1.5 [&_svg:not([class*='size-'])]:size-3",
30
+ sm: "h-7 gap-1 px-2.5 text-[0.8rem] has-data-[icon=end]:pr-1.5 has-data-[icon=start]:pl-1.5 [&_svg:not([class*='size-'])]:size-3.5",
31
+ lg: "h-9 gap-1.5 px-2.5 has-data-[icon=end]:pr-3 has-data-[icon=start]:pl-3",
32
+ icon: "size-8",
33
+ "icon-xs": "r size-6 [&_svg:not([class*='size-'])]:size-3",
34
+ "icon-sm": "size-7 ",
35
+ "icon-lg": "size-9",
36
+ },
37
+ },
38
+ defaultVariants: {
39
+ variant: "default",
40
+ size: "default",
41
+ },
42
+ },
43
+ );
44
+
45
+ function DocsButton({
46
+ className,
47
+ variant = "default",
48
+ size = "default",
49
+ ...props
50
+ }: ButtonPrimitive.Props & VariantProps<typeof buttonVariants>) {
51
+ return (
52
+ <ButtonPrimitive
53
+ data-slot="button"
54
+ className={cn(buttonVariants({ variant, size, className }))}
55
+ {...props}
56
+ />
57
+ );
58
+ }
59
+
60
+ export { DocsButton, buttonVariants };
@@ -0,0 +1,22 @@
1
+ import React from "react";
2
+
3
+ export function CircuitBoard({ className, ...props }: React.SVGProps<SVGSVGElement>) {
4
+ return (
5
+ <svg
6
+ viewBox="0 0 304 304"
7
+ xmlns="http://www.w3.org/2000/svg"
8
+ width="100%"
9
+ height="100%"
10
+ preserveAspectRatio="xMidYMid slice"
11
+ className={`block ${className ?? ""}`}
12
+ {...props}
13
+ >
14
+ <path
15
+ fill="currentColor"
16
+ d="M44.1 224a5 5 0 1 1 0 2H0v-2h44.1zm160 48a5 5 0 1 1 0 2H82v-2h122.1zm57.8-46a5 5 0 1 1 0-2H304v2h-42.1zm0 16a5 5 0 1 1 0-2H304v2h-42.1zm6.2-114a5 5 0 1 1 0 2h-86.2a5 5 0 1 1 0-2h86.2zm-256-48a5 5 0 1 1 0 2H0v-2h12.1zm185.8 34a5 5 0 1 1 0-2h86.2a5 5 0 1 1 0 2h-86.2zM258 12.1a5 5 0 1 1-2 0V0h2v12.1zm-64 208a5 5 0 1 1-2 0v-54.2a5 5 0 1 1 2 0v54.2zm48-198.2V80h62v2h-64V21.9a5 5 0 1 1 2 0zm16 16V64h46v2h-48V37.9a5 5 0 1 1 2 0zm-128 96V208h16v12.1a5 5 0 1 1-2 0V210h-16v-76.1a5 5 0 1 1 2 0zm-5.9-21.9a5 5 0 1 1 0 2H114v48H85.9a5 5 0 1 1 0-2H112v-48h12.1zm-6.2 130a5 5 0 1 1 0-2H176v-74.1a5 5 0 1 1 2 0V242h-60.1zm-16-64a5 5 0 1 1 0-2H114v48h10.1a5 5 0 1 1 0 2H112v-48h-10.1zM66 284.1a5 5 0 1 1-2 0V274H50v30h-2v-32h18v12.1zM236.1 176a5 5 0 1 1 0 2H226v94h48v32h-2v-30h-48v-98h12.1zm25.8-30a5 5 0 1 1 0-2H274v44.1a5 5 0 1 1-2 0V146h-10.1zm-64 96a5 5 0 1 1 0-2H208v-80h16v-14h-42.1a5 5 0 1 1 0-2H226v18h-16v80h-12.1zm86.2-210a5 5 0 1 1 0 2H272V0h2v32h10.1zM98 101.9V146H53.9a5 5 0 1 1 0-2H96v-42.1a5 5 0 1 1 2 0zM53.9 34a5 5 0 1 1 0-2H80V0h2v34H53.9zm60.1 3.9V66H82v64H69.9a5 5 0 1 1 0-2H80V64h32V37.9a5 5 0 1 1 2 0zM101.9 82a5 5 0 1 1 0-2H128V37.9a5 5 0 1 1 2 0V82h-28.1zm16-64a5 5 0 1 1 0-2H146v44.1a5 5 0 1 1-2 0V18h-26.1zm102.2 270a5 5 0 1 1 0 2H98v14h-2v-16h124.1zM242 149.9V160h16v34h-16v62h48v48h-2v-46h-48v-66h16v-30h-16v-12.1a5 5 0 1 1 2 0zM53.9 18a5 5 0 1 1 0-2H64V2H48V0h18v18H53.9zm112 32a5 5 0 1 1 0-2H192V0h50v2h-48v48h-28.1zm-48-48a5 5 0 0 1-9.8-2h2.07a3 3 0 1 0 5.66 0H178v34h-18V21.9a5 5 0 1 1 2 0V32h14V2h-58.1zm0 96a5 5 0 1 1 0-2H137l32-32h39V21.9a5 5 0 1 1 2 0V66h-40.17l-32 32H117.9zm28.1 90.1a5 5 0 1 1-2 0v-76.51L175.59 80H224V21.9a5 5 0 1 1 2 0V82h-49.59L146 112.41v75.69zm16 32a5 5 0 1 1-2 0v-99.51L184.59 96H300.1a5 5 0 0 1 3.9-3.9v2.07a3 3 0 0 0 0 5.66v2.07a5 5 0 0 1-3.9-3.9H185.41L162 121.41v98.69zm-144-64a5 5 0 1 1-2 0v-3.51l48-48V48h32V0h2v50H66v55.41l-48 48v2.69zM50 53.9v43.51l-48 48V208h26.1a5 5 0 1 1 0 2H0v-65.41l48-48V53.9a5 5 0 1 1 2 0zm-16 16V89.41l-34 34v-2.82l32-32V69.9a5 5 0 1 1 2 0zM12.1 32a5 5 0 1 1 0 2H9.41L0 43.41V40.6L8.59 32h3.51zm265.8 18a5 5 0 1 1 0-2h18.69l7.41-7.41v2.82L297.41 50H277.9zm-16 160a5 5 0 1 1 0-2H288v-71.41l16-16v2.82l-14 14V210h-28.1zm-208 32a5 5 0 1 1 0-2H64v-22.59L40.59 194H21.9a5 5 0 1 1 0-2H41.41L66 216.59V242H53.9zm150.2 14a5 5 0 1 1 0 2H96v-56.6L56.6 162H37.9a5 5 0 1 1 0-2h19.5L98 200.6V256h106.1zm-150.2 2a5 5 0 1 1 0-2H80v-46.59L48.59 178H21.9a5 5 0 1 1 0-2H49.41L82 208.59V258H53.9zM34 39.8v1.61L9.41 66H0v-2h8.59L32 40.59V0h2v39.8zM2 300.1a5 5 0 0 1 3.9 3.9H3.83A3 3 0 0 0 0 302.17V256h18v48h-2v-46H2v42.1zM34 241v63h-2v-62H0v-2h34v1zM17 18H0v-2h16V0h2v18h-1zm273-2h14v2h-16V0h2v16zm-32 273v15h-2v-14h-14v14h-2v-16h18v1zM0 92.1A5.02 5.02 0 0 1 6 97a5 5 0 0 1-6 4.9v-2.07a3 3 0 1 0 0-5.66V92.1zM80 272h2v32h-2v-32zm37.9 32h-2.07a3 3 0 0 0-5.66 0h-2.07a5 5 0 0 1 9.8 0zM5.9 0A5.02 5.02 0 0 1 0 5.9V3.83A3 3 0 0 0 3.83 0H5.9zm294.2 0h2.07A3 3 0 0 0 304 3.83V5.9a5 5 0 0 1-3.9-5.9zm3.9 300.1v2.07a3 3 0 0 0-1.83 1.83h-2.07a5 5 0 0 1 3.9-3.9zM97 100a3 3 0 1 0 0-6 3 3 0 0 0 0 6zm0-16a3 3 0 1 0 0-6 3 3 0 0 0 0 6zm16 16a3 3 0 1 0 0-6 3 3 0 0 0 0 6zm16 16a3 3 0 1 0 0-6 3 3 0 0 0 0 6zm0 16a3 3 0 1 0 0-6 3 3 0 0 0 0 6zm-48 32a3 3 0 1 0 0-6 3 3 0 0 0 0 6zm16 16a3 3 0 1 0 0-6 3 3 0 0 0 0 6zm32 48a3 3 0 1 0 0-6 3 3 0 0 0 0 6zm-16 16a3 3 0 1 0 0-6 3 3 0 0 0 0 6zm32-16a3 3 0 1 0 0-6 3 3 0 0 0 0 6z"
17
+ />
18
+ </svg>
19
+ );
20
+ }
21
+
22
+ export default CircuitBoard;
@@ -0,0 +1,11 @@
1
+ import { Badge } from "@/components/ui/badge";
2
+
3
+ export function Footer() {
4
+ return (
5
+ <footer className=" h-12 border-t">
6
+ <div className=" container mx-auto flex h-full items-center border-x px-4">
7
+ <Badge variant={"info"}>Alpha</Badge>
8
+ </div>
9
+ </footer>
10
+ );
11
+ }
@@ -0,0 +1,49 @@
1
+ import { Link } from "@tanstack/react-router";
2
+ import { MenuIcon, SunMoonIcon } from "lucide-react";
3
+ import { useState } from "react";
4
+
5
+ import { DocsButton } from "@/components/docs/docs-button";
6
+ import { useTheme } from "@/components/docs/theme-provider";
7
+
8
+ import { MobileMenu } from "./mobile-menu";
9
+
10
+ export function Header() {
11
+ const { toggleTheme, theme } = useTheme();
12
+ const [showMobileMenu, setShowMobileMenu] = useState(false);
13
+
14
+ return (
15
+ <header className=" sticky top-0 z-10 h-12 border-b bg-background">
16
+ <div className=" container mx-auto flex h-full items-center justify-between border-x px-4">
17
+ <Link to="/">
18
+ <img
19
+ src={theme === "dark" ? "/shps_white.svg" : "/shps_black.svg"}
20
+ alt="Shapes UI Logo"
21
+ className=" size-6"
22
+ />
23
+ </Link>
24
+
25
+ <div className=" mx-auto hidden gap-4 lg:flex">
26
+ <DocsButton variant="ghost" size="sm">
27
+ <Link to="/components">Components</Link>
28
+ </DocsButton>
29
+ </div>
30
+
31
+ <DocsButton onClick={toggleTheme} variant={"ghost"} className={"hidden lg:inline-flex"}>
32
+ <SunMoonIcon className=" size-4" />
33
+ </DocsButton>
34
+
35
+ <DocsButton
36
+ onClick={() => setShowMobileMenu(!showMobileMenu)}
37
+ variant={"ghost"}
38
+ size={"sm"}
39
+ className={"lg:hidden"}
40
+ >
41
+ <MenuIcon data-icon="start" />
42
+ Navigation
43
+ </DocsButton>
44
+ </div>
45
+
46
+ <MobileMenu open={showMobileMenu} onClose={() => setShowMobileMenu(false)} />
47
+ </header>
48
+ );
49
+ }
@@ -0,0 +1,86 @@
1
+ import { Link } from "@tanstack/react-router";
2
+ import { clsx } from "clsx";
3
+ import { allComponents } from "content-collections";
4
+ import { Suspense, useEffect, useRef } from "react";
5
+
6
+ import { SuspenseFallback } from "./suspense-fallback";
7
+
8
+ export function MobileMenu({ open, onClose }: { open: boolean; onClose: () => void }) {
9
+ const panelRef = useRef<HTMLDivElement | null>(null);
10
+
11
+ useEffect(() => {
12
+ function onKey(e: KeyboardEvent) {
13
+ if (e.key === "Escape") onClose();
14
+ }
15
+
16
+ if (open) {
17
+ document.addEventListener("keydown", onKey);
18
+ document.body.style.overflow = "hidden";
19
+ // Move focus into the panel for accessibility
20
+ setTimeout(() => panelRef.current?.focus(), 0);
21
+ } else {
22
+ document.body.style.overflow = "";
23
+ }
24
+
25
+ return () => {
26
+ document.removeEventListener("keydown", onKey);
27
+ document.body.style.overflow = "";
28
+ };
29
+ }, [open, onClose]);
30
+
31
+ return (
32
+ <div aria-hidden={!open} className={clsx("fixed inset-0 z-50", !open && "pointer-events-none")}>
33
+ <div
34
+ className={clsx(
35
+ "fixed inset-0 bg-black/40 transition-opacity duration-200",
36
+ open ? "pointer-events-auto opacity-100" : "pointer-events-none opacity-0",
37
+ "motion-reduce:transition-none",
38
+ )}
39
+ onClick={onClose}
40
+ aria-hidden
41
+ />
42
+
43
+ <div
44
+ ref={panelRef}
45
+ role="dialog"
46
+ aria-modal
47
+ tabIndex={-1}
48
+ className={clsx(
49
+ "fixed right-0 bottom-0 left-0 transform border-t bg-popup p-4 transition-transform duration-300 ease-[cubic-bezier(.22,1,.36,1)]",
50
+ open ? "translate-y-0" : "translate-y-full",
51
+ "motion-reduce:transform-none motion-reduce:transition-none",
52
+ )}
53
+ >
54
+ <nav className="mt-4 flex flex-col gap-2">
55
+ <Link to="/components" className="text-sm text-muted-foreground">
56
+ Components
57
+ </Link>
58
+ <Link to="/" className="text-base">
59
+ Home
60
+ </Link>
61
+ <div>
62
+ <Link to="/components" className="text-base">
63
+ Components
64
+ </Link>
65
+ <div className=" ml-4 flex flex-col">
66
+ <Suspense fallback={<SuspenseFallback />}>
67
+ {[...allComponents]
68
+ .sort((a, b) => a.title.localeCompare(b.title))
69
+ .map((component) => (
70
+ <Link
71
+ key={component.slug}
72
+ to="/components/$slug"
73
+ params={{ slug: component.slug }}
74
+ className="text-base"
75
+ >
76
+ {component.title}
77
+ </Link>
78
+ ))}
79
+ </Suspense>
80
+ </div>
81
+ </div>
82
+ </nav>
83
+ </div>
84
+ </div>
85
+ );
86
+ }