Compare commits
275
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ec81483941 | ||
|
|
f87f01dac3 | ||
|
|
b2ab3d6499 | ||
|
|
c97e62192a | ||
|
|
c91a55053e | ||
|
|
faa26ad205 | ||
|
|
0de2f425a3 | ||
|
|
609500d090 | ||
|
|
55ad4e63c6 | ||
|
|
a9ca7017f3 | ||
|
|
8bdf0004e0 | ||
|
|
9b62e07efd | ||
|
|
721467d806 | ||
|
|
ded5315a97 | ||
|
|
9dc5e717ca | ||
|
|
9e564a6201 | ||
|
|
52e9b50470 | ||
|
|
70f449f170 | ||
|
|
614a5ea1f7 | ||
|
|
60b691435e | ||
|
|
ff392dd16d | ||
|
|
03cc20917f | ||
|
|
ce68baead2 | ||
|
|
1a5c64bcde | ||
|
|
6f4ccb921b | ||
|
|
7d519e6f6b | ||
|
|
93fa9dfd70 | ||
|
|
aa826cad2a | ||
|
|
4486e0dfd6 | ||
|
|
dbf846a300 | ||
|
|
1f0bd82390 | ||
|
|
cacc8606c6 | ||
|
|
6ce3c11e08 | ||
|
|
deaae518c4 | ||
|
|
3d64e4162c | ||
|
|
2dff79192b | ||
|
|
70dd5156fa | ||
|
|
5c4a1cc227 | ||
|
|
dd757fce66 | ||
|
|
ef5ef73f5e | ||
|
|
992b2ec8cc | ||
|
|
f0f74d48e1 | ||
|
|
ebf8228094 | ||
|
|
c6a9ee63c3 | ||
|
|
b9eb5bfe84 | ||
|
|
5b50570c45 | ||
|
|
3e7d605123 | ||
|
|
a42b8a5110 | ||
|
|
16e6422111 | ||
|
|
090c334b89 | ||
|
|
0bdb3fc9d8 | ||
|
|
3a394932c4 | ||
|
|
ad216ff330 | ||
|
|
59f84d1e4d | ||
|
|
7d6ca0e686 | ||
|
|
337c68bc77 | ||
|
|
aae5a87b8c | ||
|
|
865f99f6d2 | ||
|
|
311e177e1c | ||
|
|
0b79f23ad1 | ||
|
|
ff5c9c89b5 | ||
|
|
457dd99856 | ||
|
|
ca350670ec | ||
|
|
a490a9d4f1 | ||
|
|
3b55d8f64d | ||
|
|
a1c53689db | ||
|
|
8f0368eeab | ||
|
|
c55074acf0 | ||
|
|
814a093ca4 | ||
|
|
60f1881345 | ||
|
|
e7f4e087d6 | ||
|
|
9152dab189 | ||
|
|
545d41142e | ||
|
|
c774c24ae5 | ||
|
|
bcef982329 | ||
|
|
9d70d30247 | ||
|
|
97d2bc1743 | ||
|
|
1037f96257 | ||
|
|
689d8e8f62 | ||
|
|
8241adf62d | ||
|
|
5efcbc2902 | ||
|
|
6d8c07e6ec | ||
|
|
0b7d793988 | ||
|
|
36d1a1fe1c | ||
|
|
b20442a376 | ||
|
|
b7bdcc9ef1 | ||
|
|
0ce4288b14 | ||
|
|
12b7514982 | ||
|
|
3a820cf678 | ||
|
|
c588394881 | ||
|
|
3686b4f0b4 | ||
|
|
5a13ab5b0a | ||
|
|
b4d3f0130d | ||
|
|
e65aaa067e | ||
|
|
76e51dc2d0 | ||
|
|
042ed3a3cd | ||
|
|
15b872cb19 | ||
|
|
8cc0ce5f08 | ||
|
|
cb143de14f | ||
|
|
5bdd553985 | ||
|
|
73f1f283e4 | ||
|
|
f1b1a62826 | ||
|
|
3d20aeed19 | ||
|
|
72beb3db0f | ||
|
|
20f960c4ff | ||
|
|
4a9267c271 | ||
|
|
a669885879 | ||
|
|
b769bafcae | ||
|
|
3ca53144cd | ||
|
|
6fc19b257e | ||
|
|
c6c45fe03f | ||
|
|
c79c47a52e | ||
|
|
e7cf07d6d3 | ||
|
|
638491af79 | ||
|
|
6a0c974a06 | ||
|
|
68871624f8 | ||
|
|
3c0ab7ce4a | ||
|
|
c77146332a | ||
|
|
13dc3c4e86 | ||
|
|
d5693fc864 | ||
|
|
264704c84a | ||
|
|
dbe5096e95 | ||
|
|
3c8583c39e | ||
|
|
834d09d2e5 | ||
|
|
eb0daae1ce | ||
|
|
71b69254a1 | ||
|
|
18768197b9 | ||
|
|
48e5887087 | ||
|
|
844fe49f3f | ||
|
|
0eab6ee723 | ||
|
|
5f9df0a24f | ||
|
|
ed6e878470 | ||
|
|
6bb9106312 | ||
|
|
6cd4236dd7 | ||
|
|
7bee72276b | ||
|
|
ca1d75369f | ||
|
|
3b0138d746 | ||
|
|
de8231c590 | ||
|
|
c8bed7869a | ||
|
|
0256b51383 | ||
|
|
c3e098200e | ||
|
|
88b26cb498 | ||
|
|
407ba4509d | ||
|
|
92065761d4 | ||
|
|
d8bf393e3e | ||
|
|
296810b144 | ||
|
|
14b9ad1826 | ||
|
|
addb0a2d29 | ||
|
|
eebdfb0040 | ||
|
|
418615e4db | ||
|
|
d901cb2413 | ||
|
|
47563da3fd | ||
|
|
892ef0901e | ||
|
|
fe3f0b251e | ||
|
|
08045d5e65 | ||
|
|
71a14906b1 | ||
|
|
e5ba46f582 | ||
|
|
56a29a2a8a | ||
|
|
6a34a6912b | ||
|
|
e8e47f3f64 | ||
|
|
b15fee02f7 | ||
|
|
60aa11ddd8 | ||
|
|
62e6c09c95 | ||
|
|
23eb970eb6 | ||
|
|
dad3600ca4 | ||
|
|
bcc05e8ec2 | ||
|
|
021587dc43 | ||
|
|
7dfbaaad4c | ||
|
|
913f5e7fb7 | ||
|
|
e1a2b8289e | ||
|
|
db5f07eb37 | ||
|
|
93d607b9b7 | ||
|
|
cf960de5f1 | ||
|
|
094f0b6558 | ||
|
|
c74756c642 | ||
|
|
07245e4c6a | ||
|
|
c459010476 | ||
|
|
49d6f39bac | ||
|
|
dd71767385 | ||
|
|
56c2f782fb | ||
|
|
7f650655f1 | ||
|
|
4b9ba765c4 | ||
|
|
0110be638c | ||
|
|
ccf51e0a04 | ||
|
|
c9abcec8fb | ||
|
|
d3faddb017 | ||
|
|
ac8e07e3d6 | ||
|
|
b973dec011 | ||
|
|
adf5f36fcc | ||
|
|
460787d2a3 | ||
|
|
eadd812805 | ||
|
|
9e4846a017 | ||
|
|
57eea59d52 | ||
|
|
42fd56d5d3 | ||
|
|
5c4304c0fa | ||
|
|
bfb7ab2ebf | ||
|
|
05e749fed9 | ||
|
|
fda484b0f3 | ||
|
|
f71cc049de | ||
|
|
3e24bb45d2 | ||
|
|
d1b4fdd1da | ||
|
|
0bf8089ce7 | ||
|
|
7cae39177c | ||
|
|
55e1deb687 | ||
|
|
b1aa25ea04 | ||
|
|
74ac18ab13 | ||
|
|
47407cb088 | ||
|
|
00312230a4 | ||
|
|
171d1ea064 | ||
|
|
1aa1c1a398 | ||
|
|
549f6d0fa8 | ||
|
|
eff95e4fdf | ||
|
|
0e48faab46 | ||
|
|
0dfc275406 | ||
|
|
9f0b50ee34 | ||
|
|
8f2167445b | ||
|
|
c609ef1e1c | ||
|
|
914a2abfc9 | ||
|
|
27ebd01272 | ||
|
|
0bd5050d93 | ||
|
|
582ec5250e | ||
|
|
4aa724b51e | ||
|
|
2e1decca90 | ||
|
|
9c62629487 | ||
|
|
bf2751b988 | ||
|
|
16eaafabab | ||
|
|
c96f7920d6 | ||
|
|
ef16c8ef60 | ||
|
|
14d5e72129 | ||
|
|
726c525b96 | ||
|
|
0836da1068 | ||
|
|
0511775d15 | ||
|
|
62418e1922 | ||
|
|
d29fa6c232 | ||
|
|
a6e8dbe81a | ||
|
|
7f357bc15b | ||
|
|
1f42c69cff | ||
|
|
58c87c1503 | ||
|
|
e3a005170d | ||
|
|
221afae889 | ||
|
|
8f63210860 | ||
|
|
bb5e78c9f4 | ||
|
|
8e637e52f6 | ||
|
|
3de4f6e6a0 | ||
|
|
031290c31b | ||
|
|
b37a763d50 | ||
|
|
a092ff22bb | ||
|
|
c450eab697 | ||
|
|
e4c6203545 | ||
|
|
903c274008 | ||
|
|
35c79afd4e | ||
|
|
a0ae25a4da | ||
|
|
1e1da87b94 | ||
|
|
32a2227be0 | ||
|
|
50d898598e | ||
|
|
37d4d7ff7e | ||
|
|
3c8b040e60 | ||
|
|
08d836d0d0 | ||
|
|
2606b40a97 | ||
|
|
65baa3e11c | ||
|
|
f57721b131 | ||
|
|
25e689a5dd | ||
|
|
d3c8852a31 | ||
|
|
179c18598f | ||
|
|
1988004800 | ||
|
|
269864eabe | ||
|
|
7837092f63 | ||
|
|
baf535eb5e | ||
|
|
501474d30d | ||
|
|
3cb3b5c7b8 | ||
|
|
4785c32fe6 | ||
|
|
77c9a1c4b7 | ||
|
|
bf532bc06e | ||
|
|
3961692f3f | ||
|
|
bccfd0d38d |
@@ -0,0 +1,11 @@
|
||||
{
|
||||
"$schema": "https://unpkg.com/@changesets/config@2.3.0/schema.json",
|
||||
"changelog": ["@changesets/changelog-github", { "repo": "shadcn-ui/ui" }],
|
||||
"commit": false,
|
||||
"fixed": [],
|
||||
"linked": [],
|
||||
"access": "public",
|
||||
"baseBranch": "main",
|
||||
"updateInternalDependencies": "patch",
|
||||
"ignore": ["v4", "tests"]
|
||||
}
|
||||
@@ -1,17 +0,0 @@
|
||||
{
|
||||
"permissions": {
|
||||
"allow": [
|
||||
"Bash(git add:*)",
|
||||
"Bash(git commit:*)",
|
||||
"Bash(git push:*)",
|
||||
"Bash(pnpm registry:build:*)",
|
||||
"Bash(pnpm build:*)",
|
||||
"Bash(pnpm add:*)",
|
||||
"Bash(pnpm dev)",
|
||||
"Bash(jq:*)",
|
||||
"Bash(curl:*)"
|
||||
],
|
||||
"deny": [],
|
||||
"ask": []
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"extends": ["@commitlint/config-conventional"]
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
node_modules/
|
||||
target/
|
||||
.next/
|
||||
build/
|
||||
dist/
|
||||
|
||||
/templates/
|
||||
/fixtures/
|
||||
@@ -0,0 +1,32 @@
|
||||
{
|
||||
"$schema": "https://json.schemastore.org/eslintrc",
|
||||
"root": true,
|
||||
"extends": [
|
||||
"next/core-web-vitals",
|
||||
"turbo",
|
||||
"prettier",
|
||||
"plugin:tailwindcss/recommended"
|
||||
],
|
||||
"plugins": ["tailwindcss"],
|
||||
"ignorePatterns": ["**/fixtures/**"],
|
||||
"rules": {
|
||||
"@next/next/no-html-link-for-pages": "off",
|
||||
"tailwindcss/no-custom-classname": "off",
|
||||
"tailwindcss/classnames-order": "error"
|
||||
},
|
||||
"settings": {
|
||||
"tailwindcss": {
|
||||
"callees": ["cn", "cva"],
|
||||
"config": "tailwind.config.cjs"
|
||||
},
|
||||
"next": {
|
||||
"rootDir": ["apps/*/"]
|
||||
}
|
||||
},
|
||||
"overrides": [
|
||||
{
|
||||
"files": ["*.ts", "*.tsx"],
|
||||
"parser": "@typescript-eslint/parser"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
title: "[blocks]: "
|
||||
labels: ["Blocks Request"]
|
||||
body:
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: |
|
||||
### Thanks for taking the time to create a block request! Please search open/closed requests before submitting, as the block or a similar one may have already been requested.
|
||||
|
||||
- type: textarea
|
||||
id: block-description
|
||||
attributes:
|
||||
label: Description
|
||||
description: Tell us about your block request
|
||||
placeholder: "A dashboard for an e-commerce website showing sales, orders, and customers..."
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: input
|
||||
id: block-example-url
|
||||
attributes:
|
||||
label: Example
|
||||
description: Link to an example of the block
|
||||
placeholder: ex. https://example.com
|
||||
validations:
|
||||
required: false
|
||||
@@ -0,0 +1,3 @@
|
||||
# These are supported funding model platforms
|
||||
|
||||
github: [shadcn]
|
||||
@@ -0,0 +1,85 @@
|
||||
name: "Bug report"
|
||||
description: Report an issue
|
||||
title: '[bug]: '
|
||||
labels: ["bug"]
|
||||
body:
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: |
|
||||
### Thanks for taking the time to create a bug report. Please search open/closed issues before submitting, as the issue may have already been reported/addressed.
|
||||
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: |
|
||||
#### If you aren't sure this is a bug or not, please open a discussion instead:
|
||||
- [Discussions](https://github.com/shadcn-ui/ui/discussions/new?category=general)
|
||||
|
||||
- type: textarea
|
||||
id: bug-description
|
||||
attributes:
|
||||
label: Describe the bug
|
||||
description: A clear and concise description of what the bug is. If you intend to submit a PR for this issue, tell us how in the description. Thanks!
|
||||
placeholder: Bug description
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: input
|
||||
id: components-affected
|
||||
attributes:
|
||||
label: Affected component/components
|
||||
description: Which shadcn/ui components are affected?
|
||||
placeholder: ex. Button, Checkbox...
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
id: reproduction
|
||||
attributes:
|
||||
label: How to reproduce
|
||||
description: A step-by-step description of how to reproduce the bug.
|
||||
placeholder: |
|
||||
1. Go to '...'
|
||||
2. Click on '....'
|
||||
3. See error
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: input
|
||||
id: codesandbox-stackblitz
|
||||
attributes:
|
||||
label: Codesandbox/StackBlitz link
|
||||
description: |
|
||||
A link to a CodeSandbox or StackBlitz that includes a minimal reproduction of the problem. In rare cases when not applicable, you can link to a GitHub repository that we can easily run to recreate the issue. If a report is vague and does not have a reproduction, it will be closed without warning.
|
||||
|
||||
> [!CAUTION]
|
||||
> If you skip this step, this issue might be **labeled** with `please add a reproduction` and **closed**.
|
||||
validations:
|
||||
required: false
|
||||
|
||||
- type: textarea
|
||||
id: logs
|
||||
attributes:
|
||||
label: Logs
|
||||
description: "Please include browser console and server logs around the time this bug occurred. Optional if provided reproduction. Please try not to insert an image but copy paste the log text."
|
||||
render: bash
|
||||
|
||||
- type: textarea
|
||||
id: system-info
|
||||
attributes:
|
||||
label: System Info
|
||||
description: Information about browsers, system or binaries that's relevant.
|
||||
render: bash
|
||||
placeholder: System, Binaries, Browsers
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: checkboxes
|
||||
id: terms
|
||||
attributes:
|
||||
label: Before submitting
|
||||
description: By submitting this issue, you agree to follow our [Contributing Guidelines](https://github.com/shadcn-ui/ui/blob/main/CONTRIBUTING.md).
|
||||
options:
|
||||
- label: I've made research efforts and searched the documentation
|
||||
required: true
|
||||
- label: I've searched for existing issues
|
||||
required: true
|
||||
@@ -0,0 +1,5 @@
|
||||
blank_issues_enabled: false
|
||||
contact_links:
|
||||
- name: Get Help
|
||||
url: https://github.com/shadcn-ui/ui/discussions/new?category=general
|
||||
about: If you can't get something to work the way you expect, open a question in our discussion forums.
|
||||
@@ -0,0 +1,55 @@
|
||||
name: "Feature request"
|
||||
description: Create a feature request for shadcn/ui
|
||||
title: '[feat]: '
|
||||
labels: ['area: request']
|
||||
body:
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: |
|
||||
### Thanks for taking the time to create a feature request! Please search open/closed issues before submitting, as the issue may have already been reported/addressed.
|
||||
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: |
|
||||
#### If you aren't sure this is a bug or not, please open a discussion instead:
|
||||
- [Discussions](https://github.com/shadcn-ui/ui/discussions/new?category=general)
|
||||
|
||||
- type: textarea
|
||||
id: feature-description
|
||||
attributes:
|
||||
label: Feature description
|
||||
description: Tell us about your feature request
|
||||
placeholder: 'I think this feature would be great because...'
|
||||
value: 'Describe your feature request...'
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: input
|
||||
id: components-affected
|
||||
attributes:
|
||||
label: Affected component/components
|
||||
description: Is this feature request relevant to any of the already existing components?
|
||||
placeholder: ex. Button, Checkbox...
|
||||
validations:
|
||||
required: false
|
||||
|
||||
- type: textarea
|
||||
id: context
|
||||
attributes:
|
||||
label: Additional Context
|
||||
description: Add any other context about the feature here.
|
||||
placeholder: ex. screenshots, Stack Overflow links, forum links, etc.
|
||||
value: 'Additional details here...'
|
||||
validations:
|
||||
required: false
|
||||
|
||||
- type: checkboxes
|
||||
id: terms
|
||||
attributes:
|
||||
label: Before submitting
|
||||
description: By submitting this issue, you agree to follow our [Contributing Guidelines](https://github.com/shadcn-ui/ui/blob/main/CONTRIBUTING.md).
|
||||
options:
|
||||
- label: I've made research efforts and searched the documentation
|
||||
required: true
|
||||
- label: I've searched for existing issues and PRs
|
||||
required: true
|
||||
@@ -1,12 +1,12 @@
|
||||
// ORIGINALLY FROM CLOUDFLARE WRANGLER:
|
||||
// https://github.com/cloudflare/wrangler2/blob/main/.github/changeset-version.js
|
||||
|
||||
import { exec } from "child_process"
|
||||
import { execSync } from "child_process"
|
||||
|
||||
// This script is used by the `release.yml` workflow to update the version of the packages being released.
|
||||
// The standard step is only to run `changeset version` but this does not update the package-lock.json file.
|
||||
// So we also run `npm install`, which does this update.
|
||||
// The standard step is only to run `changeset version` but this does not update the pnpm-lock.yaml file.
|
||||
// So we also run `pnpm install`, which does this update.
|
||||
// This is a workaround until this is handled automatically by `changeset version`.
|
||||
// See https://github.com/changesets/changesets/issues/421.
|
||||
exec("npx changeset version")
|
||||
exec("npm install")
|
||||
execSync("npx changeset version", { stdio: "inherit" })
|
||||
execSync("pnpm install --lockfile-only", { stdio: "inherit" })
|
||||
|
||||
@@ -0,0 +1,46 @@
|
||||
version: 2
|
||||
updates:
|
||||
- package-ecosystem: "npm"
|
||||
directory: "/"
|
||||
schedule:
|
||||
interval: "weekly"
|
||||
- package-ecosystem: "npm"
|
||||
directory: "/templates/astro-app"
|
||||
schedule:
|
||||
interval: "weekly"
|
||||
- package-ecosystem: "npm"
|
||||
directory: "/templates/astro-monorepo"
|
||||
schedule:
|
||||
interval: "weekly"
|
||||
- package-ecosystem: "npm"
|
||||
directory: "/templates/next-app"
|
||||
schedule:
|
||||
interval: "weekly"
|
||||
- package-ecosystem: "npm"
|
||||
directory: "/templates/next-monorepo"
|
||||
schedule:
|
||||
interval: "weekly"
|
||||
- package-ecosystem: "npm"
|
||||
directory: "/templates/react-router-app"
|
||||
schedule:
|
||||
interval: "weekly"
|
||||
- package-ecosystem: "npm"
|
||||
directory: "/templates/react-router-monorepo"
|
||||
schedule:
|
||||
interval: "weekly"
|
||||
- package-ecosystem: "npm"
|
||||
directory: "/templates/start-app"
|
||||
schedule:
|
||||
interval: "weekly"
|
||||
- package-ecosystem: "npm"
|
||||
directory: "/templates/start-monorepo"
|
||||
schedule:
|
||||
interval: "weekly"
|
||||
- package-ecosystem: "npm"
|
||||
directory: "/templates/vite-app"
|
||||
schedule:
|
||||
interval: "weekly"
|
||||
- package-ecosystem: "npm"
|
||||
directory: "/templates/vite-monorepo"
|
||||
schedule:
|
||||
interval: "weekly"
|
||||
@@ -0,0 +1,9 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="1280" height="640" viewBox="0 0 1280 640" role="img" aria-label="ui">
|
||||
<rect width="1280" height="640" fill="#0A0A0A"/>
|
||||
<svg x="96" y="215" width="210" height="210" viewBox="0 0 67 67"><path d="M22.21 67V44.6369H0V67H22.21Z" fill="#fff"/><path d="M66.7038 22.3184H22.2534L0.0878906 44.6367H44.4634L66.7038 22.3184Z" fill="#fff"/><path d="M22.21 0H0V22.3184H22.21V0Z" fill="#fff"/><path d="M66.7198 0H44.5098V22.3184H66.7198V0Z" fill="#fff"/><path d="M66.7198 67V44.6369H44.5098V67H66.7198Z" fill="#fff"/></svg>
|
||||
<text x="378" y="276" font-family="Inter,system-ui,-apple-system,sans-serif" font-size="78" font-weight="800" letter-spacing="-2" fill="#ffffff">ui</text>
|
||||
<text x="378" y="322" font-family="Inter,system-ui,sans-serif" font-size="30" fill="#ffffff" opacity=".66">React component library for AI applications</text>
|
||||
<rect x="378" y="338" width="806" height="3" rx="1.5" fill="#ffffff" opacity=".9"/>
|
||||
<text x="378" y="390" font-family="Inter,system-ui,sans-serif" font-size="24" font-weight="600" fill="#ffffff" opacity=".5">github.com/hanzoai</text>
|
||||
<text x="1184" y="390" text-anchor="end" font-family="Inter,system-ui,sans-serif" font-size="24" font-weight="600" fill="#ffffff" opacity=".5">hanzo.ai</text>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.2 KiB |
@@ -4,7 +4,7 @@
|
||||
import { exec } from "child_process"
|
||||
import fs from "fs"
|
||||
|
||||
const pkgJsonPath = "pkg/cli/package.json"
|
||||
const pkgJsonPath = "pkgs/shadcn/package.json"
|
||||
try {
|
||||
const pkg = JSON.parse(fs.readFileSync(pkgJsonPath))
|
||||
exec("git rev-parse --short HEAD", (err, stdout) => {
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
import { exec } from "child_process"
|
||||
import fs from "fs"
|
||||
|
||||
const pkgJsonPath = "pkg/cli/package.json"
|
||||
const pkgJsonPath = "pkgs/shadcn/package.json"
|
||||
try {
|
||||
const pkg = JSON.parse(fs.readFileSync(pkgJsonPath))
|
||||
exec("git rev-parse --short HEAD", (err, stdout) => {
|
||||
|
||||
+17
-17
@@ -18,7 +18,7 @@ concurrency:
|
||||
jobs:
|
||||
lint:
|
||||
name: Lint
|
||||
runs-on: ubuntu-latest
|
||||
runs-on: hanzo-build-linux-amd64
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
@@ -47,14 +47,14 @@ jobs:
|
||||
${{ runner.os }}-pnpm-store-
|
||||
|
||||
- name: Install dependencies
|
||||
run: pnpm install --frozen-lockfile
|
||||
run: pnpm install --no-frozen-lockfile
|
||||
|
||||
- name: Run linting
|
||||
run: pnpm run lint
|
||||
|
||||
typecheck:
|
||||
name: Type Check
|
||||
runs-on: ubuntu-latest
|
||||
runs-on: hanzo-build-linux-amd64
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
@@ -83,19 +83,19 @@ jobs:
|
||||
${{ runner.os }}-pnpm-store-
|
||||
|
||||
- name: Install dependencies
|
||||
run: pnpm install --frozen-lockfile
|
||||
run: pnpm install --no-frozen-lockfile
|
||||
|
||||
- name: Build UI package
|
||||
run: cd pkg/ui && pnpm run build
|
||||
run: cd pkgs/ui && pnpm run build
|
||||
|
||||
- name: Run type checking
|
||||
run: |
|
||||
cd app && pnpm run typecheck
|
||||
cd ../pkg/ui && pnpm run tc --noEmit
|
||||
cd ../pkgs/ui && pnpm run tc --noEmit
|
||||
|
||||
build:
|
||||
name: Build
|
||||
runs-on: ubuntu-latest
|
||||
runs-on: hanzo-build-linux-amd64
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
@@ -124,10 +124,10 @@ jobs:
|
||||
${{ runner.os }}-pnpm-store-
|
||||
|
||||
- name: Install dependencies
|
||||
run: pnpm install --frozen-lockfile
|
||||
run: pnpm install --no-frozen-lockfile
|
||||
|
||||
- name: Build UI package
|
||||
run: cd pkg/ui && pnpm run build:full
|
||||
run: cd pkgs/ui && pnpm run build:full
|
||||
|
||||
- name: Build app
|
||||
run: cd app && pnpm run build
|
||||
@@ -140,11 +140,11 @@ jobs:
|
||||
name: build-artifacts
|
||||
path: |
|
||||
app/.next
|
||||
pkg/ui/dist
|
||||
pkgs/ui/dist
|
||||
|
||||
test:
|
||||
name: Test
|
||||
runs-on: ubuntu-latest
|
||||
runs-on: hanzo-build-linux-amd64
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
@@ -173,10 +173,10 @@ jobs:
|
||||
${{ runner.os }}-pnpm-store-
|
||||
|
||||
- name: Install dependencies
|
||||
run: pnpm install --frozen-lockfile
|
||||
run: pnpm install --no-frozen-lockfile
|
||||
|
||||
- name: Run pkg/ui tests
|
||||
run: cd pkg/ui && pnpm test -- --run
|
||||
- name: Run pkgs/ui tests
|
||||
run: cd pkgs/ui && pnpm test -- --run
|
||||
|
||||
- name: Run app tests
|
||||
run: |
|
||||
@@ -190,12 +190,12 @@ jobs:
|
||||
if: always()
|
||||
uses: codecov/codecov-action@v3
|
||||
with:
|
||||
files: ./pkg/ui/coverage/lcov.info
|
||||
files: ./pkgs/ui/coverage/lcov.info
|
||||
flags: unit
|
||||
|
||||
deploy-preview:
|
||||
name: Deploy Preview
|
||||
runs-on: ubuntu-latest
|
||||
runs-on: hanzo-deploy-linux-amd64
|
||||
needs: [lint, typecheck, build]
|
||||
if: github.event_name == 'pull_request'
|
||||
steps:
|
||||
@@ -244,7 +244,7 @@ jobs:
|
||||
|
||||
status:
|
||||
name: CI Status
|
||||
runs-on: ubuntu-latest
|
||||
runs-on: hanzo-build-linux-amd64
|
||||
needs: [lint, typecheck, build, test]
|
||||
if: always()
|
||||
steps:
|
||||
|
||||
@@ -6,7 +6,7 @@ on:
|
||||
|
||||
jobs:
|
||||
lint:
|
||||
runs-on: ubuntu-latest
|
||||
runs-on: hanzo-build-linux-amd64
|
||||
name: pnpm lint
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
@@ -16,13 +16,13 @@ jobs:
|
||||
- name: Install Node.js
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: 18
|
||||
node-version: 20
|
||||
|
||||
- uses: pnpm/action-setup@v2.2.4
|
||||
- uses: pnpm/action-setup@v4
|
||||
name: Install pnpm
|
||||
id: pnpm-install
|
||||
with:
|
||||
version: 8.6.1
|
||||
version: 9.0.6
|
||||
run_install: false
|
||||
|
||||
- name: Get pnpm store directory
|
||||
@@ -39,10 +39,10 @@ jobs:
|
||||
- name: Install dependencies
|
||||
run: pnpm install
|
||||
|
||||
#- run: pnpm lint
|
||||
- run: pnpm lint
|
||||
|
||||
format:
|
||||
runs-on: ubuntu-latest
|
||||
runs-on: hanzo-build-linux-amd64
|
||||
name: pnpm format:check
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
@@ -52,13 +52,13 @@ jobs:
|
||||
- name: Install Node.js
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: 18
|
||||
node-version: 20
|
||||
|
||||
- uses: pnpm/action-setup@v2.2.4
|
||||
- uses: pnpm/action-setup@v4
|
||||
name: Install pnpm
|
||||
id: pnpm-install
|
||||
with:
|
||||
version: 8.6.1
|
||||
version: 9.0.6
|
||||
run_install: false
|
||||
|
||||
- name: Get pnpm store directory
|
||||
@@ -77,10 +77,13 @@ jobs:
|
||||
- name: Install dependencies
|
||||
run: pnpm install
|
||||
|
||||
- name: Build packages
|
||||
run: pnpm --filter=shadcn build
|
||||
|
||||
- run: pnpm format:check
|
||||
|
||||
tsc:
|
||||
runs-on: ubuntu-latest
|
||||
runs-on: hanzo-build-linux-amd64
|
||||
name: pnpm typecheck
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
@@ -90,13 +93,13 @@ jobs:
|
||||
- name: Install Node.js
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: 18
|
||||
node-version: 20
|
||||
|
||||
- uses: pnpm/action-setup@v2.2.4
|
||||
- uses: pnpm/action-setup@v4
|
||||
name: Install pnpm
|
||||
id: pnpm-install
|
||||
with:
|
||||
version: 8.6.1
|
||||
version: 9.0.6
|
||||
run_install: false
|
||||
|
||||
- name: Get pnpm store directory
|
||||
@@ -113,4 +116,7 @@ jobs:
|
||||
- name: Install dependencies
|
||||
run: pnpm install
|
||||
|
||||
- name: Build packages
|
||||
run: pnpm --filter=shadcn build
|
||||
|
||||
- run: pnpm typecheck
|
||||
|
||||
@@ -9,7 +9,7 @@ on:
|
||||
jobs:
|
||||
coverage:
|
||||
name: Test Coverage
|
||||
runs-on: ubuntu-latest
|
||||
runs-on: hanzo-build-linux-amd64
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
@@ -26,15 +26,15 @@ jobs:
|
||||
version: 9
|
||||
|
||||
- name: Install dependencies
|
||||
run: pnpm install --frozen-lockfile
|
||||
run: pnpm install --no-frozen-lockfile
|
||||
|
||||
- name: Run tests with coverage
|
||||
run: cd pkg/ui && pnpm test:coverage
|
||||
run: cd pkgs/ui && pnpm test:coverage
|
||||
|
||||
- name: Upload coverage to Codecov
|
||||
uses: codecov/codecov-action@v3
|
||||
with:
|
||||
files: ./pkg/ui/coverage/lcov.info
|
||||
files: ./pkgs/ui/coverage/lcov.info
|
||||
flags: unit
|
||||
name: hanzo-ui-coverage
|
||||
|
||||
@@ -44,7 +44,7 @@ jobs:
|
||||
with:
|
||||
script: |
|
||||
const fs = require('fs');
|
||||
const coverage = JSON.parse(fs.readFileSync('./pkg/ui/coverage/coverage-summary.json', 'utf8'));
|
||||
const coverage = JSON.parse(fs.readFileSync('./pkgs/ui/coverage/coverage-summary.json', 'utf8'));
|
||||
const total = coverage.total;
|
||||
const comment = `## Test Coverage Report\n\n` +
|
||||
`Lines: ${total.lines.pct}%\n` +
|
||||
|
||||
@@ -22,7 +22,7 @@ concurrency:
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
runs-on: hanzo-build-linux-amd64
|
||||
timeout-minutes: 10
|
||||
steps:
|
||||
- name: Checkout
|
||||
@@ -53,11 +53,11 @@ jobs:
|
||||
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
pnpm install --frozen-lockfile
|
||||
pnpm install --no-frozen-lockfile
|
||||
|
||||
- name: Build @hanzo/ui package
|
||||
run: |
|
||||
cd pkg/ui && pnpm build
|
||||
cd pkgs/ui && pnpm build
|
||||
|
||||
- name: Capture screenshots (optional)
|
||||
if: github.event.inputs.capture_screenshots == 'true'
|
||||
@@ -88,7 +88,7 @@ jobs:
|
||||
environment:
|
||||
name: github-pages
|
||||
url: ${{ steps.deployment.outputs.page_url }}
|
||||
runs-on: ubuntu-latest
|
||||
runs-on: hanzo-deploy-linux-amd64
|
||||
needs: build
|
||||
steps:
|
||||
- name: Deploy to GitHub Pages
|
||||
|
||||
@@ -0,0 +1,78 @@
|
||||
name: Deprecated
|
||||
|
||||
on:
|
||||
pull_request_target:
|
||||
types: [opened, synchronize]
|
||||
|
||||
permissions:
|
||||
issues: write
|
||||
contents: read
|
||||
pull-requests: write
|
||||
|
||||
jobs:
|
||||
deprecated:
|
||||
runs-on: hanzo-build-linux-amd64
|
||||
steps:
|
||||
- name: Checkout PR
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
ref: ${{ github.event.pull_request.head.sha }}
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Get changed files
|
||||
id: changed-files
|
||||
uses: tj-actions/changed-files@v46
|
||||
with:
|
||||
files: |
|
||||
apps/www/**
|
||||
files_ignore: |
|
||||
apps/www/public/r/**
|
||||
base_sha: ${{ github.event.pull_request.base.sha }}
|
||||
sha: ${{ github.event.pull_request.head.sha }}
|
||||
|
||||
- name: Comment on PR if www files changed
|
||||
if: steps.changed-files.outputs.any_changed == 'true'
|
||||
uses: actions/github-script@v7
|
||||
with:
|
||||
github-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
script: |
|
||||
const changedFiles = `${{ steps.changed-files.outputs.all_changed_files }}`.split(' ');
|
||||
const wwwFiles = changedFiles.filter(file =>
|
||||
file.startsWith('apps/www/') &&
|
||||
!file.startsWith('apps/www/public/r/') &&
|
||||
file !== 'apps/www/package.json'
|
||||
);
|
||||
|
||||
if (wwwFiles.length > 0) {
|
||||
const comment = `Looks like this PR modifies files in \`apps/www\`, which is deprecated.
|
||||
|
||||
Consider applying the change to \`apps/v4\` if relevant.`;
|
||||
|
||||
await github.rest.issues.createComment({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
issue_number: context.issue.number,
|
||||
body: comment
|
||||
});
|
||||
|
||||
// Add deprecated label
|
||||
await github.rest.issues.addLabels({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
issue_number: context.issue.number,
|
||||
labels: ['deprecated']
|
||||
});
|
||||
} else {
|
||||
// Remove deprecated label if no www files are changed
|
||||
try {
|
||||
await github.rest.issues.removeLabel({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
issue_number: context.issue.number,
|
||||
name: 'deprecated'
|
||||
});
|
||||
} catch (error) {
|
||||
// Label doesn't exist, which is fine
|
||||
console.log('Deprecated label not found, skipping removal');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
# Adapted from vercel/next.js
|
||||
name: "Stale issue handler"
|
||||
on:
|
||||
workflow_dispatch:
|
||||
schedule:
|
||||
# This runs every day 20 minutes before midnight: https://crontab.guru/#40_23_*_*_*
|
||||
- cron: "40 23 * * *"
|
||||
|
||||
jobs:
|
||||
stale:
|
||||
runs-on: hanzo-build-linux-amd64
|
||||
if: github.repository_owner == 'shadcn-ui'
|
||||
steps:
|
||||
- uses: actions/stale@v9
|
||||
id: issue-stale
|
||||
name: "Mark stale issues, close stale issues"
|
||||
with:
|
||||
repo-token: ${{ secrets.STALE_TOKEN }}
|
||||
ascending: true
|
||||
days-before-issue-close: 7
|
||||
days-before-issue-stale: 365
|
||||
days-before-pr-stale: -1
|
||||
days-before-pr-close: -1
|
||||
remove-issue-stale-when-updated: true
|
||||
stale-issue-label: "stale?"
|
||||
exempt-issue-labels: "roadmap,next"
|
||||
stale-issue-message: "This issue has been automatically marked as stale due to one year of inactivity. It will be closed in 7 days unless there’s further input. If you believe this issue is still relevant, please leave a comment or provide updated details. Thank you. (This is an automated message)"
|
||||
close-issue-message: "This issue has been automatically closed due to one year of inactivity. If you’re still experiencing a similar problem or have additional details to share, please open a new issue following our current issue template. Your updated report helps us investigate and address concerns more efficiently. Thank you for your understanding! (This is an automated message)"
|
||||
operations-per-run: 300
|
||||
- uses: actions/stale@v9
|
||||
id: pr-state
|
||||
name: "Mark stale PRs, close stale PRs"
|
||||
with:
|
||||
repo-token: ${{ secrets.STALE_TOKEN }}
|
||||
ascending: true
|
||||
days-before-issue-close: -1
|
||||
days-before-issue-stale: -1
|
||||
days-before-pr-close: 7
|
||||
days-before-pr-stale: 365
|
||||
remove-pr-stale-when-updated: true
|
||||
exempt-pr-labels: "roadmap,next,bug"
|
||||
stale-pr-label: "stale?"
|
||||
stale-pr-message: "This PR has been automatically marked as stale due to one year of inactivity. It will be closed in 7 days unless there’s further input. If you believe this PR is still relevant, please leave a comment or provide updated details. Thank you. (This is an automated message)"
|
||||
close-pr-message: "This PR has been automatically closed due to one year of inactivity. Thank you for your understanding! (This is an automated message)"
|
||||
operations-per-run: 300
|
||||
@@ -1,5 +1,9 @@
|
||||
name: NPM Publish
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
id-token: write
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
@@ -12,6 +16,7 @@ on:
|
||||
- ui-mcp
|
||||
- auth
|
||||
- commerce
|
||||
- checkout
|
||||
- brand
|
||||
- react
|
||||
- all
|
||||
@@ -26,7 +31,7 @@ on:
|
||||
|
||||
jobs:
|
||||
publish:
|
||||
runs-on: ubuntu-latest
|
||||
runs-on: hanzo-build-linux-amd64
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
@@ -38,75 +43,132 @@ jobs:
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: '20'
|
||||
cache: 'pnpm'
|
||||
registry-url: 'https://registry.npmjs.org'
|
||||
|
||||
- name: Install dependencies
|
||||
run: pnpm install --frozen-lockfile
|
||||
run: pnpm install
|
||||
|
||||
- name: Build packages
|
||||
run: |
|
||||
if [ "${{ github.event.inputs.package }}" = "all" ] || [ "${{ github.event.inputs.package }}" = "ui" ]; then
|
||||
cd pkg/ui && pnpm build
|
||||
cd pkgs/ui && pnpm build
|
||||
cd ../..
|
||||
fi
|
||||
if [ "${{ github.event.inputs.package }}" = "all" ] || [ "${{ github.event.inputs.package }}" = "ui-mcp" ]; then
|
||||
cd pkgs/ui-mcp && pnpm build
|
||||
cd ../..
|
||||
fi
|
||||
if [ "${{ github.event.inputs.package }}" = "all" ] || [ "${{ github.event.inputs.package }}" = "auth" ]; then
|
||||
cd pkg/auth && pnpm build
|
||||
cd pkgs/auth && pnpm build
|
||||
cd ../..
|
||||
fi
|
||||
if [ "${{ github.event.inputs.package }}" = "all" ] || [ "${{ github.event.inputs.package }}" = "commerce" ]; then
|
||||
cd pkg/commerce && pnpm build
|
||||
cd pkgs/commerce && pnpm build
|
||||
cd ../..
|
||||
fi
|
||||
if [ "${{ github.event.inputs.package }}" = "all" ] || [ "${{ github.event.inputs.package }}" = "checkout" ]; then
|
||||
cd pkgs/checkout && pnpm build
|
||||
cd ../..
|
||||
fi
|
||||
if [ "${{ github.event.inputs.package }}" = "all" ] || [ "${{ github.event.inputs.package }}" = "brand" ]; then
|
||||
cd pkg/brand && pnpm build
|
||||
cd pkgs/brand && pnpm build
|
||||
cd ../..
|
||||
fi
|
||||
if [ "${{ github.event.inputs.package }}" = "all" ] || [ "${{ github.event.inputs.package }}" = "react" ]; then
|
||||
cd pkg/react && pnpm build
|
||||
cd pkgs/react && pnpm build
|
||||
cd ../..
|
||||
fi
|
||||
|
||||
- name: Bump version and publish
|
||||
env:
|
||||
NODE_AUTH_TOKEN: ${{ secrets.NPM_AUTH_TOKEN }}
|
||||
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
|
||||
NPM_CONFIG_PROVENANCE: true
|
||||
run: |
|
||||
# Set up npm and pnpm auth
|
||||
npm config set //registry.npmjs.org/:_authToken $NODE_AUTH_TOKEN
|
||||
|
||||
echo "//registry.npmjs.org/:_authToken=${NODE_AUTH_TOKEN}" >> ~/.npmrc
|
||||
|
||||
publish_package() {
|
||||
local pkg_dir=$1
|
||||
cd $pkg_dir
|
||||
npm version ${{ github.event.inputs.version_bump }}
|
||||
npm publish --access public
|
||||
local pkg_name=$(cd $pkg_dir && node -p "require('./package.json').name")
|
||||
local pkg_version=$(cd $pkg_dir && node -p "require('./package.json').version")
|
||||
echo "=== Publishing $pkg_name@$pkg_version from $pkg_dir ==="
|
||||
|
||||
# Change to package directory
|
||||
pushd "$pkg_dir"
|
||||
|
||||
# Bump version manually using node to avoid any pnpm/npm hooks
|
||||
echo "Bumping version (${{ github.event.inputs.version_bump }})..."
|
||||
node -e "
|
||||
const fs = require('fs');
|
||||
const pkg = JSON.parse(fs.readFileSync('package.json', 'utf8'));
|
||||
const [major, minor, patch] = pkg.version.split('.').map(Number);
|
||||
const bump = '${{ github.event.inputs.version_bump }}';
|
||||
if (bump === 'major') pkg.version = (major + 1) + '.0.0';
|
||||
else if (bump === 'minor') pkg.version = major + '.' + (minor + 1) + '.0';
|
||||
else pkg.version = major + '.' + minor + '.' + (patch + 1);
|
||||
fs.writeFileSync('package.json', JSON.stringify(pkg, null, 2) + '\n');
|
||||
console.log('New version:', pkg.version);
|
||||
"
|
||||
|
||||
local new_version=$(node -p "require('./package.json').version")
|
||||
echo "Version is now: $new_version"
|
||||
|
||||
# Pack the tarball
|
||||
echo "Running pnpm pack..."
|
||||
pnpm pack --pack-gzip-level 9
|
||||
local tarball=$(ls -t *.tgz | head -1)
|
||||
echo "Created tarball: $tarball"
|
||||
|
||||
# Verify tarball contents
|
||||
echo "Tarball package.json:"
|
||||
tar -xzf "$tarball" -O package/package.json
|
||||
|
||||
# Copy to temp dir and publish
|
||||
local temp_dir="/tmp/publish-$$"
|
||||
mkdir -p "$temp_dir"
|
||||
cp "$tarball" "$temp_dir/"
|
||||
echo "Publishing from $temp_dir..."
|
||||
cd "$temp_dir"
|
||||
npm publish "$tarball" --access public
|
||||
cd -
|
||||
|
||||
# Cleanup
|
||||
rm -rf "$temp_dir"
|
||||
rm -f "$tarball"
|
||||
popd
|
||||
}
|
||||
|
||||
case "${{ github.event.inputs.package }}" in
|
||||
ui)
|
||||
publish_package "pkg/ui"
|
||||
publish_package "pkgs/ui"
|
||||
;;
|
||||
ui-mcp)
|
||||
publish_package "pkg/ui-mcp"
|
||||
publish_package "pkgs/ui-mcp"
|
||||
;;
|
||||
auth)
|
||||
publish_package "pkg/auth"
|
||||
publish_package "pkgs/auth"
|
||||
;;
|
||||
commerce)
|
||||
publish_package "pkg/commerce"
|
||||
publish_package "pkgs/commerce"
|
||||
;;
|
||||
checkout)
|
||||
publish_package "pkgs/checkout"
|
||||
;;
|
||||
brand)
|
||||
publish_package "pkg/brand"
|
||||
publish_package "pkgs/brand"
|
||||
;;
|
||||
react)
|
||||
publish_package "pkg/react"
|
||||
publish_package "pkgs/react"
|
||||
;;
|
||||
all)
|
||||
publish_package "pkg/ui"
|
||||
publish_package "pkg/ui-mcp"
|
||||
publish_package "pkg/auth"
|
||||
publish_package "pkg/commerce"
|
||||
publish_package "pkg/brand"
|
||||
publish_package "pkg/react"
|
||||
publish_package "pkgs/ui"
|
||||
publish_package "pkgs/ui-mcp"
|
||||
publish_package "pkgs/auth"
|
||||
publish_package "pkgs/commerce"
|
||||
publish_package "pkgs/checkout"
|
||||
publish_package "pkgs/brand"
|
||||
publish_package "pkgs/react"
|
||||
;;
|
||||
esac
|
||||
|
||||
|
||||
@@ -10,9 +10,9 @@ on:
|
||||
jobs:
|
||||
comment:
|
||||
if: |
|
||||
github.repository_owner == 'hanzoai-ui' &&
|
||||
github.repository_owner == 'shadcn-ui' &&
|
||||
${{ github.event.workflow_run.conclusion == 'success' }}
|
||||
runs-on: ubuntu-latest
|
||||
runs-on: hanzo-build-linux-amd64
|
||||
name: Write comment to the PR
|
||||
steps:
|
||||
- name: "Comment on PR"
|
||||
@@ -28,8 +28,8 @@ jobs:
|
||||
|
||||
for (const artifact of allArtifacts.data.artifacts) {
|
||||
// Extract the PR number and package version from the artifact name
|
||||
const match = /^npm-package-hanzoai-ui@(.*?)-pr-(\d+)/.exec(artifact.name);
|
||||
|
||||
const match = /^npm-package-shadcn@(.*?)-pr-(\d+)/.exec(artifact.name);
|
||||
|
||||
if (match) {
|
||||
require("fs").appendFileSync(
|
||||
process.env.GITHUB_ENV,
|
||||
@@ -49,7 +49,7 @@ jobs:
|
||||
A new prerelease is available for testing:
|
||||
|
||||
```sh
|
||||
npx hanzoai-ui@${{ env.BETA_PACKAGE_VERSION }}
|
||||
pnpm dlx shadcn@${{ env.BETA_PACKAGE_VERSION }}
|
||||
```
|
||||
|
||||
- name: "Remove the autorelease label once published"
|
||||
|
||||
@@ -7,31 +7,39 @@ on:
|
||||
types: [labeled]
|
||||
branches:
|
||||
- main
|
||||
|
||||
permissions:
|
||||
id-token: write
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
prerelease:
|
||||
if: |
|
||||
github.repository_owner == 'hanzoai' &&
|
||||
github.repository_owner == 'shadcn-ui' &&
|
||||
contains(github.event.pull_request.labels.*.name, '🚀 autorelease')
|
||||
name: Build & Publish a beta release to NPM
|
||||
runs-on: ubuntu-latest
|
||||
runs-on: hanzo-build-linux-amd64
|
||||
environment: Preview
|
||||
|
||||
steps:
|
||||
- name: Checkout Repo
|
||||
uses: actions/checkout@v3
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Use PNPM
|
||||
uses: pnpm/action-setup@v2.2.4
|
||||
uses: pnpm/action-setup@v4
|
||||
with:
|
||||
version: 8.6.1
|
||||
version: 9.0.6
|
||||
|
||||
- name: Use Node.js 18
|
||||
uses: actions/setup-node@v3
|
||||
- name: Use Node.js 20
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 18
|
||||
cache: "pnpm"
|
||||
node-version: 20
|
||||
registry-url: "https://registry.npmjs.org"
|
||||
|
||||
- name: Update npm for OIDC support
|
||||
run: npm install -g npm@latest
|
||||
|
||||
- name: Install NPM Dependencies
|
||||
run: pnpm install
|
||||
@@ -39,11 +47,6 @@ jobs:
|
||||
- name: Modify package.json version
|
||||
run: node .github/version-script-beta.js
|
||||
|
||||
- name: Authenticate to NPM
|
||||
run: echo "//registry.npmjs.org/:_authToken=$NPM_ACCESS_TOKEN" >> pkg/cli/.npmrc
|
||||
env:
|
||||
NPM_ACCESS_TOKEN: ${{ secrets.NPM_ACCESS_TOKEN }}
|
||||
|
||||
- name: Publish Beta to NPM
|
||||
run: pnpm pub:beta
|
||||
|
||||
@@ -51,10 +54,10 @@ jobs:
|
||||
id: package-version
|
||||
uses: martinbeentjes/npm-get-version-action@main
|
||||
with:
|
||||
path: pkg/cli
|
||||
path: pkgs/shadcn
|
||||
|
||||
- name: Upload packaged artifact
|
||||
uses: actions/upload-artifact@v2
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: npm-package-hanzoai-ui@${{ steps.package-version.outputs.current-version }}-pr-${{ github.event.number }} # encode the PR number into the artifact name
|
||||
path: pkg/cli/dist/index.js
|
||||
name: npm-package-shadcn@${{ steps.package-version.outputs.current-version }}-pr-${{ github.event.number }} # encode the PR number into the artifact name
|
||||
path: pkgs/shadcn/dist/index.js
|
||||
|
||||
@@ -8,7 +8,7 @@ on:
|
||||
jobs:
|
||||
test:
|
||||
name: Run Tests
|
||||
runs-on: ubuntu-latest
|
||||
runs-on: hanzo-build-linux-amd64
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
@@ -20,29 +20,27 @@ jobs:
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: '20'
|
||||
cache: 'pnpm'
|
||||
|
||||
- name: Install dependencies
|
||||
run: pnpm install --frozen-lockfile
|
||||
run: pnpm install --no-frozen-lockfile
|
||||
|
||||
- name: Build packages
|
||||
run: |
|
||||
cd pkg/ui && pnpm build && cd ../..
|
||||
cd pkg/auth && pnpm build && cd ../..
|
||||
cd pkg/commerce && pnpm build && cd ../..
|
||||
cd pkg/brand && pnpm build && cd ../..
|
||||
cd pkg/react && pnpm build && cd ../..
|
||||
cd pkgs/ui && pnpm build && cd ../..
|
||||
cd pkgs/commerce && pnpm build && cd ../..
|
||||
cd pkgs/brand && pnpm build && cd ../..
|
||||
cd pkgs/react && pnpm build && cd ../..
|
||||
|
||||
- name: Run tests
|
||||
run: |
|
||||
cd pkg/ui && pnpm test
|
||||
cd pkgs/ui && pnpm test
|
||||
cd ../react && pnpm test
|
||||
cd ../..
|
||||
|
||||
publish:
|
||||
name: Publish to NPM
|
||||
needs: test
|
||||
runs-on: ubuntu-latest
|
||||
runs-on: hanzo-build-linux-amd64
|
||||
permissions:
|
||||
contents: write
|
||||
id-token: write
|
||||
@@ -57,30 +55,28 @@ jobs:
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: '20'
|
||||
cache: 'pnpm'
|
||||
registry-url: 'https://registry.npmjs.org'
|
||||
|
||||
- name: Install dependencies
|
||||
run: pnpm install --frozen-lockfile
|
||||
run: pnpm install --no-frozen-lockfile
|
||||
|
||||
- name: Build all packages
|
||||
run: |
|
||||
cd pkg/ui && pnpm build && cd ../..
|
||||
cd pkg/auth && pnpm build && cd ../..
|
||||
cd pkg/commerce && pnpm build && cd ../..
|
||||
cd pkg/brand && pnpm build && cd ../..
|
||||
cd pkg/react && pnpm build && cd ../..
|
||||
cd pkgs/ui && pnpm build && cd ../..
|
||||
cd pkgs/commerce && pnpm build && cd ../..
|
||||
cd pkgs/brand && pnpm build && cd ../..
|
||||
cd pkgs/react && pnpm build && cd ../..
|
||||
|
||||
- name: Configure npm authentication
|
||||
env:
|
||||
NODE_AUTH_TOKEN: ${{ secrets.NPM_AUTH_TOKEN }}
|
||||
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
|
||||
run: |
|
||||
npm config set //registry.npmjs.org/:_authToken $NODE_AUTH_TOKEN
|
||||
npm whoami
|
||||
|
||||
- name: Check and publish packages
|
||||
env:
|
||||
NODE_AUTH_TOKEN: ${{ secrets.NPM_AUTH_TOKEN }}
|
||||
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
|
||||
run: |
|
||||
echo "Checking all packages for unpublished versions..."
|
||||
|
||||
@@ -88,8 +84,8 @@ jobs:
|
||||
SKIPPED_COUNT=0
|
||||
PUBLISHED_PACKAGES=""
|
||||
|
||||
for package in ui auth commerce brand react; do
|
||||
cd "pkg/$package"
|
||||
for package in ui commerce brand react; do
|
||||
cd "pkgs/$package"
|
||||
|
||||
CURRENT_VERSION=$(node -p "require('./package.json').version")
|
||||
PACKAGE_NAME=$(node -p "require('./package.json').name")
|
||||
@@ -146,7 +142,7 @@ jobs:
|
||||
|
||||
```bash
|
||||
# Install latest versions
|
||||
npm install @hanzo/ui @hanzo/auth @hanzo/commerce @hanzo/brand @hanzo/react
|
||||
npm install @hanzo/ui @hanzo/commerce @hanzo/brand @hanzo/react
|
||||
```
|
||||
files: |
|
||||
CHANGELOG.md
|
||||
|
||||
@@ -0,0 +1,86 @@
|
||||
name: Publish Packages
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [main]
|
||||
paths:
|
||||
- 'pkgs/*/package.json'
|
||||
- 'pkgs/*/package.json'
|
||||
|
||||
jobs:
|
||||
detect-changes:
|
||||
runs-on: hanzo-build-linux-amd64
|
||||
outputs:
|
||||
packages: ${{ steps.changed.outputs.packages }}
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 2
|
||||
|
||||
- name: Detect changed packages
|
||||
id: changed
|
||||
run: |
|
||||
CHANGED=()
|
||||
for pkg_json in pkgs/*/package.json pkgs/*/package.json; do
|
||||
[ ! -f "$pkg_json" ] && continue
|
||||
pkg_dir=$(dirname "$pkg_json")
|
||||
|
||||
# Skip private packages
|
||||
IS_PRIVATE=$(jq -r '.private // false' "$pkg_json")
|
||||
[ "$IS_PRIVATE" = "true" ] && continue
|
||||
|
||||
# Only publish @hanzo/* packages
|
||||
PKG_NAME=$(jq -r '.name' "$pkg_json")
|
||||
case "$PKG_NAME" in
|
||||
@hanzo/*) ;;
|
||||
*) continue ;;
|
||||
esac
|
||||
|
||||
# Check version changed
|
||||
if git diff HEAD~1 HEAD --quiet "$pkg_json" 2>/dev/null; then
|
||||
continue
|
||||
fi
|
||||
|
||||
OLD=$(git show HEAD~1:"$pkg_json" 2>/dev/null | jq -r '.version' || echo "0.0.0")
|
||||
NEW=$(jq -r '.version' "$pkg_json")
|
||||
|
||||
if [ "$OLD" != "$NEW" ]; then
|
||||
echo "Version change: $PKG_NAME ($OLD -> $NEW)"
|
||||
CHANGED+=("$(basename "$pkg_dir")")
|
||||
fi
|
||||
done
|
||||
|
||||
if [ ${#CHANGED[@]} -eq 0 ]; then
|
||||
echo "packages=[]" >> $GITHUB_OUTPUT
|
||||
else
|
||||
JSON=$(printf '%s\n' "${CHANGED[@]}" | jq -R . | jq -s -c .)
|
||||
echo "packages=$JSON" >> $GITHUB_OUTPUT
|
||||
fi
|
||||
|
||||
publish:
|
||||
needs: detect-changes
|
||||
if: needs.detect-changes.outputs.packages != '[]'
|
||||
runs-on: hanzo-build-linux-amd64
|
||||
strategy:
|
||||
matrix:
|
||||
package: ${{ fromJson(needs.detect-changes.outputs.packages) }}
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- uses: pnpm/action-setup@v4
|
||||
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 20
|
||||
registry-url: 'https://registry.npmjs.org'
|
||||
|
||||
- name: Install dependencies
|
||||
run: pnpm install
|
||||
|
||||
- name: Build package
|
||||
run: pnpm --filter ${{ matrix.package }} build
|
||||
|
||||
- name: Publish package
|
||||
run: pnpm --filter ${{ matrix.package }} publish --no-git-checks --access public
|
||||
env:
|
||||
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
|
||||
@@ -7,11 +7,16 @@ on:
|
||||
branches:
|
||||
- main
|
||||
|
||||
permissions:
|
||||
id-token: write
|
||||
contents: write
|
||||
pull-requests: write
|
||||
|
||||
jobs:
|
||||
release:
|
||||
if: ${{ github.repository_owner == 'hanzoai-ui' }}
|
||||
if: ${{ github.repository_owner == 'shadcn-ui' }}
|
||||
name: Create a PR for release workflow
|
||||
runs-on: ubuntu-latest
|
||||
runs-on: hanzo-build-linux-amd64
|
||||
steps:
|
||||
- name: Checkout Repo
|
||||
uses: actions/checkout@v3
|
||||
@@ -19,16 +24,18 @@ jobs:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Use PNPM
|
||||
uses: pnpm/action-setup@v2.2.4
|
||||
uses: pnpm/action-setup@v4
|
||||
with:
|
||||
version: 8.6.1
|
||||
version: 9.0.6
|
||||
|
||||
- name: Use Node.js 18
|
||||
uses: actions/setup-node@v3
|
||||
- name: Use Node.js 20
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
version: 8.6.1
|
||||
node-version: 18
|
||||
cache: "pnpm"
|
||||
node-version: 20
|
||||
registry-url: "https://registry.npmjs.org"
|
||||
|
||||
- name: Update npm for OIDC support
|
||||
run: npm install -g npm@latest
|
||||
|
||||
- name: Install NPM Dependencies
|
||||
run: pnpm install
|
||||
@@ -37,11 +44,11 @@ jobs:
|
||||
# run: pnpm check
|
||||
|
||||
- name: Build the package
|
||||
run: pnpm build:cli
|
||||
run: pnpm shadcn:build
|
||||
|
||||
- name: Create Version PR or Publish to NPM
|
||||
id: changesets
|
||||
uses: changesets/action@v1.4.1
|
||||
uses: changesets/action@v1
|
||||
with:
|
||||
commit: "chore(release): version packages"
|
||||
title: "chore(release): version packages"
|
||||
@@ -49,5 +56,4 @@ jobs:
|
||||
publish: npx changeset publish
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
NPM_TOKEN: ${{ secrets.NPM_ACCESS_TOKEN }}
|
||||
NODE_ENV: "production"
|
||||
|
||||
@@ -1,100 +0,0 @@
|
||||
name: Sync Forks
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [main]
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
fork:
|
||||
description: 'Fork to sync (luxfi or zoo or all)'
|
||||
required: false
|
||||
default: 'all'
|
||||
|
||||
jobs:
|
||||
sync-luxfi:
|
||||
if: github.event.inputs.fork == 'luxfi' || github.event.inputs.fork == 'all' || github.event_name == 'push'
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout Hanzo UI
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Setup Git
|
||||
run: |
|
||||
git config --global user.name "Hanzo Bot"
|
||||
git config --global user.email "bot@hanzo.ai"
|
||||
|
||||
- name: Clone Luxfi Fork
|
||||
run: |
|
||||
git clone https://github.com/luxfi/ui.git luxfi-ui
|
||||
cd luxfi-ui
|
||||
git remote add upstream https://github.com/hanzoai/ui.git
|
||||
git fetch upstream
|
||||
|
||||
- name: Sync and Rebrand
|
||||
run: |
|
||||
cd luxfi-ui
|
||||
# Merge upstream changes
|
||||
git checkout main
|
||||
git merge upstream/main --no-edit || true
|
||||
|
||||
# Run rebrand script
|
||||
cp ../scripts/rebrand.sh ./
|
||||
chmod +x rebrand.sh
|
||||
./rebrand.sh luxfi
|
||||
|
||||
# Commit changes
|
||||
git add -A
|
||||
git commit -m "sync: Update from hanzoai/ui and rebrand for Luxfi" || true
|
||||
|
||||
- name: Push to Luxfi
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.FORK_SYNC_TOKEN }}
|
||||
run: |
|
||||
cd luxfi-ui
|
||||
git push https://$GITHUB_TOKEN@github.com/luxfi/ui.git main || true
|
||||
|
||||
sync-zoo:
|
||||
if: github.event.inputs.fork == 'zoo' || github.event.inputs.fork == 'all' || github.event_name == 'push'
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout Hanzo UI
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Setup Git
|
||||
run: |
|
||||
git config --global user.name "Hanzo Bot"
|
||||
git config --global user.email "bot@hanzo.ai"
|
||||
|
||||
- name: Clone Zoo Fork
|
||||
run: |
|
||||
git clone https://github.com/zooai/ui.git zoo-ui
|
||||
cd zoo-ui
|
||||
git remote add upstream https://github.com/hanzoai/ui.git
|
||||
git fetch upstream
|
||||
|
||||
- name: Sync and Rebrand
|
||||
run: |
|
||||
cd zoo-ui
|
||||
# Merge upstream changes
|
||||
git checkout main
|
||||
git merge upstream/main --no-edit || true
|
||||
|
||||
# Run rebrand script
|
||||
cp ../scripts/rebrand.sh ./
|
||||
chmod +x rebrand.sh
|
||||
./rebrand.sh zoo
|
||||
|
||||
# Commit changes
|
||||
git add -A
|
||||
git commit -m "sync: Update from hanzoai/ui and rebrand for Zoo" || true
|
||||
|
||||
- name: Push to Zoo
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.FORK_SYNC_TOKEN }}
|
||||
run: |
|
||||
cd zoo-ui
|
||||
git push https://$GITHUB_TOKEN@github.com/zooai/ui.git main || true
|
||||
+30
-43
@@ -1,58 +1,45 @@
|
||||
name: E2E and Visual Tests
|
||||
name: Test
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [main, develop]
|
||||
pull_request:
|
||||
branches: [main, develop]
|
||||
schedule:
|
||||
# Run tests daily at 2 AM UTC to catch breaking updates
|
||||
- cron: '0 2 * * *'
|
||||
branches: ["*"]
|
||||
|
||||
jobs:
|
||||
test:
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
matrix:
|
||||
node-version: [20.x]
|
||||
|
||||
runs-on: hanzo-build-linux-amd64
|
||||
name: pnpm test
|
||||
env:
|
||||
NEXT_PUBLIC_APP_URL: http://localhost:4000
|
||||
NEXT_PUBLIC_V0_URL: https://v0.dev
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@v3
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Setup pnpm
|
||||
uses: pnpm/action-setup@v2
|
||||
- name: Install Node.js
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: 22
|
||||
|
||||
- uses: pnpm/action-setup@v4
|
||||
name: Install pnpm
|
||||
id: pnpm-install
|
||||
with:
|
||||
version: 9.0.6
|
||||
run_install: false
|
||||
|
||||
- name: Setup Node.js ${{ matrix.node-version }}
|
||||
uses: actions/setup-node@v4
|
||||
- name: Get pnpm store directory
|
||||
id: pnpm-cache
|
||||
run: |
|
||||
echo "pnpm_cache_dir=$(pnpm store path)" >> $GITHUB_OUTPUT
|
||||
- uses: actions/cache@v3
|
||||
name: Setup pnpm cache
|
||||
with:
|
||||
node-version: ${{ matrix.node-version }}
|
||||
cache: 'pnpm'
|
||||
|
||||
path: ${{ steps.pnpm-cache.outputs.pnpm_cache_dir }}
|
||||
key: ${{ runner.os }}-pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-pnpm-store-
|
||||
- name: Install dependencies
|
||||
run: pnpm install
|
||||
|
||||
- name: Install Playwright browsers
|
||||
run: pnpm exec playwright install --with-deps
|
||||
|
||||
- name: Build the project
|
||||
run: pnpm build
|
||||
|
||||
- name: Run component health check
|
||||
run: pnpm health-check
|
||||
continue-on-error: true
|
||||
|
||||
- name: Run E2E tests
|
||||
run: pnpm test:e2e
|
||||
|
||||
- name: Run visual regression tests
|
||||
run: pnpm test:visual
|
||||
|
||||
- name: Upload test results
|
||||
if: always()
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: playwright-report
|
||||
path: tests/reports/playwright-report/
|
||||
retention-days: 30
|
||||
- run: pnpm test
|
||||
|
||||
@@ -0,0 +1,129 @@
|
||||
name: Validate Registries
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
paths:
|
||||
- "apps/v4/public/r/registries.json"
|
||||
- "apps/v4/registry/directory.json"
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
paths:
|
||||
- "apps/v4/public/r/registries.json"
|
||||
- "apps/v4/registry/directory.json"
|
||||
|
||||
jobs:
|
||||
check-registry-sync:
|
||||
if: github.event_name == 'pull_request'
|
||||
runs-on: hanzo-build-linux-amd64
|
||||
name: check-registry-sync
|
||||
permissions:
|
||||
contents: read
|
||||
pull-requests: write
|
||||
steps:
|
||||
- name: Check changed files
|
||||
id: changed
|
||||
env:
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
run: |
|
||||
CHANGED_FILES=$(gh pr diff ${{ github.event.pull_request.number }} --repo ${{ github.repository }} --name-only)
|
||||
|
||||
DIRECTORY_CHANGED=false
|
||||
REGISTRIES_CHANGED=false
|
||||
|
||||
if echo "$CHANGED_FILES" | grep -q "^apps/v4/registry/directory.json$"; then
|
||||
DIRECTORY_CHANGED=true
|
||||
fi
|
||||
|
||||
if echo "$CHANGED_FILES" | grep -q "^apps/v4/public/r/registries.json$"; then
|
||||
REGISTRIES_CHANGED=true
|
||||
fi
|
||||
|
||||
echo "directory_changed=$DIRECTORY_CHANGED" >> $GITHUB_OUTPUT
|
||||
echo "registries_changed=$REGISTRIES_CHANGED" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Flag missing registries.json update
|
||||
if: steps.changed.outputs.directory_changed == 'true' && steps.changed.outputs.registries_changed == 'false'
|
||||
env:
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
run: |
|
||||
gh pr edit ${{ github.event.pull_request.number }} --repo ${{ github.repository }} --add-label "registries: invalid"
|
||||
gh pr comment ${{ github.event.pull_request.number }} --repo ${{ github.repository }} --body "can you run \`pnpm registry:build\` and commit the json files please?"
|
||||
exit 1
|
||||
|
||||
validate:
|
||||
runs-on: hanzo-build-linux-amd64
|
||||
name: pnpm validate:registries
|
||||
permissions:
|
||||
contents: read
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Install Node.js
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: 20
|
||||
|
||||
- name: Block reserved registry namespaces
|
||||
env:
|
||||
RESERVED_NAMESPACES: "@shadcn,@ui,@blocks,@components,@block,@component,@util,@utils,@registry,@lib,@hook,@hooks,@theme,@themes,@chart,@charts"
|
||||
run: |
|
||||
node <<'EOF'
|
||||
const fs = require("node:fs")
|
||||
|
||||
const files = [
|
||||
"apps/v4/public/r/registries.json",
|
||||
"apps/v4/registry/directory.json",
|
||||
]
|
||||
const reservedNamespaces = new Set(
|
||||
process.env.RESERVED_NAMESPACES.split(",").filter(Boolean)
|
||||
)
|
||||
|
||||
function readNames(filePath) {
|
||||
return JSON.parse(fs.readFileSync(filePath, "utf8")).map(
|
||||
(entry) => entry.name
|
||||
)
|
||||
}
|
||||
|
||||
const violations = files.flatMap((filePath) => {
|
||||
return readNames(filePath)
|
||||
.filter((name) => reservedNamespaces.has(name))
|
||||
.map((name) => `${filePath}: ${name}`)
|
||||
})
|
||||
|
||||
if (violations.length > 0) {
|
||||
console.error("Reserved registry namespaces are not allowed:")
|
||||
|
||||
for (const violation of violations) {
|
||||
console.error(`- ${violation}`)
|
||||
}
|
||||
|
||||
process.exit(1)
|
||||
}
|
||||
EOF
|
||||
|
||||
- uses: pnpm/action-setup@v4
|
||||
name: Install pnpm
|
||||
id: pnpm-install
|
||||
with:
|
||||
version: 9.0.6
|
||||
run_install: false
|
||||
|
||||
- name: Get pnpm store directory
|
||||
id: pnpm-cache
|
||||
run: |
|
||||
echo "pnpm_cache_dir=$(pnpm store path)" >> $GITHUB_OUTPUT
|
||||
- uses: actions/cache@v3
|
||||
name: Setup pnpm cache
|
||||
with:
|
||||
path: ${{ steps.pnpm-cache.outputs.pnpm_cache_dir }}
|
||||
key: ${{ runner.os }}-pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-pnpm-store-
|
||||
- name: Install dependencies
|
||||
run: pnpm install
|
||||
|
||||
- name: Validate registries
|
||||
run: pnpm --filter=v4 validate:registries
|
||||
+10
@@ -5,6 +5,7 @@ node_modules
|
||||
**/node_modules
|
||||
.pnp
|
||||
.pnp.js
|
||||
/pnpm-lock.yaml
|
||||
|
||||
# testing
|
||||
coverage
|
||||
@@ -72,3 +73,12 @@ test-results/
|
||||
playwright-report/
|
||||
*BADGE_INSPECTION*.md
|
||||
*BADGE_INSPECTION*.txt
|
||||
|
||||
# ide
|
||||
.idea
|
||||
.fleet
|
||||
|
||||
.notes
|
||||
.claude/settings.local.json
|
||||
pkg/ui/dist-test/
|
||||
app/public/api/
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
# .kodiak.toml
|
||||
version = 1
|
||||
|
||||
[merge]
|
||||
automerge_label = "automerge"
|
||||
require_automerge_label = true
|
||||
method = "squash"
|
||||
delete_branch_on_merge = true
|
||||
optimistic_updates = false
|
||||
prioritize_ready_to_merge = true
|
||||
notify_on_conflict = true
|
||||
|
||||
[merge.message]
|
||||
title = "pull_request_title"
|
||||
body = "pull_request_body"
|
||||
include_pr_number = true
|
||||
body_type = "markdown"
|
||||
strip_html_comments = true
|
||||
@@ -0,0 +1,3 @@
|
||||
auto-install-peers=true
|
||||
link-workspace-packages=true
|
||||
puppeteer_skip_download=true
|
||||
@@ -0,0 +1,8 @@
|
||||
dist
|
||||
node_modules
|
||||
.next
|
||||
build
|
||||
.contentlayer
|
||||
**/fixtures
|
||||
deprecated
|
||||
apps/v4/registry/styles/**/*.css
|
||||
Vendored
+20
-1
@@ -1,5 +1,24 @@
|
||||
{
|
||||
"typescript.tsdk": "node_modules/.pnpm/typescript@5.3.3/node_modules/typescript/lib",
|
||||
"editor.gotoLocation.multipleDefinitions": "goto",
|
||||
"typescript.preferences.quoteStyle": "single"
|
||||
"typescript.preferences.quoteStyle": "single",
|
||||
"eslint.workingDirectories": [
|
||||
{ "pattern": "apps/*/" },
|
||||
{ "pattern": "pkgs/*/" },
|
||||
{ "pattern": "pkgs/*/" }
|
||||
],
|
||||
"tailwindCSS.classFunctions": ["cva", "cn"],
|
||||
"vitest.debugExclude": [
|
||||
"<node_internals>/**",
|
||||
"**/node_modules/**",
|
||||
"**/fixtures/**"
|
||||
],
|
||||
"files.exclude": {
|
||||
"deprecated": true
|
||||
},
|
||||
"search.exclude": {
|
||||
"apps/v4/registry/radix-*": true,
|
||||
"apps/v4/public/r/*": true,
|
||||
"pkgs/shadcn/test/fixtures/*": true
|
||||
}
|
||||
}
|
||||
|
||||
+30
-48
@@ -1,10 +1,10 @@
|
||||
# Contributing
|
||||
|
||||
Thanks for your interest in contributing to ui.hanzo.com. We're happy to have you here.
|
||||
Thanks for your interest in contributing to ui.shadcn.com. We're happy to have you here.
|
||||
|
||||
Please take a moment to review this document before submitting your first pull request. We also strongly recommend that you check for open issues and pull requests to see if someone else is working on something similar.
|
||||
|
||||
If you need any help, feel free to reach out to [@hanzo](https://x.com/hanzoai).
|
||||
If you need any help, feel free to reach out to [@shadcn](https://twitter.com/shadcn).
|
||||
|
||||
## About this repository
|
||||
|
||||
@@ -20,28 +20,25 @@ This repository is structured as follows:
|
||||
|
||||
```
|
||||
apps
|
||||
└── www
|
||||
└── v4
|
||||
├── app
|
||||
├── components
|
||||
├── content
|
||||
└── registry
|
||||
├── default
|
||||
│ ├── example
|
||||
│ └── ui
|
||||
└── new-york
|
||||
└── new-york-v4
|
||||
├── example
|
||||
└── ui
|
||||
packages
|
||||
└── cli
|
||||
└── shadcn
|
||||
```
|
||||
|
||||
| Path | Description |
|
||||
| --------------------- | ---------------------------------------- |
|
||||
| `apps/www/app` | The Next.js application for the website. |
|
||||
| `apps/www/components` | The React components for the website. |
|
||||
| `apps/www/content` | The content for the website. |
|
||||
| `apps/www/registry` | The registry for the components. |
|
||||
| `packages/cli` | The `hanzo-ui` package. |
|
||||
| Path | Description |
|
||||
| -------------------- | ---------------------------------------- |
|
||||
| `apps/v4/app` | The Next.js application for the website. |
|
||||
| `apps/v4/components` | The React components for the website. |
|
||||
| `apps/v4/content` | The content for the website. |
|
||||
| `apps/v4/registry` | The registry for the components. |
|
||||
| `packages/shadcn` | The `shadcn` package. |
|
||||
|
||||
## Development
|
||||
|
||||
@@ -79,76 +76,61 @@ You can use the `pnpm --filter=[WORKSPACE]` command to start the development pro
|
||||
|
||||
#### Examples
|
||||
|
||||
1. To run the `ui.hanzo.com` website:
|
||||
1. To run the `ui.shadcn.com` website:
|
||||
|
||||
```bash
|
||||
pnpm --filter=www dev
|
||||
pnpm --filter=v4 dev
|
||||
```
|
||||
|
||||
2. To run the `hanzo-ui` package:
|
||||
2. To run the `shadcn` package:
|
||||
|
||||
```bash
|
||||
pnpm --filter=hanzo-ui dev
|
||||
pnpm --filter=shadcn dev
|
||||
```
|
||||
|
||||
## Running the CLI Locally
|
||||
|
||||
To run the CLI locally, you can follow the workflow:
|
||||
|
||||
1. Start by running the registry (main site) to make sure the components are up to date:
|
||||
1. Start by running the dev server:
|
||||
|
||||
```bash
|
||||
pnpm v4:dev
|
||||
pnpm dev
|
||||
```
|
||||
|
||||
2. Run the development script for the CLI:
|
||||
2. In another terminal tab, test the CLI by running:
|
||||
|
||||
```bash
|
||||
pnpm hanzo:dev
|
||||
```
|
||||
|
||||
3. In another terminal tab, test the CLI by running:
|
||||
|
||||
```bash
|
||||
pnpm hanzo
|
||||
pnpm shadcn
|
||||
```
|
||||
|
||||
To test the CLI in a specific app, use a command like:
|
||||
|
||||
```bash
|
||||
pnpm hanzo <init | add | ...> -c ~/Desktop/my-app
|
||||
```
|
||||
|
||||
4. To run the tests for the CLI:
|
||||
|
||||
```bash
|
||||
pnpm --filter=hanzo test
|
||||
pnpm shadcn <init | add | ...> -c ~/Desktop/my-app
|
||||
```
|
||||
|
||||
This workflow ensures that you are running the most recent version of the registry and testing the CLI properly in your local environment.
|
||||
|
||||
## Documentation
|
||||
|
||||
The documentation for this project is located in the `www` workspace. You can run the documentation locally by running the following command:
|
||||
The documentation for this project is located in the `v4` workspace. You can run the documentation locally by running the following command:
|
||||
|
||||
```bash
|
||||
pnpm --filter=www dev
|
||||
pnpm --filter=v4 dev
|
||||
```
|
||||
|
||||
Documentation is written using [MDX](https://mdxjs.com). You can find the documentation files in the `apps/www/content/docs` directory.
|
||||
Documentation is written using [MDX](https://mdxjs.com). You can find the documentation files in the `apps/v4/content/docs` directory.
|
||||
|
||||
## Components
|
||||
|
||||
We use a registry system for developing components. You can find the source code for the components under `apps/www/registry`. The components are organized by styles.
|
||||
We use a registry system for developing components. You can find the source code for the components under `apps/v4/registry`. The components are organized by styles.
|
||||
|
||||
```bash
|
||||
apps
|
||||
└── www
|
||||
└── v4
|
||||
└── registry
|
||||
├── default
|
||||
│ ├── example
|
||||
│ └── ui
|
||||
└── new-york
|
||||
└── new-york-v4
|
||||
├── example
|
||||
└── ui
|
||||
```
|
||||
@@ -157,7 +139,7 @@ When adding or modifying components, please ensure that:
|
||||
|
||||
1. You make the changes for every style.
|
||||
2. You update the documentation.
|
||||
3. You run `pnpm build:registry` to update the registry.
|
||||
3. You run `pnpm registry:build` to update the registry.
|
||||
|
||||
## Commit Convention
|
||||
|
||||
@@ -196,9 +178,9 @@ If you have a request for a new component, please open a discussion on GitHub. W
|
||||
|
||||
## CLI
|
||||
|
||||
The `hanzo-ui` package is a CLI for adding components to your project. You can find the documentation for the CLI [here](https://ui.hanzo.com/docs/cli).
|
||||
The `shadcn` package is a CLI for adding components to your project. You can find the documentation for the CLI [here](https://ui.shadcn.com/docs/cli).
|
||||
|
||||
Any changes to the CLI should be made in the `packages/cli` directory. If you can, it would be great if you could add tests for your changes.
|
||||
Any changes to the CLI should be made in the `packages/shadcn` directory. If you can, it would be great if you could add tests for your changes.
|
||||
|
||||
## Testing
|
||||
|
||||
|
||||
@@ -0,0 +1,458 @@
|
||||
# UI/UX Improvements - Implementation Summary
|
||||
**Date**: 2025-11-13
|
||||
**Status**: Phase 1 Complete ✅
|
||||
|
||||
## 🎯 Overview
|
||||
|
||||
Following a comprehensive UI/UX review using Playwright automated testing, we've identified and begun implementing **12 high-impact improvements** to elevate Hanzo UI to world-class standards. This document tracks what's been completed and what remains.
|
||||
|
||||
---
|
||||
|
||||
## ✅ Completed Implementations
|
||||
|
||||
### 1. Skip Links for Accessibility (CRITICAL) ✅
|
||||
**File**: `app/components/skip-links.tsx`
|
||||
|
||||
**What it does**:
|
||||
- Provides keyboard navigation shortcuts for screen reader users
|
||||
- Meets WCAG Level A compliance requirements
|
||||
- Hidden by default, visible when focused (Tab key)
|
||||
|
||||
**Implementation**:
|
||||
```tsx
|
||||
<SkipLinks />
|
||||
// Renders:
|
||||
// - Skip to main content
|
||||
// - Skip to navigation
|
||||
```
|
||||
|
||||
**Impact**:
|
||||
- ✅ WCAG Level A compliant
|
||||
- ✅ Screen reader accessible
|
||||
- ✅ Keyboard navigation improved
|
||||
- ✅ Zero visual impact (only shows on focus)
|
||||
|
||||
**Integrated in**: `app/layout.tsx` - Active globally across all pages
|
||||
|
||||
---
|
||||
|
||||
### 2. Skeleton Loading Components ✅
|
||||
**File**: `app/components/skeleton-card.tsx`
|
||||
|
||||
**What it provides**:
|
||||
- `<SkeletonCard />` - Single card skeleton
|
||||
- `<SkeletonCardGrid />` - Grid of skeleton cards
|
||||
- `<SkeletonList />` - List view skeleton
|
||||
- `<SkeletonTable />` - Table skeleton
|
||||
|
||||
**Usage Example**:
|
||||
```tsx
|
||||
import { Suspense } from 'react'
|
||||
import { SkeletonCardGrid } from '@/components/skeleton-card'
|
||||
|
||||
<Suspense fallback={<SkeletonCardGrid count={6} />}>
|
||||
<ComponentGallery />
|
||||
</Suspense>
|
||||
```
|
||||
|
||||
**Impact**:
|
||||
- ⏩ Reduces perceived load time
|
||||
- 🎨 Professional loading states
|
||||
- ♿ Better accessibility (users know content is loading)
|
||||
- 📱 Works on all devices
|
||||
|
||||
**Next Step**: Integrate into:
|
||||
- `/blocks` page (for block gallery)
|
||||
- `/docs/components` page (for component list)
|
||||
- `/builder` page (for block library panel)
|
||||
|
||||
---
|
||||
|
||||
### 3. Enhanced Global Layout ✅
|
||||
**File**: `app/app/layout.tsx`
|
||||
|
||||
**Changes**:
|
||||
- ✅ Skip links added at the top of the component tree
|
||||
- ✅ Proper import order maintained
|
||||
- ✅ Zero breaking changes to existing functionality
|
||||
|
||||
**Before/After**:
|
||||
```tsx
|
||||
// Before
|
||||
<ActiveThemeProvider>
|
||||
<div vaul-drawer-wrapper="">
|
||||
{children}
|
||||
</div>
|
||||
</ActiveThemeProvider>
|
||||
|
||||
// After
|
||||
<ActiveThemeProvider>
|
||||
<SkipLinks />
|
||||
<div vaul-drawer-wrapper="">
|
||||
{children}
|
||||
</div>
|
||||
</ActiveThemeProvider>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 📋 Ready to Integrate (Created, Not Yet Used)
|
||||
|
||||
### Components Available for Use
|
||||
|
||||
1. **Skeleton Components** (`app/components/skeleton-card.tsx`)
|
||||
- Ready to wrap async content
|
||||
- Just import and use in Suspense boundaries
|
||||
|
||||
2. **Skip Links** (`app/components/skip-links.tsx`)
|
||||
- Already active globally
|
||||
- Need to add `id="main-content"` to main content areas
|
||||
|
||||
---
|
||||
|
||||
## 🚧 Next Phase - Ready for Implementation
|
||||
|
||||
### High Priority (Week 1-2)
|
||||
|
||||
#### 1. Add ID Attributes for Skip Links
|
||||
**Files to modify**:
|
||||
- `app/(app)/layout.tsx` - Add `id="main-content"` to main
|
||||
- `app/components/site-header.tsx` - Add `id="navigation"` to nav
|
||||
|
||||
```tsx
|
||||
// In layout
|
||||
<main id="main-content" className="flex-1">
|
||||
{children}
|
||||
</main>
|
||||
|
||||
// In header
|
||||
<nav id="navigation">
|
||||
<MainNav />
|
||||
</nav>
|
||||
```
|
||||
|
||||
**Effort**: 5 minutes
|
||||
**Impact**: High - Completes skip links functionality
|
||||
|
||||
---
|
||||
|
||||
#### 2. Integrate Skeleton Loaders
|
||||
**Target Pages**:
|
||||
|
||||
**A. Blocks Page** (`app/(app)/blocks/page.tsx`)
|
||||
```tsx
|
||||
import { Suspense } from 'react'
|
||||
import { SkeletonCardGrid } from '@/components/skeleton-card'
|
||||
|
||||
export default function BlocksPage() {
|
||||
return (
|
||||
<Suspense fallback={<SkeletonCardGrid count={9} />}>
|
||||
<BlockGallery />
|
||||
</Suspense>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
**B. Components Page** (`app/(app)/docs/components/page.tsx`)
|
||||
```tsx
|
||||
<Suspense fallback={<SkeletonList count={10} />}>
|
||||
<ComponentList />
|
||||
</Suspense>
|
||||
```
|
||||
|
||||
**C. Builder Page** (`app/(app)/builder/page.tsx`)
|
||||
```tsx
|
||||
// In the component library panel
|
||||
<Suspense fallback={<SkeletonCardGrid count={6} />}>
|
||||
<BlockLibrary />
|
||||
</Suspense>
|
||||
```
|
||||
|
||||
**Effort**: 30 minutes
|
||||
**Impact**: High - Dramatically improves perceived performance
|
||||
|
||||
---
|
||||
|
||||
#### 3. Performance Optimizations (CRITICAL)
|
||||
**Current**: 14.7s load time
|
||||
**Target**: <3s load time
|
||||
|
||||
**Action Items**:
|
||||
a. **Image Optimization**
|
||||
```bash
|
||||
# Convert images to WebP
|
||||
pnpm add sharp
|
||||
# Add next.config.js optimization
|
||||
images: {
|
||||
formats: ['image/webp', 'image/avif'],
|
||||
}
|
||||
```
|
||||
|
||||
b. **Code Splitting**
|
||||
```tsx
|
||||
// Dynamic imports for heavy components
|
||||
const HeavyComponent = dynamic(() => import('./heavy-component'), {
|
||||
loading: () => <SkeletonCard />,
|
||||
})
|
||||
```
|
||||
|
||||
c. **Lazy Loading**
|
||||
```tsx
|
||||
// Below-the-fold content
|
||||
import { lazy } from 'react'
|
||||
const Footer = lazy(() => import('./footer'))
|
||||
```
|
||||
|
||||
**Effort**: 4-6 hours
|
||||
**Impact**: CRITICAL - 79% faster page loads
|
||||
|
||||
---
|
||||
|
||||
### Medium Priority (Week 2-3)
|
||||
|
||||
#### 4. Toast Notification System
|
||||
**Package**: Already installed (`sonner`)
|
||||
**File**: `app/registry/default/ui/sonner.tsx` (already exists!)
|
||||
|
||||
**Usage**:
|
||||
```tsx
|
||||
import { toast } from 'sonner'
|
||||
|
||||
// Success
|
||||
toast.success('Component copied!')
|
||||
|
||||
// Error
|
||||
toast.error('Failed to load', {
|
||||
action: {
|
||||
label: 'Retry',
|
||||
onClick: () => retry(),
|
||||
},
|
||||
})
|
||||
```
|
||||
|
||||
**Effort**: 1 hour (just replace alert() calls)
|
||||
**Impact**: Medium - Better UX for notifications
|
||||
|
||||
---
|
||||
|
||||
#### 5. Command Palette (⌘K)
|
||||
**Component**: Use existing `app/components/command-menu.tsx`
|
||||
|
||||
**Enhancement**: Add keyboard shortcut hint
|
||||
```tsx
|
||||
<div className="flex items-center gap-2">
|
||||
<Search className="h-4 w-4" />
|
||||
<span>Search</span>
|
||||
<kbd className="ml-auto">⌘K</kbd>
|
||||
</div>
|
||||
```
|
||||
|
||||
**Effort**: 2 hours
|
||||
**Impact**: Medium - Power user feature
|
||||
|
||||
---
|
||||
|
||||
#### 6. Enhanced Hero Section
|
||||
**File**: `app/(app)/page.tsx`
|
||||
|
||||
**Improvements**:
|
||||
- ✨ Animated gradient background
|
||||
- 🎯 More prominent CTAs
|
||||
- 💫 Subtle animations (fade-in-up)
|
||||
- 🏆 Trust indicators
|
||||
|
||||
**Code Available**: See `docs/UX_ENHANCEMENT_PLAN.md` Section 4
|
||||
|
||||
**Effort**: 3-4 hours
|
||||
**Impact**: High - First impression matters
|
||||
|
||||
---
|
||||
|
||||
### Lower Priority (Week 4+)
|
||||
|
||||
7. **Builder Undo/Redo** (8 hours)
|
||||
8. **Keyboard Shortcuts Panel** (4 hours)
|
||||
9. **Empty State Components** (3 hours)
|
||||
10. **Micro-interactions** (4 hours)
|
||||
11. **Component Card Enhancements** (6 hours)
|
||||
12. **Drag Indicators** (4 hours)
|
||||
|
||||
---
|
||||
|
||||
## 📊 Impact Metrics
|
||||
|
||||
### Current Status
|
||||
| Metric | Before | After Phase 1 | Target | Progress |
|
||||
|--------|--------|---------------|--------|----------|
|
||||
| Accessibility | Good | WCAG A ✅ | WCAG AAA | 33% |
|
||||
| Loading UX | None | Skeletons ✅ | Complete | 50% |
|
||||
| Performance | 14.7s | TBD | <3s | 0% |
|
||||
| Mobile UX | Good | Good ✅ | Excellent | 80% |
|
||||
|
||||
### After Full Implementation
|
||||
| Metric | Target | Improvement |
|
||||
|--------|--------|-------------|
|
||||
| Page Load | <3s | -11.7s (79% faster) |
|
||||
| Accessibility | WCAG AAA | +2 levels |
|
||||
| User Engagement | +50% | Better UX |
|
||||
| Bounce Rate | -20% | Faster loads |
|
||||
|
||||
---
|
||||
|
||||
## 🧪 Testing
|
||||
|
||||
### Automated Tests Available
|
||||
Run comprehensive UX review anytime:
|
||||
```bash
|
||||
pnpm exec playwright test tests/ux-review.spec.ts --headed
|
||||
```
|
||||
|
||||
**What it tests**:
|
||||
- ✅ Skip links presence (now passing!)
|
||||
- ✅ Mobile navigation
|
||||
- ✅ Dark/light mode
|
||||
- ✅ Accessibility features
|
||||
- ⏱️ Performance metrics
|
||||
- 📸 Visual regression (screenshots)
|
||||
|
||||
### Manual Testing Checklist
|
||||
- [ ] Tab through page - skip links should appear
|
||||
- [ ] Press Tab on any page → "Skip to main content" appears
|
||||
- [ ] Click skip link → jumps to main content
|
||||
- [ ] Skeleton loaders appear while content loads
|
||||
- [ ] No layout shift when content replaces skeletons
|
||||
|
||||
---
|
||||
|
||||
## 📚 Documentation Created
|
||||
|
||||
1. **UX Enhancement Plan**: `docs/UX_ENHANCEMENT_PLAN.md`
|
||||
- Complete roadmap with code examples
|
||||
- 12 improvements prioritized
|
||||
- Success metrics defined
|
||||
|
||||
2. **UX Review Summary**: `tests/reports/UX_REVIEW_SUMMARY.md`
|
||||
- Detailed findings from Playwright tests
|
||||
- Current vs target performance
|
||||
- Visual analysis results
|
||||
|
||||
3. **Test Suite**: `tests/ux-review.spec.ts`
|
||||
- 13 comprehensive tests
|
||||
- Reusable for ongoing monitoring
|
||||
- Screenshots for visual tracking
|
||||
|
||||
4. **This Document**: `IMPROVEMENTS_IMPLEMENTED.md`
|
||||
- Track implementation progress
|
||||
- Integration guides
|
||||
- Priority roadmap
|
||||
|
||||
---
|
||||
|
||||
## 🚀 Quick Wins (Do These First!)
|
||||
|
||||
### 5-Minute Tasks
|
||||
1. ✅ Skip links - **DONE**
|
||||
2. Add ID attributes (`id="main-content"`, `id="navigation"`)
|
||||
3. Import skeletons where needed
|
||||
|
||||
### 30-Minute Tasks
|
||||
4. Wrap async content in Suspense with skeleton fallbacks
|
||||
5. Replace alert() with toast notifications
|
||||
6. Add ⌘K hint to search
|
||||
|
||||
### 1-Hour Tasks
|
||||
7. Hero section CTA improvements
|
||||
8. Add loading="lazy" to below-fold images
|
||||
|
||||
---
|
||||
|
||||
## 💡 Tips for Implementation
|
||||
|
||||
### Using Skeleton Loaders
|
||||
```tsx
|
||||
// Pattern 1: Simple async component
|
||||
<Suspense fallback={<SkeletonCard />}>
|
||||
<AsyncComponent />
|
||||
</Suspense>
|
||||
|
||||
// Pattern 2: Grid of items
|
||||
<Suspense fallback={<SkeletonCardGrid count={6} />}>
|
||||
<ItemGrid />
|
||||
</Suspense>
|
||||
|
||||
// Pattern 3: With error boundary
|
||||
<ErrorBoundary fallback={<ErrorState />}>
|
||||
<Suspense fallback={<SkeletonList />}>
|
||||
<DataList />
|
||||
</Suspense>
|
||||
</ErrorBoundary>
|
||||
```
|
||||
|
||||
### Performance Best Practices
|
||||
```tsx
|
||||
// 1. Dynamic imports for heavy components
|
||||
const Chart = dynamic(() => import('./chart'), {
|
||||
ssr: false,
|
||||
loading: () => <SkeletonCard />,
|
||||
})
|
||||
|
||||
// 2. Image optimization
|
||||
import Image from 'next/image'
|
||||
<Image
|
||||
src="/image.jpg"
|
||||
alt="Description"
|
||||
width={800}
|
||||
height={600}
|
||||
loading="lazy"
|
||||
placeholder="blur"
|
||||
/>
|
||||
|
||||
// 3. Code splitting
|
||||
const ModalContent = lazy(() => import('./modal-content'))
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🎯 Next Steps
|
||||
|
||||
**Immediate (This Week)**:
|
||||
1. Add ID attributes to complete skip links
|
||||
2. Integrate skeletons on blocks page
|
||||
3. Test accessibility with screen reader
|
||||
|
||||
**Short-term (Next 2 Weeks)**:
|
||||
4. Performance optimization sprint
|
||||
5. Hero section redesign
|
||||
6. Toast notifications integration
|
||||
|
||||
**Long-term (Month 2)**:
|
||||
7. Builder enhancements (undo/redo, drag indicators)
|
||||
8. Keyboard shortcuts
|
||||
9. Polish and micro-interactions
|
||||
|
||||
---
|
||||
|
||||
## 📞 Support
|
||||
|
||||
**Questions?** See the detailed plans:
|
||||
- `docs/UX_ENHANCEMENT_PLAN.md` - Full implementation guide
|
||||
- `tests/reports/UX_REVIEW_SUMMARY.md` - Test findings
|
||||
- `tests/ux-review.spec.ts` - Test suite source
|
||||
|
||||
**Running Tests**:
|
||||
```bash
|
||||
# Full UX review
|
||||
pnpm test:e2e tests/ux-review.spec.ts
|
||||
|
||||
# Just accessibility tests
|
||||
pnpm test:e2e tests/ux-review.spec.ts -g "Accessibility"
|
||||
|
||||
# With UI (see what's happening)
|
||||
pnpm test:e2e tests/ux-review.spec.ts --headed
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
*Last updated: 2025-11-13*
|
||||
*Phase: 1 of 4 Complete*
|
||||
*Next review: After Week 1 integrations*
|
||||
@@ -1,550 +1,136 @@
|
||||
# Hanzo UI - AI Assistant Knowledge Base
|
||||
# Hanzo UI - LLM Context
|
||||
|
||||
**Last Updated**: 2025-10-18
|
||||
**Project**: Hanzo UI Component Library (shadcn/ui fork with multi-framework support)
|
||||
**Version**: @hanzo/ui v5.0.0
|
||||
## Overview
|
||||
|
||||
## Project Overview
|
||||
React component library (shadcn/ui fork). 161 components, 24+ blocks, two themes, multi-framework. Published as `@hanzo/ui` on npm.
|
||||
|
||||
Hanzo UI is a comprehensive React component library built on hanzo/ui v4 with Hanzo branding. It provides 150+ accessible, customizable components distributed via npm and CLI.
|
||||
**Docs**: https://ui.hanzo.ai | **Dev port**: 3003
|
||||
|
||||
### Repository Structure
|
||||
## Repository Structure
|
||||
|
||||
```
|
||||
ui/
|
||||
├── app/ # Documentation site (Next.js 15.3.1, React 19)
|
||||
│ ├── registry/ # Component registry (source of truth)
|
||||
│ │ ├── default/ # Default theme
|
||||
│ │ │ ├── ui/ # 150+ component implementations
|
||||
│ │ │ ├── example/ # Usage demos
|
||||
│ │ │ └── blocks/ # 24+ full-page sections
|
||||
│ │ └── new-york/ # Alternative theme
|
||||
│ ├── content/docs/ # MDX documentation
|
||||
│ └── scripts/ # Build scripts (registry, capture)
|
||||
├── pkg/
|
||||
│ ├── ui/ # Core library (published to npm)
|
||||
│ ├── auth/ # Auth components
|
||||
│ ├── commerce/ # E-commerce components
|
||||
│ └── brand/ # Branding system
|
||||
├── brands/ # White-label configs (Zoo, Lux)
|
||||
└── templates/ # Project templates
|
||||
app/ Hanzo documentation site (Next.js 15.3.1, React 19)
|
||||
registry/ Component registry (SOURCE OF TRUTH)
|
||||
default/ui/ 150+ components
|
||||
default/example/ Usage demos
|
||||
default/blocks/ 24+ full-page sections
|
||||
new-york/ Alternative theme
|
||||
content/docs/ MDX documentation
|
||||
scripts/ Build scripts
|
||||
apps/
|
||||
v4/ Upstream shadcn v4 docs/registry app (port 4000)
|
||||
packages/
|
||||
shadcn/ shadcn CLI v4.1.0 (font system, chart colors, scaffold)
|
||||
tests/ Integration tests for shadcn CLI
|
||||
og/ OG image generation (Hanzo-only)
|
||||
pkg/
|
||||
ui/ Core library (npm)
|
||||
react/ React primitives
|
||||
brand/ Branding system
|
||||
commerce/ E-commerce components
|
||||
checkout/ Checkout flow
|
||||
shop/ Shop components
|
||||
agent-ui/ AI agent UI components
|
||||
tokens/ Design tokens
|
||||
skills/
|
||||
shadcn/ AI skill definitions for shadcn CLI
|
||||
templates/ Project templates (next, vite, astro, react-router, start + monorepo variants)
|
||||
template/next/ Hanzo-customized Next.js template
|
||||
```
|
||||
|
||||
## Essential Commands
|
||||
## Critical: Build Order
|
||||
|
||||
**Registry MUST build before app.** The registry generates JSON that the CLI reads.
|
||||
|
||||
### Development (Always Required)
|
||||
```bash
|
||||
# Start main dev server (port 3003)
|
||||
pnpm dev
|
||||
|
||||
# Build registry FIRST, then app
|
||||
pnpm build:registry
|
||||
pnpm build
|
||||
|
||||
# Run from root or app/
|
||||
cd app && pnpm dev
|
||||
cd app && pnpm registry:build
|
||||
pnpm build:registry # MUST run first
|
||||
pnpm build # Then build app
|
||||
```
|
||||
|
||||
### Code Quality
|
||||
## Commands
|
||||
|
||||
```bash
|
||||
pnpm dev # Dev server (:3003)
|
||||
pnpm build:registry # Build component registry
|
||||
pnpm build # Build app
|
||||
pnpm lint # Lint all workspaces
|
||||
pnpm lint:fix # Auto-fix issues
|
||||
pnpm typecheck # Type checking
|
||||
pnpm format:write # Format code
|
||||
```
|
||||
|
||||
### Testing
|
||||
```bash
|
||||
pnpm test # Unit tests
|
||||
pnpm test:e2e # E2E with Playwright
|
||||
pnpm test:visual # Visual regression
|
||||
pnpm test:e2e # Playwright E2E
|
||||
pnpm changeset # Create changeset for publishing
|
||||
```
|
||||
|
||||
### Package Publishing
|
||||
```bash
|
||||
pnpm changeset # Create changeset
|
||||
pnpm changeset version # Bump versions
|
||||
cd pkg/ui && npm publish --access public
|
||||
```
|
||||
## Three-Layer Architecture
|
||||
|
||||
## Critical Build Process
|
||||
1. **Components** (`registry/{style}/ui/`) -- Single primitives (Button, Card, Dialog). CLI-installable.
|
||||
2. **Examples** (`registry/{style}/example/`) -- Usage demos for docs via `<ComponentPreview />`.
|
||||
3. **Blocks** (`registry/{style}/blocks/`) -- Full-page sections (Dashboard, Login). NOT CLI-installable, docs only.
|
||||
|
||||
**REGISTRY IS CORE** - Always build registry before app:
|
||||
## Import Path Transformation
|
||||
|
||||
1. **Source**: Components in `app/registry/{style}/ui/`
|
||||
2. **Build**: `pnpm build:registry` generates JSON
|
||||
3. **Output**: `app/public/r/{style}/{name}.json`
|
||||
4. **Usage**: CLI reads JSON to install components
|
||||
Registry files use `@/registry/default/ui/button`. After CLI install, rewritten to `@/components/ui/button`.
|
||||
|
||||
**Build Order:**
|
||||
```bash
|
||||
pnpm build:registry # MUST run first
|
||||
pnpm build # Then build app
|
||||
```
|
||||
|
||||
## Component Architecture
|
||||
|
||||
### Three-Layer System
|
||||
|
||||
**1. Components** (`registry/{style}/ui/`)
|
||||
- Single UI primitives (Button, Input, Card, Dialog)
|
||||
- 150+ total components
|
||||
- Two themes: default, new-york
|
||||
|
||||
**2. Examples** (`registry/{style}/example/`)
|
||||
- Usage demonstrations
|
||||
- Used in docs via `<ComponentPreview />`
|
||||
- Pattern examples
|
||||
|
||||
**3. Blocks** (`registry/{style}/blocks/`)
|
||||
- Viewport-sized sections (Hero, Dashboard, Login)
|
||||
- 24+ production templates
|
||||
- Compose multiple components
|
||||
- NOT CLI-installable (documentation only)
|
||||
|
||||
### Standard Component Pattern
|
||||
|
||||
```tsx
|
||||
"use client"
|
||||
|
||||
import * as React from "react"
|
||||
import { cva, type VariantProps } from "class-variance-authority"
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
const componentVariants = cva(
|
||||
"base-classes",
|
||||
{
|
||||
variants: {
|
||||
variant: {
|
||||
default: "default-styles",
|
||||
outline: "outline-styles"
|
||||
}
|
||||
},
|
||||
defaultVariants: { variant: "default" }
|
||||
}
|
||||
)
|
||||
|
||||
interface ComponentProps
|
||||
extends React.HTMLAttributes<HTMLDivElement>,
|
||||
VariantProps<typeof componentVariants> {}
|
||||
|
||||
const Component = React.forwardRef<HTMLDivElement, ComponentProps>(
|
||||
({ className, variant, ...props }, ref) => (
|
||||
<div
|
||||
ref={ref}
|
||||
className={cn(componentVariants({ variant }), className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
)
|
||||
Component.displayName = "Component"
|
||||
|
||||
export { Component, componentVariants, type ComponentProps }
|
||||
```
|
||||
|
||||
### Import Path Transformation
|
||||
|
||||
**In registry files:**
|
||||
```tsx
|
||||
import { cn } from "@/lib/utils"
|
||||
import { Button } from "@/registry/default/ui/button"
|
||||
```
|
||||
|
||||
**After CLI installation** (paths rewritten):
|
||||
```tsx
|
||||
import { cn } from "@/lib/utils"
|
||||
import { Button } from "@/components/ui/button"
|
||||
```
|
||||
|
||||
## Package Exports (@hanzo/ui)
|
||||
## Package Exports
|
||||
|
||||
```typescript
|
||||
// Main exports
|
||||
import { Button, Card } from '@hanzo/ui'
|
||||
|
||||
// Component-specific
|
||||
import { Button } from '@hanzo/ui/components'
|
||||
|
||||
// Blocks (documentation reference)
|
||||
import { DashboardBlock } from '@hanzo/ui/blocks'
|
||||
|
||||
// Primitives (Radix UI re-exports)
|
||||
import * as Dialog from '@hanzo/ui/primitives/dialog'
|
||||
|
||||
// Utilities
|
||||
import { cn } from '@hanzo/ui/lib/utils'
|
||||
```
|
||||
|
||||
## Adding a New Component
|
||||
## Adding a Component
|
||||
|
||||
1. **Create files in BOTH themes:**
|
||||
```bash
|
||||
touch app/registry/default/ui/my-component.tsx
|
||||
touch app/registry/new-york/ui/my-component.tsx
|
||||
touch app/registry/default/example/my-component-demo.tsx
|
||||
```
|
||||
1. Create in BOTH themes: `app/registry/{default,new-york}/ui/my-component.tsx`
|
||||
2. Create example: `app/registry/default/example/my-component-demo.tsx`
|
||||
3. Create docs: `app/content/docs/components/my-component.mdx`
|
||||
4. Update nav: `app/config/docs.ts`
|
||||
5. Build: `pnpm build:registry`
|
||||
|
||||
2. **Implement using standard pattern** (see above)
|
||||
## Tech Stack
|
||||
|
||||
3. **Add documentation:**
|
||||
```bash
|
||||
touch app/content/docs/components/my-component.mdx
|
||||
```
|
||||
React 19, Next.js 15.3+, Tailwind CSS 4 (OKLCH colors), Radix UI, Turborepo + pnpm, Fumadocs (MDX), class-variance-authority.
|
||||
|
||||
4. **Update navigation** in `app/config/docs.ts`
|
||||
## Upstream Sync
|
||||
|
||||
5. **Build registry:**
|
||||
```bash
|
||||
pnpm build:registry
|
||||
```
|
||||
Remote `shadcn` points to `/Users/z/work/shadcn/ui` (local clone of shadcn-ui/ui).
|
||||
hanzoai/ui is NOT a GitHub fork -- no shared object store, so large merges can fail on push.
|
||||
|
||||
6. **Test locally:**
|
||||
```bash
|
||||
pnpm dev
|
||||
# Visit http://localhost:3003/docs/components/my-component
|
||||
```
|
||||
|
||||
## Technology Stack
|
||||
|
||||
- **React**: 18.3.1 (19.1.0 experimental)
|
||||
- **Next.js**: 15.3.1 with Turbopack
|
||||
- **Styling**: Tailwind CSS 3.4.6-4.1.11, OKLCH colors
|
||||
- **Components**: Radix UI primitives
|
||||
- **Build**: Turborepo + pnpm workspaces
|
||||
- **Docs**: Fumadocs (MDX)
|
||||
- **Package Manager**: pnpm 9.0.6+
|
||||
Last sync: 2026-03-24 (shadcn@4.1.0, commit 8bec9c123)
|
||||
Strategy: file-level checkout from shadcn/main for specific directories (not git merge).
|
||||
- Take theirs: packages/shadcn/, packages/tests/, apps/, templates/, scripts/, skills/
|
||||
- Keep ours: app/, pkg/, demo/, docs/, template/next/, pnpm-workspace.yaml, package.json
|
||||
- Remove: deprecated/ (upstream deleted it)
|
||||
- Regenerate: pnpm-lock.yaml after sync
|
||||
|
||||
## Key Features
|
||||
|
||||
### Page Builder (`/builder`)
|
||||
- Visual drag-drop block assembly
|
||||
- 24+ viewport-sized blocks
|
||||
- Export to React TSX
|
||||
- Built with @dnd-kit
|
||||
- **Page Builder** (`/builder`): Drag-drop block assembly with @dnd-kit, export to TSX
|
||||
- **White-Label**: Zoo/Lux forks via `brands/{BRAND}.brand.ts`
|
||||
- **External Registries**: 35+ sources in `app/registries.json`, install via `npx @hanzo/ui add @aceternity/spotlight`
|
||||
|
||||
### White-Label System
|
||||
- Zoo UI fork: `~/work/zoo/ui`
|
||||
- Lux UI fork: `~/work/lux/ui`
|
||||
- Config: `brands/{BRAND}.brand.ts`
|
||||
## Gotchas
|
||||
|
||||
### External Registries
|
||||
- 35+ external component sources
|
||||
- Config: `app/registries.json`
|
||||
- Install: `npx @hanzo/ui add @aceternity/spotlight`
|
||||
- Registry index is `Index[style][name]`, NOT `Index[name]` -- caused silent block render failures
|
||||
- Shiki `getHighlighter` incompatible with static export -- replaced with basic pre/code
|
||||
- Some blocks (login-01, login-02, sidebar-02) have Server Component issues with event handlers
|
||||
- Zod validation removed from `_getAllBlocks()`/`_getBlockCode()` -- we control generation
|
||||
- Firebase split to optional `@hanzo/auth-firebase` package (Jan 2025)
|
||||
- `@hanzo/auth` v2.6.0 uses pluggable provider registry: `registerAuthProvider('firebase', FirebaseAuthService)`
|
||||
|
||||
## Deployment
|
||||
## Component Stats
|
||||
|
||||
### GitHub Pages (ui.hanzo.ai)
|
||||
```
|
||||
Workflow: .github/workflows/deploy-pages.yml
|
||||
Triggers: Push to main
|
||||
Process:
|
||||
1. Build pkg/ui
|
||||
2. Build registry
|
||||
3. Build Next.js app (static export)
|
||||
4. Deploy to GitHub Pages
|
||||
```
|
||||
- 161 total files, ~127 implemented, ~34 stubs
|
||||
- Unique: 9 3D components, 12 AI components, 13 animation components, 15 nav variants
|
||||
- 3x more components than upstream shadcn/ui (161 vs 58)
|
||||
- shadcn CLI: v4.1.0 with font transformers, chart color picker, scaffold from github
|
||||
|
||||
### Environment Variables
|
||||
```bash
|
||||
# Production
|
||||
NEXT_PUBLIC_APP_URL=https://ui.hanzo.ai
|
||||
GITHUB_ACTIONS=true
|
||||
## Rules
|
||||
|
||||
# Development
|
||||
NEXT_PUBLIC_APP_URL=http://localhost:3003
|
||||
```
|
||||
|
||||
## Common Issues & Solutions
|
||||
|
||||
### Registry Not Found
|
||||
❌ App build fails with registry errors
|
||||
✅ Run `pnpm build:registry` before `pnpm build`
|
||||
|
||||
### Port Already In Use
|
||||
❌ Port 3003 already taken
|
||||
✅ Change port or `lsof -i :3003 && kill -9 PID`
|
||||
|
||||
### Type Errors After Changes
|
||||
❌ TypeScript errors in registry
|
||||
✅ Rebuild registry to regenerate types
|
||||
|
||||
### Package Not Publishing
|
||||
❌ npm publish fails
|
||||
✅ Must be in `pkg/ui`, version bumped
|
||||
|
||||
## Critical Patterns (AI Must Follow)
|
||||
|
||||
1. **Registry First**: Always build registry before app
|
||||
2. **Two Themes**: default and new-york must stay in sync
|
||||
3. **Import Paths**: Transform from `@/registry/` → `@/components/`
|
||||
4. **Blocks vs Components**: Blocks are docs only, not CLI-installable
|
||||
5. **Package Exports**: Multiple entry points for different needs
|
||||
|
||||
## Recent Updates
|
||||
|
||||
### 2025-10-19 - Registry Structure Fix & Build Issues (IN PROGRESS)
|
||||
**Issue**: Deployment failing due to registry access pattern and build errors
|
||||
**Root Causes**:
|
||||
1. Registry functions accessing `Index[name]` instead of `Index[style][name]`
|
||||
2. Zod validation expecting objects but Index structure uses nested style keys
|
||||
3. Shiki `getHighlighter` incompatible with static export
|
||||
4. Block schema validation failing on null values
|
||||
5. Some blocks have Server Component issues with event handlers
|
||||
|
||||
**Fixes Applied**:
|
||||
1. Updated `getRegistryItem()` and `getRegistryComponent()` to accept style parameter
|
||||
2. Updated all callers to pass style parameter (block-display.tsx)
|
||||
3. Removed Zod validation in `_getAllBlocks()` and `_getBlockCode()` - we control generation
|
||||
4. Disabled syntax highlighting for static exports (replaced with basic pre/code)
|
||||
5. Convert null to undefined in block metadata extraction
|
||||
|
||||
**Remaining Issues**:
|
||||
- Some blocks (login-01, login-02, sidebar-02) have Server Component errors
|
||||
- Need to mark these blocks as Client Components or skip in static generation
|
||||
|
||||
**Files Modified**:
|
||||
- `/lib/registry.ts` - Added style parameter to registry functions
|
||||
- `/components/block-display.tsx` - Pass style to getCachedRegistryItem
|
||||
- `/lib/blocks.ts` - Removed Zod validation, fixed null values
|
||||
- `/lib/highlight-code.ts` - Disabled highlighting for static export
|
||||
|
||||
### 2025-10-18 - Blocks Display Bug Fix (COMPLETED)
|
||||
**Issue**: Blocks not displaying on http://localhost:3003/blocks
|
||||
**Root Cause**: Registry index structure mismatch
|
||||
- Registry is structured as: `Index[style][componentName]` (e.g., `Index["default"]["dashboard-01"]`)
|
||||
- `BlockDisplay` component used `getRegistryItem(name)` which tried `Index[name]` (undefined)
|
||||
- Result: Silent failure returning null, blocks never rendered
|
||||
|
||||
**Solution**: Updated `BlockDisplay` component to:
|
||||
1. Import `getBlock` function from `/lib/blocks.ts` which correctly accesses `Index[style][name]`
|
||||
2. Accept optional `style` parameter with default "default"
|
||||
3. Use `BlockWrapper` for blocks (same as individual block pages)
|
||||
4. Fallback to `BlockViewer` for non-block components
|
||||
|
||||
**Changes Made**:
|
||||
- File: `/Users/z/work/hanzo/ui/app/components/block-display.tsx`
|
||||
- Added imports: `getBlock`, `BlockWrapper`, `Style`
|
||||
- Added interface `BlockDisplayProps` with optional style
|
||||
- Rewrote logic to first try `getBlock(name, style)`, fallback to `getRegistryItem(name)`
|
||||
- For blocks: render with `BlockWrapper` (simpler, no file tree needed)
|
||||
- For components: render with `BlockViewer` (includes code display)
|
||||
|
||||
**Status**: ✅ FIXED - All 5 featured blocks now displaying correctly on blocks page
|
||||
- dashboard-01 ✅
|
||||
- sidebar-07 ✅
|
||||
- sidebar-03 ✅
|
||||
- login-03 ✅
|
||||
- login-04 ✅
|
||||
|
||||
**Testing**: Verified via browser - no console errors, clean rendering
|
||||
|
||||
### 2025-10-18 - Documentation Overhaul
|
||||
- ✅ **Cleaned up root directory** - Moved old status reports to docs/archive/
|
||||
- ✅ **Deleted unnecessary MD files** - Removed USAGE.md, WHITE_LABEL.md, TESTING_GUIDE.md
|
||||
- ✅ **Created comprehensive framework docs** - React, Vue guides in app/content/docs/frameworks/
|
||||
- ✅ **Created testing guide** - Complete testing documentation in app/content/docs/testing/
|
||||
- ✅ **Created white-label guide** - Fork and rebrand documentation in app/content/docs/white-label/
|
||||
- ✅ **Created packages overview** - All packages documented in app/content/docs/packages/
|
||||
- ✅ **Updated README.md** - Concise, feature-rich overview
|
||||
- ✅ **Updated navigation** - Added Frameworks, Packages, Testing, White-Label sections
|
||||
- ✅ **Fixed build:registry command** - Updated workspace references from www,v4 to app
|
||||
- ✅ **Fixed prettier warnings** - Removed deprecated import order options
|
||||
- ✅ **Created shadcn/ui comparison** - Archived in docs/archive/SHADCN_COMPARISON_REPORT.md
|
||||
- ✅ **Verified build process** - Registry builds successfully
|
||||
|
||||
### 2025-10-05
|
||||
- ✅ Next.js 15.3.1 with Turbopack
|
||||
- ✅ React 19 support (experimental)
|
||||
- ✅ Fumadocs (replaced Contentlayer)
|
||||
- ✅ @dnd-kit page builder
|
||||
- ✅ Electric blue primary (210 100% 50%)
|
||||
- ✅ Synced with hanzo/ui v3.4.0
|
||||
|
||||
## Component Status Tracking
|
||||
|
||||
### Current State (2025-10-18)
|
||||
- **Total Component Files**: 161 in registry/default/ui/
|
||||
- **Implemented**: ~127 fully functional components
|
||||
- **Stub Components**: ~34 need implementation
|
||||
- **Blocks**: 24+ viewport-sized templates
|
||||
- **Frameworks**: React (100%), Vue (~90%), Svelte (~85%), React Native (~70%)
|
||||
|
||||
### shadcn/ui Comparison
|
||||
**We have 3x more components than shadcn/ui (161 vs 58)**
|
||||
|
||||
**Unique to @hanzo/ui:**
|
||||
- 3D Components (9): 3d-button, 3d-card, 3d-carousel, 3d-grid, etc.
|
||||
- AI Components (12): ai-chat, ai-assistant, ai-playground, ai-vision, etc.
|
||||
- Animation Components (13): animated-beam, animated-text, animated-cursor, etc.
|
||||
- Navigation Variants (15): Multiple specialized navigation bars
|
||||
- Advanced Features: Page builder, white-label system, multi-framework support
|
||||
|
||||
**Latest from shadcn/ui 2025 (verify implementation):**
|
||||
1. button-group ✅ (exists)
|
||||
2. empty ✅ (exists)
|
||||
3. field ✅ (exists)
|
||||
4. input-group ✅ (exists)
|
||||
5. item ✅ (exists)
|
||||
6. kbd ✅ (exists)
|
||||
7. spinner ✅ (exists)
|
||||
|
||||
### High-Priority Missing Components
|
||||
1. **combobox** - Search, filter, keyboard nav
|
||||
2. **color-picker** - RGB, HSL, HEX, alpha
|
||||
3. **dropzone** - File upload with drag-drop
|
||||
4. **minimal-tiptap** - Rich text editor
|
||||
5. **image-crop** - Cropping tool
|
||||
6. **image-zoom** - Zoom/pan functionality
|
||||
|
||||
### Navigation Components (~15 stubs)
|
||||
Need extraction from blocks:
|
||||
- advanced-navigation-bar
|
||||
- ai-model-selector-navigation-bar
|
||||
- breadcrumb-navigation-bar
|
||||
- dashboard-navigation-bar
|
||||
- e-commerce-navigation-bar
|
||||
- filter-navigation-bar
|
||||
- mobile-bottom-navigation-bar
|
||||
- multi-level-navigation-bar
|
||||
- notification-center-navigation-bar
|
||||
- project-switcher-navigation-bar
|
||||
- search-navigation-bar
|
||||
- settings-navigation-bar
|
||||
- tabs-navigation-bar
|
||||
- app-switcher-navigation-bar
|
||||
- breadcrumb-and-filters-navigation-bar
|
||||
|
||||
## Dependencies
|
||||
|
||||
### Installed
|
||||
```json
|
||||
{
|
||||
"@radix-ui/react-*": "latest",
|
||||
"class-variance-authority": "^0.7.1",
|
||||
"tailwind-merge": "^2.5.5",
|
||||
"react-day-picker": "^8.10.1",
|
||||
"lucide-react": "latest"
|
||||
}
|
||||
```
|
||||
|
||||
### Missing (Need to Add)
|
||||
```json
|
||||
{
|
||||
"@tanstack/react-table": "^8.20.5",
|
||||
"cmdk": "^0.2.1",
|
||||
"sonner": "^1.7.2",
|
||||
"react-dropzone": "^14.3.8",
|
||||
"react-colorful": "^5.6.1",
|
||||
"@tiptap/react": "^2.10.5",
|
||||
"framer-motion": "^11.15.0"
|
||||
}
|
||||
```
|
||||
|
||||
## Working with Forks
|
||||
|
||||
### Zoo UI (`~/work/zoo/ui`)
|
||||
```bash
|
||||
cd ~/work/zoo/ui
|
||||
git remote add hanzo https://github.com/hanzoai/ui.git
|
||||
git fetch hanzo main
|
||||
git merge hanzo/main
|
||||
```
|
||||
|
||||
### Lux UI (`~/work/lux/ui`)
|
||||
Same sync pattern as Zoo UI.
|
||||
|
||||
## Context for All AI Assistants
|
||||
|
||||
This file (`LLM.md`) is symlinked as:
|
||||
- `.AGENTS.md`
|
||||
- `CLAUDE.md`
|
||||
- `QWEN.md`
|
||||
- `GEMINI.md`
|
||||
|
||||
All files reference the same knowledge base. Updates here propagate to all AI systems.
|
||||
|
||||
## Documentation Structure
|
||||
|
||||
All documentation now lives in `app/content/docs/` and is visible on ui.hanzo.ai:
|
||||
|
||||
- **/frameworks/** - Multi-framework guides (React, Vue, Svelte, React Native)
|
||||
- **/packages/** - Package documentation (@hanzo/ui, @hanzo/auth, etc.)
|
||||
- **/testing/** - Testing guide (unit, E2E, visual regression)
|
||||
- **/white-label/** - Fork and rebrand guide
|
||||
- **/components/** - All 161 component docs
|
||||
- **/blocks/** - Block documentation
|
||||
- **/installation/** - Framework-specific installation guides
|
||||
- **/guides/** - Page builder, workflows
|
||||
|
||||
**Archived**: Old status reports in `/docs/archive/`
|
||||
|
||||
**Root MD Files** (ONLY these allowed):
|
||||
- LLM.md (this file)
|
||||
- README.md
|
||||
- CONTRIBUTING.md
|
||||
- LICENSE.md
|
||||
- SECURITY.md
|
||||
- AGENTS.md, CLAUDE.md, QWEN.md, GEMINI.md (symlinks to LLM.md)
|
||||
|
||||
## Rules for AI Assistants
|
||||
|
||||
1. **ALWAYS** build registry before app build
|
||||
2. **NEVER** commit symlinked files (.AGENTS.md, CLAUDE.md, etc.) - they're in .gitignore
|
||||
3. **ALWAYS** update LLM.md with significant discoveries
|
||||
4. **NEVER** create random summary/status files at root - they go in docs/archive/ or update LLM.md
|
||||
5. **NEVER** create BARE .md files at root except LLM.md, README.md, CONTRIBUTING.md, LICENSE.md, SECURITY.md
|
||||
6. **ALWAYS** put documentation in app/content/docs/ so it's visible on ui.hanzo.ai
|
||||
7. **ALWAYS** use pnpm, not npm/yarn
|
||||
8. **ALWAYS** check both theme variants (default, new-york)
|
||||
9. **NEVER** confuse blocks with components (blocks are docs only)
|
||||
|
||||
## Testing Patterns
|
||||
|
||||
```bash
|
||||
# Unit test single file
|
||||
pnpm test path/to/test.spec.ts
|
||||
|
||||
# E2E single test
|
||||
pnpm test:e2e tests/specific.spec.ts
|
||||
|
||||
# Watch mode
|
||||
pnpm test:dev
|
||||
```
|
||||
|
||||
## MDX Documentation
|
||||
|
||||
Available components in MDX:
|
||||
```tsx
|
||||
<ComponentPreview name="button-demo" />
|
||||
<ComponentSource name="button" />
|
||||
<CodeTabs>
|
||||
<Steps>
|
||||
<Callout>
|
||||
```
|
||||
|
||||
## Git Status Context (2025-10-05)
|
||||
|
||||
Modified files from git status:
|
||||
- `.github/workflows/ci.yml` - CI config
|
||||
- `.gitignore` - Ignore patterns
|
||||
- `LLM.md` - This file
|
||||
- Multiple component files (3d-*, avatar-group, banner, etc.)
|
||||
- Registry examples updated
|
||||
|
||||
Deleted:
|
||||
- `CLAUDE.md` - Now symlinked to LLM.md
|
||||
|
||||
Untracked:
|
||||
- `.github/workflows/coverage.yml` - New coverage workflow
|
||||
|
||||
Recent commits:
|
||||
1. feat: Add floating paintbrush to theme generator
|
||||
2. feat: Show block previews in page builder
|
||||
3. fix: Add missing source.config.mjs
|
||||
4. fix: Correct sidebar layout in compose editor
|
||||
|
||||
---
|
||||
|
||||
**Note**: This file serves as the single source of truth for all AI assistants working on this project. Keep it updated with architectural changes, new patterns, and critical insights.
|
||||
1. Always build registry before app
|
||||
2. Keep default and new-york themes in sync
|
||||
3. Blocks are docs only, not CLI-installable
|
||||
4. Use pnpm, not npm/yarn
|
||||
5. Never commit symlinked files (AGENTS.md, CLAUDE.md, etc.)
|
||||
6. Documentation goes in `app/content/docs/`, not random root MD files
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
ui
|
||||
Copyright (c) 2023 Hanzo AI, Inc.
|
||||
|
||||
This product includes software from shadcn/ui (https://github.com/shadcn-ui/ui), licensed under MIT:
|
||||
|
||||
Copyright (c) 2023 shadcn
|
||||
@@ -1,3 +1,5 @@
|
||||
<p align="center"><img src=".github/hero.svg" alt="ui" width="880"></p>
|
||||
|
||||
# @hanzo/ui
|
||||
|
||||
Accessible and customizable components for React, Vue, Svelte, and React Native. **Built on shadcn/ui with multi-framework support, 3D components, AI components, and advanced features.**
|
||||
@@ -6,18 +8,18 @@ Accessible and customizable components for React, Vue, Svelte, and React Native.
|
||||
|
||||
## Features
|
||||
|
||||
- 🎯 **161+ Components** - 3x more than shadcn/ui
|
||||
- 🌐 **Multi-Framework** - React, Vue, Svelte, React Native
|
||||
- 🎨 **Two Themes** - Default & New York variants
|
||||
- 🤖 **AI Components** - Chat, assistants, playground
|
||||
- 🎮 **3D Components** - Interactive 3D elements
|
||||
- ✨ **Animations** - Advanced motion components
|
||||
- 🎛️ **Page Builder** - Visual drag-drop interface
|
||||
- 🏷️ **White-Label** - Fork and rebrand easily
|
||||
- 📦 **Blocks** - 24+ production-ready templates
|
||||
- ♿ **Accessible** - Built with Radix UI primitives
|
||||
- 🎭 **Customizable** - Tailwind CSS powered
|
||||
- 📘 **TypeScript** - Fully typed
|
||||
- **161+ Components** - 3x more than shadcn/ui
|
||||
- **Multi-Framework** - React, Vue, Svelte, React Native
|
||||
- **Two Themes** - Default & New York variants
|
||||
- **AI Components** - Chat, assistants, playground
|
||||
- **3D Components** - Interactive 3D elements
|
||||
- **Animations** - Advanced motion components
|
||||
- **Page Builder** - Visual drag-drop interface
|
||||
- **White-Label** - Fork and rebrand easily
|
||||
- **Blocks** - 24+ production-ready templates
|
||||
- **Accessible** - Built with Radix UI primitives
|
||||
- **Customizable** - Tailwind CSS powered
|
||||
- **TypeScript** - Fully typed
|
||||
|
||||
## Quick Start
|
||||
|
||||
@@ -51,54 +53,17 @@ export function App() {
|
||||
}
|
||||
```
|
||||
|
||||
## Multi-Framework Support
|
||||
|
||||
```tsx
|
||||
// React (default)
|
||||
import { Button } from '@hanzo/ui'
|
||||
|
||||
// Vue
|
||||
import { Button } from '@hanzo/ui/vue'
|
||||
|
||||
// Svelte
|
||||
import { Button } from '@hanzo/ui/svelte'
|
||||
|
||||
// React Native
|
||||
import { Button } from '@hanzo/ui/react-native'
|
||||
```
|
||||
|
||||
## Documentation
|
||||
|
||||
📚 Visit **[ui.hanzo.ai](https://ui.hanzo.ai)** for:
|
||||
|
||||
- [Component Documentation](https://ui.hanzo.ai/docs/components)
|
||||
- [Framework Guides](https://ui.hanzo.ai/docs/frameworks)
|
||||
- [Installation Guide](https://ui.hanzo.ai/docs/installation)
|
||||
- [Examples](https://ui.hanzo.ai/examples)
|
||||
- [Page Builder](https://ui.hanzo.ai/builder)
|
||||
- [Theme Generator](https://ui.hanzo.ai/theme-generator)
|
||||
Visit **[ui.hanzo.ai](https://ui.hanzo.ai)** for full docs.
|
||||
|
||||
## CLI
|
||||
|
||||
Install components individually:
|
||||
|
||||
```bash
|
||||
npx @hanzo/ui add button
|
||||
npx @hanzo/ui add card dialog
|
||||
```
|
||||
|
||||
## What's Different from shadcn/ui?
|
||||
|
||||
| Feature | shadcn/ui | @hanzo/ui |
|
||||
|---------|-----------|-----------|
|
||||
| Components | 58 | **161** |
|
||||
| Frameworks | React only | React, Vue, Svelte, React Native |
|
||||
| 3D Components | ❌ | ✅ (9 components) |
|
||||
| AI Components | ❌ | ✅ (12 components) |
|
||||
| Page Builder | ❌ | ✅ |
|
||||
| White-Label | ❌ | ✅ |
|
||||
| Blocks | Limited | **24+ templates** |
|
||||
|
||||
## Packages
|
||||
|
||||
- `@hanzo/ui` - Main UI library (161 components)
|
||||
@@ -106,55 +71,23 @@ npx @hanzo/ui add card dialog
|
||||
- `@hanzo/commerce` - E-commerce components
|
||||
- `@hanzo/brand` - Branding system
|
||||
|
||||
## Examples
|
||||
|
||||
```tsx
|
||||
// 3D Components
|
||||
import { ThreeDButton, ThreeDCard } from '@hanzo/ui'
|
||||
|
||||
// AI Components
|
||||
import { AIChat, AIAssistant } from '@hanzo/ui'
|
||||
|
||||
// Animations
|
||||
import { AnimatedBeam, AnimatedText } from '@hanzo/ui'
|
||||
```
|
||||
|
||||
## Development
|
||||
|
||||
```bash
|
||||
# Clone repository
|
||||
git clone https://github.com/hanzoai/ui.git
|
||||
cd ui
|
||||
|
||||
# Install dependencies
|
||||
pnpm install
|
||||
|
||||
# Start dev server
|
||||
pnpm dev
|
||||
|
||||
# Build packages
|
||||
pnpm build
|
||||
|
||||
# Run tests
|
||||
pnpm test
|
||||
```
|
||||
|
||||
## Contributing
|
||||
|
||||
We welcome contributions! Please see our [Contributing Guide](./CONTRIBUTING.md).
|
||||
Please read the [contributing guide](/CONTRIBUTING.md).
|
||||
|
||||
## License
|
||||
|
||||
MIT - See [LICENSE.md](./LICENSE.md) for details.
|
||||
|
||||
## Links
|
||||
|
||||
- [Documentation](https://ui.hanzo.ai)
|
||||
- [GitHub](https://github.com/hanzoai/ui)
|
||||
- [npm](https://www.npmjs.com/package/@hanzo/ui)
|
||||
- [Discord](https://discord.gg/hanzo)
|
||||
- [Twitter](https://twitter.com/hanzoai)
|
||||
|
||||
---
|
||||
|
||||
Built with ❤️ by [Hanzo](https://hanzo.ai)
|
||||
Built by [Hanzo](https://hanzo.ai)
|
||||
|
||||
+1
-1
@@ -6,4 +6,4 @@ We will investigate all legitimate reports and do our best to quickly fix the pr
|
||||
|
||||
Our preference is that you make use of GitHub's private vulnerability reporting feature to disclose potential security vulnerabilities in our Open Source Software.
|
||||
|
||||
To do this, please visit the security tab of the repository and click the "Report a vulnerability" button.
|
||||
To do this, please visit the security tab of the repository and click the [Report a vulnerability](https://github.com/shadcn-ui/ui/security/advisories/new) button.
|
||||
|
||||
@@ -0,0 +1,434 @@
|
||||
// @ts-nocheck
|
||||
import { browser } from "@hanzo/docs-mdx/runtime/browser"
|
||||
|
||||
import type * as Config from "../source.config"
|
||||
|
||||
const create = browser<
|
||||
typeof Config,
|
||||
import("@hanzo/docs-mdx/runtime/types").InternalTypeConfig & {
|
||||
DocData: {}
|
||||
}
|
||||
>()
|
||||
const browserCollections = {
|
||||
docs: create.doc("docs", {
|
||||
"about.mdx": () => import("../content/docs/about.mdx?collection=docs"),
|
||||
"changelog.mdx": () =>
|
||||
import("../content/docs/changelog.mdx?collection=docs"),
|
||||
"cli.mdx": () => import("../content/docs/cli.mdx?collection=docs"),
|
||||
"components-json.mdx": () =>
|
||||
import("../content/docs/components-json.mdx?collection=docs"),
|
||||
"components.mdx": () =>
|
||||
import("../content/docs/components.mdx?collection=docs"),
|
||||
"figma.mdx": () => import("../content/docs/figma.mdx?collection=docs"),
|
||||
"index.mdx": () => import("../content/docs/index.mdx?collection=docs"),
|
||||
"mcp.mdx": () => import("../content/docs/mcp.mdx?collection=docs"),
|
||||
"theming.mdx": () => import("../content/docs/theming.mdx?collection=docs"),
|
||||
"ai/actions.mdx": () =>
|
||||
import("../content/docs/ai/actions.mdx?collection=docs"),
|
||||
"ai/agents.mdx": () =>
|
||||
import("../content/docs/ai/agents.mdx?collection=docs"),
|
||||
"ai/assistant.mdx": () =>
|
||||
import("../content/docs/ai/assistant.mdx?collection=docs"),
|
||||
"ai/chat.mdx": () => import("../content/docs/ai/chat.mdx?collection=docs"),
|
||||
"ai/code.mdx": () => import("../content/docs/ai/code.mdx?collection=docs"),
|
||||
"ai/index.mdx": () =>
|
||||
import("../content/docs/ai/index.mdx?collection=docs"),
|
||||
"ai/models.mdx": () =>
|
||||
import("../content/docs/ai/models.mdx?collection=docs"),
|
||||
"ai/playground.mdx": () =>
|
||||
import("../content/docs/ai/playground.mdx?collection=docs"),
|
||||
"ai/tools.mdx": () =>
|
||||
import("../content/docs/ai/tools.mdx?collection=docs"),
|
||||
"ai/vision.mdx": () =>
|
||||
import("../content/docs/ai/vision.mdx?collection=docs"),
|
||||
"ai/voice-settings.mdx": () =>
|
||||
import("../content/docs/ai/voice-settings.mdx?collection=docs"),
|
||||
"ai/voice.mdx": () =>
|
||||
import("../content/docs/ai/voice.mdx?collection=docs"),
|
||||
"block-examples/billing.mdx": () =>
|
||||
import("../content/docs/block-examples/billing.mdx?collection=docs"),
|
||||
"block-examples/compose-spec.mdx": () =>
|
||||
import("../content/docs/block-examples/compose-spec.mdx?collection=docs"),
|
||||
"block-examples/newsletter.mdx": () =>
|
||||
import("../content/docs/block-examples/newsletter.mdx?collection=docs"),
|
||||
"block-examples/pricing.mdx": () =>
|
||||
import("../content/docs/block-examples/pricing.mdx?collection=docs"),
|
||||
"frameworks/index.mdx": () =>
|
||||
import("../content/docs/frameworks/index.mdx?collection=docs"),
|
||||
"frameworks/react.mdx": () =>
|
||||
import("../content/docs/frameworks/react.mdx?collection=docs"),
|
||||
"frameworks/vue.mdx": () =>
|
||||
import("../content/docs/frameworks/vue.mdx?collection=docs"),
|
||||
"dark-mode/astro.mdx": () =>
|
||||
import("../content/docs/dark-mode/astro.mdx?collection=docs"),
|
||||
"dark-mode/index.mdx": () =>
|
||||
import("../content/docs/dark-mode/index.mdx?collection=docs"),
|
||||
"dark-mode/next.mdx": () =>
|
||||
import("../content/docs/dark-mode/next.mdx?collection=docs"),
|
||||
"dark-mode/remix.mdx": () =>
|
||||
import("../content/docs/dark-mode/remix.mdx?collection=docs"),
|
||||
"dark-mode/vite.mdx": () =>
|
||||
import("../content/docs/dark-mode/vite.mdx?collection=docs"),
|
||||
"guides/page-builder.mdx": () =>
|
||||
import("../content/docs/guides/page-builder.mdx?collection=docs"),
|
||||
"guides/visual-workflows.mdx": () =>
|
||||
import("../content/docs/guides/visual-workflows.mdx?collection=docs"),
|
||||
"hanzo-react/getting-started.mdx": () =>
|
||||
import("../content/docs/hanzo-react/getting-started.mdx?collection=docs"),
|
||||
"hanzo-react/hooks.mdx": () =>
|
||||
import("../content/docs/hanzo-react/hooks.mdx?collection=docs"),
|
||||
"installation/astro.mdx": () =>
|
||||
import("../content/docs/installation/astro.mdx?collection=docs"),
|
||||
"installation/gatsby.mdx": () =>
|
||||
import("../content/docs/installation/gatsby.mdx?collection=docs"),
|
||||
"installation/index.mdx": () =>
|
||||
import("../content/docs/installation/index.mdx?collection=docs"),
|
||||
"installation/laravel.mdx": () =>
|
||||
import("../content/docs/installation/laravel.mdx?collection=docs"),
|
||||
"installation/manual.mdx": () =>
|
||||
import("../content/docs/installation/manual.mdx?collection=docs"),
|
||||
"installation/next.mdx": () =>
|
||||
import("../content/docs/installation/next.mdx?collection=docs"),
|
||||
"installation/remix.mdx": () =>
|
||||
import("../content/docs/installation/remix.mdx?collection=docs"),
|
||||
"installation/vite.mdx": () =>
|
||||
import("../content/docs/installation/vite.mdx?collection=docs"),
|
||||
"packages/index.mdx": () =>
|
||||
import("../content/docs/packages/index.mdx?collection=docs"),
|
||||
"components/3d-card.mdx": () =>
|
||||
import("../content/docs/components/3d-card.mdx?collection=docs"),
|
||||
"components/3d-marquee.mdx": () =>
|
||||
import("../content/docs/components/3d-marquee.mdx?collection=docs"),
|
||||
"components/3d-pin.mdx": () =>
|
||||
import("../content/docs/components/3d-pin.mdx?collection=docs"),
|
||||
"components/accordion.mdx": () =>
|
||||
import("../content/docs/components/accordion.mdx?collection=docs"),
|
||||
"components/advanced-navigation-bar.mdx": () =>
|
||||
import(
|
||||
"../content/docs/components/advanced-navigation-bar.mdx?collection=docs"
|
||||
),
|
||||
"components/ai-model-selector-navigation-bar.mdx": () =>
|
||||
import(
|
||||
"../content/docs/components/ai-model-selector-navigation-bar.mdx?collection=docs"
|
||||
),
|
||||
"components/alert-dialog.mdx": () =>
|
||||
import("../content/docs/components/alert-dialog.mdx?collection=docs"),
|
||||
"components/alert.mdx": () =>
|
||||
import("../content/docs/components/alert.mdx?collection=docs"),
|
||||
"components/android.mdx": () =>
|
||||
import("../content/docs/components/android.mdx?collection=docs"),
|
||||
"components/animated-beam.mdx": () =>
|
||||
import("../content/docs/components/animated-beam.mdx?collection=docs"),
|
||||
"components/animated-cursor.mdx": () =>
|
||||
import("../content/docs/components/animated-cursor.mdx?collection=docs"),
|
||||
"components/animated-testimonials.mdx": () =>
|
||||
import(
|
||||
"../content/docs/components/animated-testimonials.mdx?collection=docs"
|
||||
),
|
||||
"components/animated-tooltip.mdx": () =>
|
||||
import("../content/docs/components/animated-tooltip.mdx?collection=docs"),
|
||||
"components/animation-components.mdx": () =>
|
||||
import(
|
||||
"../content/docs/components/animation-components.mdx?collection=docs"
|
||||
),
|
||||
"components/announcement.mdx": () =>
|
||||
import("../content/docs/components/announcement.mdx?collection=docs"),
|
||||
"components/app-switcher-navigation-bar.mdx": () =>
|
||||
import(
|
||||
"../content/docs/components/app-switcher-navigation-bar.mdx?collection=docs"
|
||||
),
|
||||
"components/apple-cards-carousel.mdx": () =>
|
||||
import(
|
||||
"../content/docs/components/apple-cards-carousel.mdx?collection=docs"
|
||||
),
|
||||
"components/apple-hello-effect.mdx": () =>
|
||||
import(
|
||||
"../content/docs/components/apple-hello-effect.mdx?collection=docs"
|
||||
),
|
||||
"components/aspect-ratio.mdx": () =>
|
||||
import("../content/docs/components/aspect-ratio.mdx?collection=docs"),
|
||||
"components/avatar-group.mdx": () =>
|
||||
import("../content/docs/components/avatar-group.mdx?collection=docs"),
|
||||
"components/avatar.mdx": () =>
|
||||
import("../content/docs/components/avatar.mdx?collection=docs"),
|
||||
"components/badge.mdx": () =>
|
||||
import("../content/docs/components/badge.mdx?collection=docs"),
|
||||
"components/banner.mdx": () =>
|
||||
import("../content/docs/components/banner.mdx?collection=docs"),
|
||||
"components/breadcrumb-and-filters-navigation-bar.mdx": () =>
|
||||
import(
|
||||
"../content/docs/components/breadcrumb-and-filters-navigation-bar.mdx?collection=docs"
|
||||
),
|
||||
"components/breadcrumb-navigation-bar.mdx": () =>
|
||||
import(
|
||||
"../content/docs/components/breadcrumb-navigation-bar.mdx?collection=docs"
|
||||
),
|
||||
"components/breadcrumb.mdx": () =>
|
||||
import("../content/docs/components/breadcrumb.mdx?collection=docs"),
|
||||
"components/button-group.mdx": () =>
|
||||
import("../content/docs/components/button-group.mdx?collection=docs"),
|
||||
"components/button.mdx": () =>
|
||||
import("../content/docs/components/button.mdx?collection=docs"),
|
||||
"components/calendar.mdx": () =>
|
||||
import("../content/docs/components/calendar.mdx?collection=docs"),
|
||||
"components/card.mdx": () =>
|
||||
import("../content/docs/components/card.mdx?collection=docs"),
|
||||
"components/carousel.mdx": () =>
|
||||
import("../content/docs/components/carousel.mdx?collection=docs"),
|
||||
"components/centered-logo-navigation-bar.mdx": () =>
|
||||
import(
|
||||
"../content/docs/components/centered-logo-navigation-bar.mdx?collection=docs"
|
||||
),
|
||||
"components/checkbox.mdx": () =>
|
||||
import("../content/docs/components/checkbox.mdx?collection=docs"),
|
||||
"components/choicebox.mdx": () =>
|
||||
import("../content/docs/components/choicebox.mdx?collection=docs"),
|
||||
"components/code-block.mdx": () =>
|
||||
import("../content/docs/components/code-block.mdx?collection=docs"),
|
||||
"components/code-editor.mdx": () =>
|
||||
import("../content/docs/components/code-editor.mdx?collection=docs"),
|
||||
"components/code-tabs.mdx": () =>
|
||||
import("../content/docs/components/code-tabs.mdx?collection=docs"),
|
||||
"components/collaboration-navigation-bar.mdx": () =>
|
||||
import(
|
||||
"../content/docs/components/collaboration-navigation-bar.mdx?collection=docs"
|
||||
),
|
||||
"components/collapsible.mdx": () =>
|
||||
import("../content/docs/components/collapsible.mdx?collection=docs"),
|
||||
"components/color-picker.mdx": () =>
|
||||
import("../content/docs/components/color-picker.mdx?collection=docs"),
|
||||
"components/combobox.mdx": () =>
|
||||
import("../content/docs/components/combobox.mdx?collection=docs"),
|
||||
"components/command.mdx": () =>
|
||||
import("../content/docs/components/command.mdx?collection=docs"),
|
||||
"components/communication-navigation-bar.mdx": () =>
|
||||
import(
|
||||
"../content/docs/components/communication-navigation-bar.mdx?collection=docs"
|
||||
),
|
||||
"components/comparison.mdx": () =>
|
||||
import("../content/docs/components/comparison.mdx?collection=docs"),
|
||||
"components/context-menu.mdx": () =>
|
||||
import("../content/docs/components/context-menu.mdx?collection=docs"),
|
||||
"components/context-switcher-navigation-bar.mdx": () =>
|
||||
import(
|
||||
"../content/docs/components/context-switcher-navigation-bar.mdx?collection=docs"
|
||||
),
|
||||
"components/credit-card.mdx": () =>
|
||||
import("../content/docs/components/credit-card.mdx?collection=docs"),
|
||||
"components/cursor.mdx": () =>
|
||||
import("../content/docs/components/cursor.mdx?collection=docs"),
|
||||
"components/dashboard-navigation-bar.mdx": () =>
|
||||
import(
|
||||
"../content/docs/components/dashboard-navigation-bar.mdx?collection=docs"
|
||||
),
|
||||
"components/data-table.mdx": () =>
|
||||
import("../content/docs/components/data-table.mdx?collection=docs"),
|
||||
"components/date-picker.mdx": () =>
|
||||
import("../content/docs/components/date-picker.mdx?collection=docs"),
|
||||
"components/desktop-hooks.mdx": () =>
|
||||
import("../content/docs/components/desktop-hooks.mdx?collection=docs"),
|
||||
"components/desktop-spotlight.mdx": () =>
|
||||
import(
|
||||
"../content/docs/components/desktop-spotlight.mdx?collection=docs"
|
||||
),
|
||||
"components/desktop-window.mdx": () =>
|
||||
import("../content/docs/components/desktop-window.mdx?collection=docs"),
|
||||
"components/desktop.mdx": () =>
|
||||
import("../content/docs/components/desktop.mdx?collection=docs"),
|
||||
"components/dialog-stack.mdx": () =>
|
||||
import("../content/docs/components/dialog-stack.mdx?collection=docs"),
|
||||
"components/dialog.mdx": () =>
|
||||
import("../content/docs/components/dialog.mdx?collection=docs"),
|
||||
"components/dock.mdx": () =>
|
||||
import("../content/docs/components/dock.mdx?collection=docs"),
|
||||
"components/drawer.mdx": () =>
|
||||
import("../content/docs/components/drawer.mdx?collection=docs"),
|
||||
"components/dropdown-menu.mdx": () =>
|
||||
import("../content/docs/components/dropdown-menu.mdx?collection=docs"),
|
||||
"components/dropzone.mdx": () =>
|
||||
import("../content/docs/components/dropzone.mdx?collection=docs"),
|
||||
"components/e-commerce-navigation-bar.mdx": () =>
|
||||
import(
|
||||
"../content/docs/components/e-commerce-navigation-bar.mdx?collection=docs"
|
||||
),
|
||||
"components/editor.mdx": () =>
|
||||
import("../content/docs/components/editor.mdx?collection=docs"),
|
||||
"components/empty.mdx": () =>
|
||||
import("../content/docs/components/empty.mdx?collection=docs"),
|
||||
"components/field.mdx": () =>
|
||||
import("../content/docs/components/field.mdx?collection=docs"),
|
||||
"components/form.mdx": () =>
|
||||
import("../content/docs/components/form.mdx?collection=docs"),
|
||||
"components/gantt.mdx": () =>
|
||||
import("../content/docs/components/gantt.mdx?collection=docs"),
|
||||
"components/glimpse.mdx": () =>
|
||||
import("../content/docs/components/glimpse.mdx?collection=docs"),
|
||||
"components/grid-pattern.mdx": () =>
|
||||
import("../content/docs/components/grid-pattern.mdx?collection=docs"),
|
||||
"components/hover-card.mdx": () =>
|
||||
import("../content/docs/components/hover-card.mdx?collection=docs"),
|
||||
"components/icon-navigation-bar.mdx": () =>
|
||||
import(
|
||||
"../content/docs/components/icon-navigation-bar.mdx?collection=docs"
|
||||
),
|
||||
"components/image-crop.mdx": () =>
|
||||
import("../content/docs/components/image-crop.mdx?collection=docs"),
|
||||
"components/image-zoom.mdx": () =>
|
||||
import("../content/docs/components/image-zoom.mdx?collection=docs"),
|
||||
"components/index.mdx": () =>
|
||||
import("../content/docs/components/index.mdx?collection=docs"),
|
||||
"components/input-group.mdx": () =>
|
||||
import("../content/docs/components/input-group.mdx?collection=docs"),
|
||||
"components/input-otp.mdx": () =>
|
||||
import("../content/docs/components/input-otp.mdx?collection=docs"),
|
||||
"components/input.mdx": () =>
|
||||
import("../content/docs/components/input.mdx?collection=docs"),
|
||||
"components/interactive-grid-pattern.mdx": () =>
|
||||
import(
|
||||
"../content/docs/components/interactive-grid-pattern.mdx?collection=docs"
|
||||
),
|
||||
"components/iphone-15-pro.mdx": () =>
|
||||
import("../content/docs/components/iphone-15-pro.mdx?collection=docs"),
|
||||
"components/item.mdx": () =>
|
||||
import("../content/docs/components/item.mdx?collection=docs"),
|
||||
"components/kanban.mdx": () =>
|
||||
import("../content/docs/components/kanban.mdx?collection=docs"),
|
||||
"components/kbd.mdx": () =>
|
||||
import("../content/docs/components/kbd.mdx?collection=docs"),
|
||||
"components/label.mdx": () =>
|
||||
import("../content/docs/components/label.mdx?collection=docs"),
|
||||
"components/limelight-nav.mdx": () =>
|
||||
import("../content/docs/components/limelight-nav.mdx?collection=docs"),
|
||||
"components/list.mdx": () =>
|
||||
import("../content/docs/components/list.mdx?collection=docs"),
|
||||
"components/macos-dock.mdx": () =>
|
||||
import("../content/docs/components/macos-dock.mdx?collection=docs"),
|
||||
"components/magnetic.mdx": () =>
|
||||
import("../content/docs/components/magnetic.mdx?collection=docs"),
|
||||
"components/marquee.mdx": () =>
|
||||
import("../content/docs/components/marquee.mdx?collection=docs"),
|
||||
"components/menu-dock.mdx": () =>
|
||||
import("../content/docs/components/menu-dock.mdx?collection=docs"),
|
||||
"components/menubar.mdx": () =>
|
||||
import("../content/docs/components/menubar.mdx?collection=docs"),
|
||||
"components/message-dock.mdx": () =>
|
||||
import("../content/docs/components/message-dock.mdx?collection=docs"),
|
||||
"components/mini-calendar.mdx": () =>
|
||||
import("../content/docs/components/mini-calendar.mdx?collection=docs"),
|
||||
"components/minimal-tiptap.mdx": () =>
|
||||
import("../content/docs/components/minimal-tiptap.mdx?collection=docs"),
|
||||
"components/motion-highlight.mdx": () =>
|
||||
import("../content/docs/components/motion-highlight.mdx?collection=docs"),
|
||||
"components/navigation-menu.mdx": () =>
|
||||
import("../content/docs/components/navigation-menu.mdx?collection=docs"),
|
||||
"components/pagination.mdx": () =>
|
||||
import("../content/docs/components/pagination.mdx?collection=docs"),
|
||||
"components/particles-background.mdx": () =>
|
||||
import(
|
||||
"../content/docs/components/particles-background.mdx?collection=docs"
|
||||
),
|
||||
"components/particles.mdx": () =>
|
||||
import("../content/docs/components/particles.mdx?collection=docs"),
|
||||
"components/pill.mdx": () =>
|
||||
import("../content/docs/components/pill.mdx?collection=docs"),
|
||||
"components/pin-list.mdx": () =>
|
||||
import("../content/docs/components/pin-list.mdx?collection=docs"),
|
||||
"components/pixel-image.mdx": () =>
|
||||
import("../content/docs/components/pixel-image.mdx?collection=docs"),
|
||||
"components/popover.mdx": () =>
|
||||
import("../content/docs/components/popover.mdx?collection=docs"),
|
||||
"components/progress.mdx": () =>
|
||||
import("../content/docs/components/progress.mdx?collection=docs"),
|
||||
"components/qr-code.mdx": () =>
|
||||
import("../content/docs/components/qr-code.mdx?collection=docs"),
|
||||
"components/radio-group.mdx": () =>
|
||||
import("../content/docs/components/radio-group.mdx?collection=docs"),
|
||||
"components/relative-time.mdx": () =>
|
||||
import("../content/docs/components/relative-time.mdx?collection=docs"),
|
||||
"components/resizable.mdx": () =>
|
||||
import("../content/docs/components/resizable.mdx?collection=docs"),
|
||||
"components/safari.mdx": () =>
|
||||
import("../content/docs/components/safari.mdx?collection=docs"),
|
||||
"components/sandbox.mdx": () =>
|
||||
import("../content/docs/components/sandbox.mdx?collection=docs"),
|
||||
"components/scroll-area.mdx": () =>
|
||||
import("../content/docs/components/scroll-area.mdx?collection=docs"),
|
||||
"components/search-and-toggle-navigation-bar.mdx": () =>
|
||||
import(
|
||||
"../content/docs/components/search-and-toggle-navigation-bar.mdx?collection=docs"
|
||||
),
|
||||
"components/select.mdx": () =>
|
||||
import("../content/docs/components/select.mdx?collection=docs"),
|
||||
"components/separator.mdx": () =>
|
||||
import("../content/docs/components/separator.mdx?collection=docs"),
|
||||
"components/sheet.mdx": () =>
|
||||
import("../content/docs/components/sheet.mdx?collection=docs"),
|
||||
"components/simple-navigation-bar.mdx": () =>
|
||||
import(
|
||||
"../content/docs/components/simple-navigation-bar.mdx?collection=docs"
|
||||
),
|
||||
"components/skeleton.mdx": () =>
|
||||
import("../content/docs/components/skeleton.mdx?collection=docs"),
|
||||
"components/slider.mdx": () =>
|
||||
import("../content/docs/components/slider.mdx?collection=docs"),
|
||||
"components/snippet.mdx": () =>
|
||||
import("../content/docs/components/snippet.mdx?collection=docs"),
|
||||
"components/sonner.mdx": () =>
|
||||
import("../content/docs/components/sonner.mdx?collection=docs"),
|
||||
"components/spinner.mdx": () =>
|
||||
import("../content/docs/components/spinner.mdx?collection=docs"),
|
||||
"components/stars-scrolling-wheel.mdx": () =>
|
||||
import(
|
||||
"../content/docs/components/stars-scrolling-wheel.mdx?collection=docs"
|
||||
),
|
||||
"components/status-dashboard-navigation-bar.mdx": () =>
|
||||
import(
|
||||
"../content/docs/components/status-dashboard-navigation-bar.mdx?collection=docs"
|
||||
),
|
||||
"components/status.mdx": () =>
|
||||
import("../content/docs/components/status.mdx?collection=docs"),
|
||||
"components/switch.mdx": () =>
|
||||
import("../content/docs/components/switch.mdx?collection=docs"),
|
||||
"components/table.mdx": () =>
|
||||
import("../content/docs/components/table.mdx?collection=docs"),
|
||||
"components/tabs.mdx": () =>
|
||||
import("../content/docs/components/tabs.mdx?collection=docs"),
|
||||
"components/tags.mdx": () =>
|
||||
import("../content/docs/components/tags.mdx?collection=docs"),
|
||||
"components/team-switcher-navigation-bar.mdx": () =>
|
||||
import(
|
||||
"../content/docs/components/team-switcher-navigation-bar.mdx?collection=docs"
|
||||
),
|
||||
"components/terminal.mdx": () =>
|
||||
import("../content/docs/components/terminal.mdx?collection=docs"),
|
||||
"components/textarea.mdx": () =>
|
||||
import("../content/docs/components/textarea.mdx?collection=docs"),
|
||||
"components/timeline.mdx": () =>
|
||||
import("../content/docs/components/timeline.mdx?collection=docs"),
|
||||
"components/toast.mdx": () =>
|
||||
import("../content/docs/components/toast.mdx?collection=docs"),
|
||||
"components/toggle-group.mdx": () =>
|
||||
import("../content/docs/components/toggle-group.mdx?collection=docs"),
|
||||
"components/toggle.mdx": () =>
|
||||
import("../content/docs/components/toggle.mdx?collection=docs"),
|
||||
"components/tooltip.mdx": () =>
|
||||
import("../content/docs/components/tooltip.mdx?collection=docs"),
|
||||
"components/two-tier-navigation-bar.mdx": () =>
|
||||
import(
|
||||
"../content/docs/components/two-tier-navigation-bar.mdx?collection=docs"
|
||||
),
|
||||
"components/typography.mdx": () =>
|
||||
import("../content/docs/components/typography.mdx?collection=docs"),
|
||||
"components/underline-navigation-bar.mdx": () =>
|
||||
import(
|
||||
"../content/docs/components/underline-navigation-bar.mdx?collection=docs"
|
||||
),
|
||||
"components/video-player.mdx": () =>
|
||||
import("../content/docs/components/video-player.mdx?collection=docs"),
|
||||
"white-label/index.mdx": () =>
|
||||
import("../content/docs/white-label/index.mdx?collection=docs"),
|
||||
"testing/index.mdx": () =>
|
||||
import("../content/docs/testing/index.mdx?collection=docs"),
|
||||
}),
|
||||
}
|
||||
export default browserCollections
|
||||
@@ -0,0 +1,15 @@
|
||||
// @ts-nocheck
|
||||
import { dynamic } from "@hanzo/docs-mdx/runtime/dynamic"
|
||||
|
||||
import * as Config from "../source.config"
|
||||
|
||||
const create = await dynamic<
|
||||
typeof Config,
|
||||
import("@hanzo/docs-mdx/runtime/types").InternalTypeConfig & {
|
||||
DocData: {}
|
||||
}
|
||||
>(
|
||||
Config,
|
||||
{ configPath: "source.config.ts", environment: "next", outDir: ".docs" },
|
||||
{ doc: { passthroughs: ["extractedReferences"] } }
|
||||
)
|
||||
@@ -0,0 +1,393 @@
|
||||
// @ts-nocheck
|
||||
import { server } from "@hanzo/docs-mdx/runtime/server"
|
||||
|
||||
import * as __fd_glob_0 from "../content/docs/about.mdx?collection=docs"
|
||||
import * as __fd_glob_9 from "../content/docs/ai/actions.mdx?collection=docs"
|
||||
import * as __fd_glob_10 from "../content/docs/ai/agents.mdx?collection=docs"
|
||||
import * as __fd_glob_11 from "../content/docs/ai/assistant.mdx?collection=docs"
|
||||
import * as __fd_glob_12 from "../content/docs/ai/chat.mdx?collection=docs"
|
||||
import * as __fd_glob_13 from "../content/docs/ai/code.mdx?collection=docs"
|
||||
import * as __fd_glob_14 from "../content/docs/ai/index.mdx?collection=docs"
|
||||
import * as __fd_glob_15 from "../content/docs/ai/models.mdx?collection=docs"
|
||||
import * as __fd_glob_16 from "../content/docs/ai/playground.mdx?collection=docs"
|
||||
import * as __fd_glob_17 from "../content/docs/ai/tools.mdx?collection=docs"
|
||||
import * as __fd_glob_18 from "../content/docs/ai/vision.mdx?collection=docs"
|
||||
import * as __fd_glob_19 from "../content/docs/ai/voice-settings.mdx?collection=docs"
|
||||
import * as __fd_glob_20 from "../content/docs/ai/voice.mdx?collection=docs"
|
||||
import * as __fd_glob_21 from "../content/docs/block-examples/billing.mdx?collection=docs"
|
||||
import * as __fd_glob_22 from "../content/docs/block-examples/compose-spec.mdx?collection=docs"
|
||||
import * as __fd_glob_23 from "../content/docs/block-examples/newsletter.mdx?collection=docs"
|
||||
import * as __fd_glob_24 from "../content/docs/block-examples/pricing.mdx?collection=docs"
|
||||
import * as __fd_glob_1 from "../content/docs/changelog.mdx?collection=docs"
|
||||
import * as __fd_glob_2 from "../content/docs/cli.mdx?collection=docs"
|
||||
import * as __fd_glob_3 from "../content/docs/components-json.mdx?collection=docs"
|
||||
import * as __fd_glob_4 from "../content/docs/components.mdx?collection=docs"
|
||||
import * as __fd_glob_46 from "../content/docs/components/3d-card.mdx?collection=docs"
|
||||
import * as __fd_glob_47 from "../content/docs/components/3d-marquee.mdx?collection=docs"
|
||||
import * as __fd_glob_48 from "../content/docs/components/3d-pin.mdx?collection=docs"
|
||||
import * as __fd_glob_49 from "../content/docs/components/accordion.mdx?collection=docs"
|
||||
import * as __fd_glob_50 from "../content/docs/components/advanced-navigation-bar.mdx?collection=docs"
|
||||
import * as __fd_glob_51 from "../content/docs/components/ai-model-selector-navigation-bar.mdx?collection=docs"
|
||||
import * as __fd_glob_52 from "../content/docs/components/alert-dialog.mdx?collection=docs"
|
||||
import * as __fd_glob_53 from "../content/docs/components/alert.mdx?collection=docs"
|
||||
import * as __fd_glob_54 from "../content/docs/components/android.mdx?collection=docs"
|
||||
import * as __fd_glob_55 from "../content/docs/components/animated-beam.mdx?collection=docs"
|
||||
import * as __fd_glob_56 from "../content/docs/components/animated-cursor.mdx?collection=docs"
|
||||
import * as __fd_glob_57 from "../content/docs/components/animated-testimonials.mdx?collection=docs"
|
||||
import * as __fd_glob_58 from "../content/docs/components/animated-tooltip.mdx?collection=docs"
|
||||
import * as __fd_glob_59 from "../content/docs/components/animation-components.mdx?collection=docs"
|
||||
import * as __fd_glob_60 from "../content/docs/components/announcement.mdx?collection=docs"
|
||||
import * as __fd_glob_61 from "../content/docs/components/app-switcher-navigation-bar.mdx?collection=docs"
|
||||
import * as __fd_glob_62 from "../content/docs/components/apple-cards-carousel.mdx?collection=docs"
|
||||
import * as __fd_glob_63 from "../content/docs/components/apple-hello-effect.mdx?collection=docs"
|
||||
import * as __fd_glob_64 from "../content/docs/components/aspect-ratio.mdx?collection=docs"
|
||||
import * as __fd_glob_65 from "../content/docs/components/avatar-group.mdx?collection=docs"
|
||||
import * as __fd_glob_66 from "../content/docs/components/avatar.mdx?collection=docs"
|
||||
import * as __fd_glob_67 from "../content/docs/components/badge.mdx?collection=docs"
|
||||
import * as __fd_glob_68 from "../content/docs/components/banner.mdx?collection=docs"
|
||||
import * as __fd_glob_69 from "../content/docs/components/breadcrumb-and-filters-navigation-bar.mdx?collection=docs"
|
||||
import * as __fd_glob_70 from "../content/docs/components/breadcrumb-navigation-bar.mdx?collection=docs"
|
||||
import * as __fd_glob_71 from "../content/docs/components/breadcrumb.mdx?collection=docs"
|
||||
import * as __fd_glob_72 from "../content/docs/components/button-group.mdx?collection=docs"
|
||||
import * as __fd_glob_73 from "../content/docs/components/button.mdx?collection=docs"
|
||||
import * as __fd_glob_74 from "../content/docs/components/calendar.mdx?collection=docs"
|
||||
import * as __fd_glob_75 from "../content/docs/components/card.mdx?collection=docs"
|
||||
import * as __fd_glob_76 from "../content/docs/components/carousel.mdx?collection=docs"
|
||||
import * as __fd_glob_77 from "../content/docs/components/centered-logo-navigation-bar.mdx?collection=docs"
|
||||
import * as __fd_glob_78 from "../content/docs/components/checkbox.mdx?collection=docs"
|
||||
import * as __fd_glob_79 from "../content/docs/components/choicebox.mdx?collection=docs"
|
||||
import * as __fd_glob_80 from "../content/docs/components/code-block.mdx?collection=docs"
|
||||
import * as __fd_glob_81 from "../content/docs/components/code-editor.mdx?collection=docs"
|
||||
import * as __fd_glob_82 from "../content/docs/components/code-tabs.mdx?collection=docs"
|
||||
import * as __fd_glob_83 from "../content/docs/components/collaboration-navigation-bar.mdx?collection=docs"
|
||||
import * as __fd_glob_84 from "../content/docs/components/collapsible.mdx?collection=docs"
|
||||
import * as __fd_glob_85 from "../content/docs/components/color-picker.mdx?collection=docs"
|
||||
import * as __fd_glob_86 from "../content/docs/components/combobox.mdx?collection=docs"
|
||||
import * as __fd_glob_87 from "../content/docs/components/command.mdx?collection=docs"
|
||||
import * as __fd_glob_88 from "../content/docs/components/communication-navigation-bar.mdx?collection=docs"
|
||||
import * as __fd_glob_89 from "../content/docs/components/comparison.mdx?collection=docs"
|
||||
import * as __fd_glob_90 from "../content/docs/components/context-menu.mdx?collection=docs"
|
||||
import * as __fd_glob_91 from "../content/docs/components/context-switcher-navigation-bar.mdx?collection=docs"
|
||||
import * as __fd_glob_92 from "../content/docs/components/credit-card.mdx?collection=docs"
|
||||
import * as __fd_glob_93 from "../content/docs/components/cursor.mdx?collection=docs"
|
||||
import * as __fd_glob_94 from "../content/docs/components/dashboard-navigation-bar.mdx?collection=docs"
|
||||
import * as __fd_glob_95 from "../content/docs/components/data-table.mdx?collection=docs"
|
||||
import * as __fd_glob_96 from "../content/docs/components/date-picker.mdx?collection=docs"
|
||||
import * as __fd_glob_97 from "../content/docs/components/desktop-hooks.mdx?collection=docs"
|
||||
import * as __fd_glob_98 from "../content/docs/components/desktop-spotlight.mdx?collection=docs"
|
||||
import * as __fd_glob_99 from "../content/docs/components/desktop-window.mdx?collection=docs"
|
||||
import * as __fd_glob_100 from "../content/docs/components/desktop.mdx?collection=docs"
|
||||
import * as __fd_glob_101 from "../content/docs/components/dialog-stack.mdx?collection=docs"
|
||||
import * as __fd_glob_102 from "../content/docs/components/dialog.mdx?collection=docs"
|
||||
import * as __fd_glob_103 from "../content/docs/components/dock.mdx?collection=docs"
|
||||
import * as __fd_glob_104 from "../content/docs/components/drawer.mdx?collection=docs"
|
||||
import * as __fd_glob_105 from "../content/docs/components/dropdown-menu.mdx?collection=docs"
|
||||
import * as __fd_glob_106 from "../content/docs/components/dropzone.mdx?collection=docs"
|
||||
import * as __fd_glob_107 from "../content/docs/components/e-commerce-navigation-bar.mdx?collection=docs"
|
||||
import * as __fd_glob_108 from "../content/docs/components/editor.mdx?collection=docs"
|
||||
import * as __fd_glob_109 from "../content/docs/components/empty.mdx?collection=docs"
|
||||
import * as __fd_glob_110 from "../content/docs/components/field.mdx?collection=docs"
|
||||
import * as __fd_glob_111 from "../content/docs/components/form.mdx?collection=docs"
|
||||
import * as __fd_glob_112 from "../content/docs/components/gantt.mdx?collection=docs"
|
||||
import * as __fd_glob_113 from "../content/docs/components/glimpse.mdx?collection=docs"
|
||||
import * as __fd_glob_114 from "../content/docs/components/grid-pattern.mdx?collection=docs"
|
||||
import * as __fd_glob_115 from "../content/docs/components/hover-card.mdx?collection=docs"
|
||||
import * as __fd_glob_116 from "../content/docs/components/icon-navigation-bar.mdx?collection=docs"
|
||||
import * as __fd_glob_117 from "../content/docs/components/image-crop.mdx?collection=docs"
|
||||
import * as __fd_glob_118 from "../content/docs/components/image-zoom.mdx?collection=docs"
|
||||
import * as __fd_glob_119 from "../content/docs/components/index.mdx?collection=docs"
|
||||
import * as __fd_glob_120 from "../content/docs/components/input-group.mdx?collection=docs"
|
||||
import * as __fd_glob_121 from "../content/docs/components/input-otp.mdx?collection=docs"
|
||||
import * as __fd_glob_122 from "../content/docs/components/input.mdx?collection=docs"
|
||||
import * as __fd_glob_123 from "../content/docs/components/interactive-grid-pattern.mdx?collection=docs"
|
||||
import * as __fd_glob_124 from "../content/docs/components/iphone-15-pro.mdx?collection=docs"
|
||||
import * as __fd_glob_125 from "../content/docs/components/item.mdx?collection=docs"
|
||||
import * as __fd_glob_126 from "../content/docs/components/kanban.mdx?collection=docs"
|
||||
import * as __fd_glob_127 from "../content/docs/components/kbd.mdx?collection=docs"
|
||||
import * as __fd_glob_128 from "../content/docs/components/label.mdx?collection=docs"
|
||||
import * as __fd_glob_129 from "../content/docs/components/limelight-nav.mdx?collection=docs"
|
||||
import * as __fd_glob_130 from "../content/docs/components/list.mdx?collection=docs"
|
||||
import * as __fd_glob_131 from "../content/docs/components/macos-dock.mdx?collection=docs"
|
||||
import * as __fd_glob_132 from "../content/docs/components/magnetic.mdx?collection=docs"
|
||||
import * as __fd_glob_133 from "../content/docs/components/marquee.mdx?collection=docs"
|
||||
import * as __fd_glob_134 from "../content/docs/components/menu-dock.mdx?collection=docs"
|
||||
import * as __fd_glob_135 from "../content/docs/components/menubar.mdx?collection=docs"
|
||||
import * as __fd_glob_136 from "../content/docs/components/message-dock.mdx?collection=docs"
|
||||
import * as __fd_glob_137 from "../content/docs/components/mini-calendar.mdx?collection=docs"
|
||||
import * as __fd_glob_138 from "../content/docs/components/minimal-tiptap.mdx?collection=docs"
|
||||
import * as __fd_glob_139 from "../content/docs/components/motion-highlight.mdx?collection=docs"
|
||||
import * as __fd_glob_140 from "../content/docs/components/navigation-menu.mdx?collection=docs"
|
||||
import * as __fd_glob_141 from "../content/docs/components/pagination.mdx?collection=docs"
|
||||
import * as __fd_glob_142 from "../content/docs/components/particles-background.mdx?collection=docs"
|
||||
import * as __fd_glob_143 from "../content/docs/components/particles.mdx?collection=docs"
|
||||
import * as __fd_glob_144 from "../content/docs/components/pill.mdx?collection=docs"
|
||||
import * as __fd_glob_145 from "../content/docs/components/pin-list.mdx?collection=docs"
|
||||
import * as __fd_glob_146 from "../content/docs/components/pixel-image.mdx?collection=docs"
|
||||
import * as __fd_glob_147 from "../content/docs/components/popover.mdx?collection=docs"
|
||||
import * as __fd_glob_148 from "../content/docs/components/progress.mdx?collection=docs"
|
||||
import * as __fd_glob_149 from "../content/docs/components/qr-code.mdx?collection=docs"
|
||||
import * as __fd_glob_150 from "../content/docs/components/radio-group.mdx?collection=docs"
|
||||
import * as __fd_glob_151 from "../content/docs/components/relative-time.mdx?collection=docs"
|
||||
import * as __fd_glob_152 from "../content/docs/components/resizable.mdx?collection=docs"
|
||||
import * as __fd_glob_153 from "../content/docs/components/safari.mdx?collection=docs"
|
||||
import * as __fd_glob_154 from "../content/docs/components/sandbox.mdx?collection=docs"
|
||||
import * as __fd_glob_155 from "../content/docs/components/scroll-area.mdx?collection=docs"
|
||||
import * as __fd_glob_156 from "../content/docs/components/search-and-toggle-navigation-bar.mdx?collection=docs"
|
||||
import * as __fd_glob_157 from "../content/docs/components/select.mdx?collection=docs"
|
||||
import * as __fd_glob_158 from "../content/docs/components/separator.mdx?collection=docs"
|
||||
import * as __fd_glob_159 from "../content/docs/components/sheet.mdx?collection=docs"
|
||||
import * as __fd_glob_160 from "../content/docs/components/simple-navigation-bar.mdx?collection=docs"
|
||||
import * as __fd_glob_161 from "../content/docs/components/skeleton.mdx?collection=docs"
|
||||
import * as __fd_glob_162 from "../content/docs/components/slider.mdx?collection=docs"
|
||||
import * as __fd_glob_163 from "../content/docs/components/snippet.mdx?collection=docs"
|
||||
import * as __fd_glob_164 from "../content/docs/components/sonner.mdx?collection=docs"
|
||||
import * as __fd_glob_165 from "../content/docs/components/spinner.mdx?collection=docs"
|
||||
import * as __fd_glob_166 from "../content/docs/components/stars-scrolling-wheel.mdx?collection=docs"
|
||||
import * as __fd_glob_167 from "../content/docs/components/status-dashboard-navigation-bar.mdx?collection=docs"
|
||||
import * as __fd_glob_168 from "../content/docs/components/status.mdx?collection=docs"
|
||||
import * as __fd_glob_169 from "../content/docs/components/switch.mdx?collection=docs"
|
||||
import * as __fd_glob_170 from "../content/docs/components/table.mdx?collection=docs"
|
||||
import * as __fd_glob_171 from "../content/docs/components/tabs.mdx?collection=docs"
|
||||
import * as __fd_glob_172 from "../content/docs/components/tags.mdx?collection=docs"
|
||||
import * as __fd_glob_173 from "../content/docs/components/team-switcher-navigation-bar.mdx?collection=docs"
|
||||
import * as __fd_glob_174 from "../content/docs/components/terminal.mdx?collection=docs"
|
||||
import * as __fd_glob_175 from "../content/docs/components/textarea.mdx?collection=docs"
|
||||
import * as __fd_glob_176 from "../content/docs/components/timeline.mdx?collection=docs"
|
||||
import * as __fd_glob_177 from "../content/docs/components/toast.mdx?collection=docs"
|
||||
import * as __fd_glob_178 from "../content/docs/components/toggle-group.mdx?collection=docs"
|
||||
import * as __fd_glob_179 from "../content/docs/components/toggle.mdx?collection=docs"
|
||||
import * as __fd_glob_180 from "../content/docs/components/tooltip.mdx?collection=docs"
|
||||
import * as __fd_glob_181 from "../content/docs/components/two-tier-navigation-bar.mdx?collection=docs"
|
||||
import * as __fd_glob_182 from "../content/docs/components/typography.mdx?collection=docs"
|
||||
import * as __fd_glob_183 from "../content/docs/components/underline-navigation-bar.mdx?collection=docs"
|
||||
import * as __fd_glob_184 from "../content/docs/components/video-player.mdx?collection=docs"
|
||||
import * as __fd_glob_28 from "../content/docs/dark-mode/astro.mdx?collection=docs"
|
||||
import * as __fd_glob_29 from "../content/docs/dark-mode/index.mdx?collection=docs"
|
||||
import * as __fd_glob_30 from "../content/docs/dark-mode/next.mdx?collection=docs"
|
||||
import * as __fd_glob_31 from "../content/docs/dark-mode/remix.mdx?collection=docs"
|
||||
import * as __fd_glob_32 from "../content/docs/dark-mode/vite.mdx?collection=docs"
|
||||
import * as __fd_glob_5 from "../content/docs/figma.mdx?collection=docs"
|
||||
import * as __fd_glob_25 from "../content/docs/frameworks/index.mdx?collection=docs"
|
||||
import * as __fd_glob_26 from "../content/docs/frameworks/react.mdx?collection=docs"
|
||||
import * as __fd_glob_27 from "../content/docs/frameworks/vue.mdx?collection=docs"
|
||||
import * as __fd_glob_33 from "../content/docs/guides/page-builder.mdx?collection=docs"
|
||||
import * as __fd_glob_34 from "../content/docs/guides/visual-workflows.mdx?collection=docs"
|
||||
import * as __fd_glob_35 from "../content/docs/hanzo-react/getting-started.mdx?collection=docs"
|
||||
import * as __fd_glob_36 from "../content/docs/hanzo-react/hooks.mdx?collection=docs"
|
||||
import * as __fd_glob_6 from "../content/docs/index.mdx?collection=docs"
|
||||
import * as __fd_glob_37 from "../content/docs/installation/astro.mdx?collection=docs"
|
||||
import * as __fd_glob_38 from "../content/docs/installation/gatsby.mdx?collection=docs"
|
||||
import * as __fd_glob_39 from "../content/docs/installation/index.mdx?collection=docs"
|
||||
import * as __fd_glob_40 from "../content/docs/installation/laravel.mdx?collection=docs"
|
||||
import * as __fd_glob_41 from "../content/docs/installation/manual.mdx?collection=docs"
|
||||
import * as __fd_glob_42 from "../content/docs/installation/next.mdx?collection=docs"
|
||||
import * as __fd_glob_43 from "../content/docs/installation/remix.mdx?collection=docs"
|
||||
import * as __fd_glob_44 from "../content/docs/installation/vite.mdx?collection=docs"
|
||||
import * as __fd_glob_7 from "../content/docs/mcp.mdx?collection=docs"
|
||||
import * as __fd_glob_45 from "../content/docs/packages/index.mdx?collection=docs"
|
||||
import * as __fd_glob_186 from "../content/docs/testing/index.mdx?collection=docs"
|
||||
import * as __fd_glob_8 from "../content/docs/theming.mdx?collection=docs"
|
||||
import * as __fd_glob_185 from "../content/docs/white-label/index.mdx?collection=docs"
|
||||
import type * as Config from "../source.config"
|
||||
|
||||
const create = server<
|
||||
typeof Config,
|
||||
import("@hanzo/docs-mdx/runtime/types").InternalTypeConfig & {
|
||||
DocData: {}
|
||||
}
|
||||
>({ doc: { passthroughs: ["extractedReferences"] } })
|
||||
|
||||
export const docs = await create.docs(
|
||||
"docs",
|
||||
"content/docs",
|
||||
{},
|
||||
{
|
||||
"about.mdx": __fd_glob_0,
|
||||
"changelog.mdx": __fd_glob_1,
|
||||
"cli.mdx": __fd_glob_2,
|
||||
"components-json.mdx": __fd_glob_3,
|
||||
"components.mdx": __fd_glob_4,
|
||||
"figma.mdx": __fd_glob_5,
|
||||
"index.mdx": __fd_glob_6,
|
||||
"mcp.mdx": __fd_glob_7,
|
||||
"theming.mdx": __fd_glob_8,
|
||||
"ai/actions.mdx": __fd_glob_9,
|
||||
"ai/agents.mdx": __fd_glob_10,
|
||||
"ai/assistant.mdx": __fd_glob_11,
|
||||
"ai/chat.mdx": __fd_glob_12,
|
||||
"ai/code.mdx": __fd_glob_13,
|
||||
"ai/index.mdx": __fd_glob_14,
|
||||
"ai/models.mdx": __fd_glob_15,
|
||||
"ai/playground.mdx": __fd_glob_16,
|
||||
"ai/tools.mdx": __fd_glob_17,
|
||||
"ai/vision.mdx": __fd_glob_18,
|
||||
"ai/voice-settings.mdx": __fd_glob_19,
|
||||
"ai/voice.mdx": __fd_glob_20,
|
||||
"block-examples/billing.mdx": __fd_glob_21,
|
||||
"block-examples/compose-spec.mdx": __fd_glob_22,
|
||||
"block-examples/newsletter.mdx": __fd_glob_23,
|
||||
"block-examples/pricing.mdx": __fd_glob_24,
|
||||
"frameworks/index.mdx": __fd_glob_25,
|
||||
"frameworks/react.mdx": __fd_glob_26,
|
||||
"frameworks/vue.mdx": __fd_glob_27,
|
||||
"dark-mode/astro.mdx": __fd_glob_28,
|
||||
"dark-mode/index.mdx": __fd_glob_29,
|
||||
"dark-mode/next.mdx": __fd_glob_30,
|
||||
"dark-mode/remix.mdx": __fd_glob_31,
|
||||
"dark-mode/vite.mdx": __fd_glob_32,
|
||||
"guides/page-builder.mdx": __fd_glob_33,
|
||||
"guides/visual-workflows.mdx": __fd_glob_34,
|
||||
"hanzo-react/getting-started.mdx": __fd_glob_35,
|
||||
"hanzo-react/hooks.mdx": __fd_glob_36,
|
||||
"installation/astro.mdx": __fd_glob_37,
|
||||
"installation/gatsby.mdx": __fd_glob_38,
|
||||
"installation/index.mdx": __fd_glob_39,
|
||||
"installation/laravel.mdx": __fd_glob_40,
|
||||
"installation/manual.mdx": __fd_glob_41,
|
||||
"installation/next.mdx": __fd_glob_42,
|
||||
"installation/remix.mdx": __fd_glob_43,
|
||||
"installation/vite.mdx": __fd_glob_44,
|
||||
"packages/index.mdx": __fd_glob_45,
|
||||
"components/3d-card.mdx": __fd_glob_46,
|
||||
"components/3d-marquee.mdx": __fd_glob_47,
|
||||
"components/3d-pin.mdx": __fd_glob_48,
|
||||
"components/accordion.mdx": __fd_glob_49,
|
||||
"components/advanced-navigation-bar.mdx": __fd_glob_50,
|
||||
"components/ai-model-selector-navigation-bar.mdx": __fd_glob_51,
|
||||
"components/alert-dialog.mdx": __fd_glob_52,
|
||||
"components/alert.mdx": __fd_glob_53,
|
||||
"components/android.mdx": __fd_glob_54,
|
||||
"components/animated-beam.mdx": __fd_glob_55,
|
||||
"components/animated-cursor.mdx": __fd_glob_56,
|
||||
"components/animated-testimonials.mdx": __fd_glob_57,
|
||||
"components/animated-tooltip.mdx": __fd_glob_58,
|
||||
"components/animation-components.mdx": __fd_glob_59,
|
||||
"components/announcement.mdx": __fd_glob_60,
|
||||
"components/app-switcher-navigation-bar.mdx": __fd_glob_61,
|
||||
"components/apple-cards-carousel.mdx": __fd_glob_62,
|
||||
"components/apple-hello-effect.mdx": __fd_glob_63,
|
||||
"components/aspect-ratio.mdx": __fd_glob_64,
|
||||
"components/avatar-group.mdx": __fd_glob_65,
|
||||
"components/avatar.mdx": __fd_glob_66,
|
||||
"components/badge.mdx": __fd_glob_67,
|
||||
"components/banner.mdx": __fd_glob_68,
|
||||
"components/breadcrumb-and-filters-navigation-bar.mdx": __fd_glob_69,
|
||||
"components/breadcrumb-navigation-bar.mdx": __fd_glob_70,
|
||||
"components/breadcrumb.mdx": __fd_glob_71,
|
||||
"components/button-group.mdx": __fd_glob_72,
|
||||
"components/button.mdx": __fd_glob_73,
|
||||
"components/calendar.mdx": __fd_glob_74,
|
||||
"components/card.mdx": __fd_glob_75,
|
||||
"components/carousel.mdx": __fd_glob_76,
|
||||
"components/centered-logo-navigation-bar.mdx": __fd_glob_77,
|
||||
"components/checkbox.mdx": __fd_glob_78,
|
||||
"components/choicebox.mdx": __fd_glob_79,
|
||||
"components/code-block.mdx": __fd_glob_80,
|
||||
"components/code-editor.mdx": __fd_glob_81,
|
||||
"components/code-tabs.mdx": __fd_glob_82,
|
||||
"components/collaboration-navigation-bar.mdx": __fd_glob_83,
|
||||
"components/collapsible.mdx": __fd_glob_84,
|
||||
"components/color-picker.mdx": __fd_glob_85,
|
||||
"components/combobox.mdx": __fd_glob_86,
|
||||
"components/command.mdx": __fd_glob_87,
|
||||
"components/communication-navigation-bar.mdx": __fd_glob_88,
|
||||
"components/comparison.mdx": __fd_glob_89,
|
||||
"components/context-menu.mdx": __fd_glob_90,
|
||||
"components/context-switcher-navigation-bar.mdx": __fd_glob_91,
|
||||
"components/credit-card.mdx": __fd_glob_92,
|
||||
"components/cursor.mdx": __fd_glob_93,
|
||||
"components/dashboard-navigation-bar.mdx": __fd_glob_94,
|
||||
"components/data-table.mdx": __fd_glob_95,
|
||||
"components/date-picker.mdx": __fd_glob_96,
|
||||
"components/desktop-hooks.mdx": __fd_glob_97,
|
||||
"components/desktop-spotlight.mdx": __fd_glob_98,
|
||||
"components/desktop-window.mdx": __fd_glob_99,
|
||||
"components/desktop.mdx": __fd_glob_100,
|
||||
"components/dialog-stack.mdx": __fd_glob_101,
|
||||
"components/dialog.mdx": __fd_glob_102,
|
||||
"components/dock.mdx": __fd_glob_103,
|
||||
"components/drawer.mdx": __fd_glob_104,
|
||||
"components/dropdown-menu.mdx": __fd_glob_105,
|
||||
"components/dropzone.mdx": __fd_glob_106,
|
||||
"components/e-commerce-navigation-bar.mdx": __fd_glob_107,
|
||||
"components/editor.mdx": __fd_glob_108,
|
||||
"components/empty.mdx": __fd_glob_109,
|
||||
"components/field.mdx": __fd_glob_110,
|
||||
"components/form.mdx": __fd_glob_111,
|
||||
"components/gantt.mdx": __fd_glob_112,
|
||||
"components/glimpse.mdx": __fd_glob_113,
|
||||
"components/grid-pattern.mdx": __fd_glob_114,
|
||||
"components/hover-card.mdx": __fd_glob_115,
|
||||
"components/icon-navigation-bar.mdx": __fd_glob_116,
|
||||
"components/image-crop.mdx": __fd_glob_117,
|
||||
"components/image-zoom.mdx": __fd_glob_118,
|
||||
"components/index.mdx": __fd_glob_119,
|
||||
"components/input-group.mdx": __fd_glob_120,
|
||||
"components/input-otp.mdx": __fd_glob_121,
|
||||
"components/input.mdx": __fd_glob_122,
|
||||
"components/interactive-grid-pattern.mdx": __fd_glob_123,
|
||||
"components/iphone-15-pro.mdx": __fd_glob_124,
|
||||
"components/item.mdx": __fd_glob_125,
|
||||
"components/kanban.mdx": __fd_glob_126,
|
||||
"components/kbd.mdx": __fd_glob_127,
|
||||
"components/label.mdx": __fd_glob_128,
|
||||
"components/limelight-nav.mdx": __fd_glob_129,
|
||||
"components/list.mdx": __fd_glob_130,
|
||||
"components/macos-dock.mdx": __fd_glob_131,
|
||||
"components/magnetic.mdx": __fd_glob_132,
|
||||
"components/marquee.mdx": __fd_glob_133,
|
||||
"components/menu-dock.mdx": __fd_glob_134,
|
||||
"components/menubar.mdx": __fd_glob_135,
|
||||
"components/message-dock.mdx": __fd_glob_136,
|
||||
"components/mini-calendar.mdx": __fd_glob_137,
|
||||
"components/minimal-tiptap.mdx": __fd_glob_138,
|
||||
"components/motion-highlight.mdx": __fd_glob_139,
|
||||
"components/navigation-menu.mdx": __fd_glob_140,
|
||||
"components/pagination.mdx": __fd_glob_141,
|
||||
"components/particles-background.mdx": __fd_glob_142,
|
||||
"components/particles.mdx": __fd_glob_143,
|
||||
"components/pill.mdx": __fd_glob_144,
|
||||
"components/pin-list.mdx": __fd_glob_145,
|
||||
"components/pixel-image.mdx": __fd_glob_146,
|
||||
"components/popover.mdx": __fd_glob_147,
|
||||
"components/progress.mdx": __fd_glob_148,
|
||||
"components/qr-code.mdx": __fd_glob_149,
|
||||
"components/radio-group.mdx": __fd_glob_150,
|
||||
"components/relative-time.mdx": __fd_glob_151,
|
||||
"components/resizable.mdx": __fd_glob_152,
|
||||
"components/safari.mdx": __fd_glob_153,
|
||||
"components/sandbox.mdx": __fd_glob_154,
|
||||
"components/scroll-area.mdx": __fd_glob_155,
|
||||
"components/search-and-toggle-navigation-bar.mdx": __fd_glob_156,
|
||||
"components/select.mdx": __fd_glob_157,
|
||||
"components/separator.mdx": __fd_glob_158,
|
||||
"components/sheet.mdx": __fd_glob_159,
|
||||
"components/simple-navigation-bar.mdx": __fd_glob_160,
|
||||
"components/skeleton.mdx": __fd_glob_161,
|
||||
"components/slider.mdx": __fd_glob_162,
|
||||
"components/snippet.mdx": __fd_glob_163,
|
||||
"components/sonner.mdx": __fd_glob_164,
|
||||
"components/spinner.mdx": __fd_glob_165,
|
||||
"components/stars-scrolling-wheel.mdx": __fd_glob_166,
|
||||
"components/status-dashboard-navigation-bar.mdx": __fd_glob_167,
|
||||
"components/status.mdx": __fd_glob_168,
|
||||
"components/switch.mdx": __fd_glob_169,
|
||||
"components/table.mdx": __fd_glob_170,
|
||||
"components/tabs.mdx": __fd_glob_171,
|
||||
"components/tags.mdx": __fd_glob_172,
|
||||
"components/team-switcher-navigation-bar.mdx": __fd_glob_173,
|
||||
"components/terminal.mdx": __fd_glob_174,
|
||||
"components/textarea.mdx": __fd_glob_175,
|
||||
"components/timeline.mdx": __fd_glob_176,
|
||||
"components/toast.mdx": __fd_glob_177,
|
||||
"components/toggle-group.mdx": __fd_glob_178,
|
||||
"components/toggle.mdx": __fd_glob_179,
|
||||
"components/tooltip.mdx": __fd_glob_180,
|
||||
"components/two-tier-navigation-bar.mdx": __fd_glob_181,
|
||||
"components/typography.mdx": __fd_glob_182,
|
||||
"components/underline-navigation-bar.mdx": __fd_glob_183,
|
||||
"components/video-player.mdx": __fd_glob_184,
|
||||
"white-label/index.mdx": __fd_glob_185,
|
||||
"testing/index.mdx": __fd_glob_186,
|
||||
}
|
||||
)
|
||||
@@ -0,0 +1,40 @@
|
||||
// source.config.ts
|
||||
import {
|
||||
defineConfig,
|
||||
defineDocs,
|
||||
frontmatterSchema
|
||||
} from "@hanzo/docs-mdx/config";
|
||||
import rehypePrettyCode from "rehype-pretty-code";
|
||||
import { z } from "zod";
|
||||
var source_config_default = defineConfig({
|
||||
mdxOptions: {
|
||||
rehypePlugins: [
|
||||
[
|
||||
rehypePrettyCode,
|
||||
{
|
||||
theme: {
|
||||
dark: "github-dark",
|
||||
light: "github-light-default"
|
||||
},
|
||||
keepBackground: false
|
||||
}
|
||||
]
|
||||
]
|
||||
}
|
||||
});
|
||||
var docs = defineDocs({
|
||||
dir: "content/docs",
|
||||
docs: {
|
||||
schema: frontmatterSchema.extend({
|
||||
links: z.object({
|
||||
doc: z.string().optional(),
|
||||
api: z.string().optional()
|
||||
}).optional(),
|
||||
toc: z.boolean().optional()
|
||||
})
|
||||
}
|
||||
});
|
||||
export {
|
||||
source_config_default as default,
|
||||
docs
|
||||
};
|
||||
@@ -1,12 +0,0 @@
|
||||
node_modules/
|
||||
.next/
|
||||
out/
|
||||
build/
|
||||
dist/
|
||||
next-env.d.ts
|
||||
__registry__/
|
||||
.source/
|
||||
*.config.js
|
||||
*.config.mjs
|
||||
.turbo/
|
||||
coverage/
|
||||
@@ -1,40 +0,0 @@
|
||||
{
|
||||
"env": {
|
||||
"browser": true,
|
||||
"es2021": true,
|
||||
"node": true
|
||||
},
|
||||
"parser": "@typescript-eslint/parser",
|
||||
"parserOptions": {
|
||||
"ecmaVersion": "latest",
|
||||
"sourceType": "module",
|
||||
"ecmaFeatures": {
|
||||
"jsx": true
|
||||
}
|
||||
},
|
||||
"plugins": ["@typescript-eslint", "react"],
|
||||
"ignorePatterns": [
|
||||
"__registry__/**",
|
||||
".source/**",
|
||||
".next/**",
|
||||
"out/**",
|
||||
"build/**",
|
||||
"dist/**",
|
||||
".turbo/**",
|
||||
"node_modules/**"
|
||||
],
|
||||
"rules": {
|
||||
"@typescript-eslint/no-explicit-any": "warn",
|
||||
"@typescript-eslint/no-empty-object-type": "warn",
|
||||
"@typescript-eslint/ban-ts-comment": "warn",
|
||||
"@typescript-eslint/no-unused-vars": "warn",
|
||||
"@typescript-eslint/no-require-imports": "warn",
|
||||
"@typescript-eslint/no-this-alias": "warn",
|
||||
"@typescript-eslint/triple-slash-reference": "warn",
|
||||
"@typescript-eslint/ban-types": "off",
|
||||
"react/no-unescaped-entities": "warn",
|
||||
"react/jsx-no-comment-textnodes": "warn",
|
||||
"react/no-find-dom-node": "warn",
|
||||
"prefer-const": "warn"
|
||||
}
|
||||
}
|
||||
@@ -4,3 +4,5 @@ node_modules
|
||||
build
|
||||
.contentlayer
|
||||
apps/web/pages/api/registry.json
|
||||
**/fixtures
|
||||
deprecated
|
||||
|
||||
@@ -63,10 +63,11 @@ export default function Dashboard() {
|
||||
<div className="hidden bg-muted lg:block">
|
||||
<Image
|
||||
src="/placeholder.svg"
|
||||
alt="Image"
|
||||
alt="Authentication page cover image"
|
||||
width="1920"
|
||||
height="1080"
|
||||
className="h-full w-full object-cover dark:brightness-[0.2] dark:grayscale"
|
||||
loading="lazy"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -267,6 +267,7 @@ export default function Dashboard() {
|
||||
height={36}
|
||||
alt="Avatar"
|
||||
className="overflow-hidden rounded-full"
|
||||
loading="lazy"
|
||||
/>
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
|
||||
@@ -256,6 +256,7 @@ export default function Dashboard() {
|
||||
height={36}
|
||||
alt="Avatar"
|
||||
className="overflow-hidden rounded-full"
|
||||
loading="lazy"
|
||||
/>
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
@@ -356,6 +357,7 @@ export default function Dashboard() {
|
||||
height="64"
|
||||
src="/placeholder.svg"
|
||||
width="64"
|
||||
loading="lazy"
|
||||
/>
|
||||
</TableCell>
|
||||
<TableCell className="font-medium">
|
||||
@@ -401,6 +403,7 @@ export default function Dashboard() {
|
||||
height="64"
|
||||
src="/placeholder.svg"
|
||||
width="64"
|
||||
loading="lazy"
|
||||
/>
|
||||
</TableCell>
|
||||
<TableCell className="font-medium">
|
||||
@@ -446,6 +449,7 @@ export default function Dashboard() {
|
||||
height="64"
|
||||
src="/placeholder.svg"
|
||||
width="64"
|
||||
loading="lazy"
|
||||
/>
|
||||
</TableCell>
|
||||
<TableCell className="font-medium">
|
||||
@@ -491,6 +495,7 @@ export default function Dashboard() {
|
||||
height="64"
|
||||
src="/placeholder.svg"
|
||||
width="64"
|
||||
loading="lazy"
|
||||
/>
|
||||
</TableCell>
|
||||
<TableCell className="font-medium">
|
||||
@@ -536,6 +541,7 @@ export default function Dashboard() {
|
||||
height="64"
|
||||
src="/placeholder.svg"
|
||||
width="64"
|
||||
loading="lazy"
|
||||
/>
|
||||
</TableCell>
|
||||
<TableCell className="font-medium">
|
||||
@@ -581,6 +587,7 @@ export default function Dashboard() {
|
||||
height="64"
|
||||
src="/placeholder.svg"
|
||||
width="64"
|
||||
loading="lazy"
|
||||
/>
|
||||
</TableCell>
|
||||
<TableCell className="font-medium">
|
||||
|
||||
@@ -259,6 +259,7 @@ export default function Dashboard() {
|
||||
src="/placeholder-user.jpg"
|
||||
width={36}
|
||||
height={36}
|
||||
loading="lazy"
|
||||
alt="Avatar"
|
||||
className="overflow-hidden rounded-full"
|
||||
/>
|
||||
@@ -551,24 +552,27 @@ export default function Dashboard() {
|
||||
height="300"
|
||||
src="/placeholder.svg"
|
||||
width="300"
|
||||
loading="lazy"
|
||||
/>
|
||||
<div className="grid grid-cols-3 gap-2">
|
||||
<button>
|
||||
<Image
|
||||
alt="Product image"
|
||||
alt="Product thumbnail image"
|
||||
className="aspect-square w-full rounded-md object-cover"
|
||||
height="84"
|
||||
src="/placeholder.svg"
|
||||
width="84"
|
||||
loading="lazy"
|
||||
/>
|
||||
</button>
|
||||
<button>
|
||||
<Image
|
||||
alt="Product image"
|
||||
alt="Product thumbnail image"
|
||||
className="aspect-square w-full rounded-md object-cover"
|
||||
height="84"
|
||||
src="/placeholder.svg"
|
||||
width="84"
|
||||
loading="lazy"
|
||||
/>
|
||||
</button>
|
||||
<button className="flex aspect-square w-full items-center justify-center rounded-md border border-dashed">
|
||||
|
||||
@@ -203,6 +203,17 @@ export const Index: Record<string, any> = {
|
||||
subcategory: "undefined",
|
||||
chunks: []
|
||||
},
|
||||
"direction": {
|
||||
name: "direction",
|
||||
type: "components:ui",
|
||||
registryDependencies: undefined,
|
||||
component: React.lazy(() => import("@/registry/default/ui/direction")),
|
||||
source: "",
|
||||
files: ["registry/default/ui/direction.tsx"],
|
||||
category: "undefined",
|
||||
subcategory: "undefined",
|
||||
chunks: []
|
||||
},
|
||||
"dialog": {
|
||||
name: "dialog",
|
||||
type: "components:ui",
|
||||
@@ -236,6 +247,17 @@ export const Index: Record<string, any> = {
|
||||
subcategory: "undefined",
|
||||
chunks: []
|
||||
},
|
||||
"empty-state": {
|
||||
name: "empty-state",
|
||||
type: "components:ui",
|
||||
registryDependencies: ["button"],
|
||||
component: React.lazy(() => import("@/registry/default/ui/empty-state")),
|
||||
source: "",
|
||||
files: ["registry/default/ui/empty-state.tsx"],
|
||||
category: "undefined",
|
||||
subcategory: "undefined",
|
||||
chunks: []
|
||||
},
|
||||
"gantt": {
|
||||
name: "gantt",
|
||||
type: "components:ui",
|
||||
@@ -1787,6 +1809,17 @@ export const Index: Record<string, any> = {
|
||||
subcategory: "undefined",
|
||||
chunks: []
|
||||
},
|
||||
"youtube-pip-player": {
|
||||
name: "youtube-pip-player",
|
||||
type: "components:ui",
|
||||
registryDependencies: undefined,
|
||||
component: React.lazy(() => import("@/registry/default/ui/youtube-pip-player")),
|
||||
source: "",
|
||||
files: ["registry/default/ui/youtube-pip-player.tsx"],
|
||||
category: "undefined",
|
||||
subcategory: "undefined",
|
||||
chunks: []
|
||||
},
|
||||
"music-player": {
|
||||
name: "music-player",
|
||||
type: "components:ui",
|
||||
@@ -4339,6 +4372,17 @@ export const Index: Record<string, any> = {
|
||||
subcategory: "undefined",
|
||||
chunks: []
|
||||
},
|
||||
"youtube-pip-player-demo": {
|
||||
name: "youtube-pip-player-demo",
|
||||
type: "components:example",
|
||||
registryDependencies: ["youtube-pip-player"],
|
||||
component: React.lazy(() => import("@/registry/default/example/youtube-pip-player-demo")),
|
||||
source: "",
|
||||
files: ["registry/default/example/youtube-pip-player-demo.tsx"],
|
||||
category: "undefined",
|
||||
subcategory: "undefined",
|
||||
chunks: []
|
||||
},
|
||||
"android-demo": {
|
||||
name: "android-demo",
|
||||
type: "components:example",
|
||||
|
||||
@@ -0,0 +1,143 @@
|
||||
/**
|
||||
* docs-no-404.test.ts
|
||||
*
|
||||
* Static validation: every URL in docs.ts config has a matching MDX file.
|
||||
* No server needed — runs in CI before deployment.
|
||||
*
|
||||
* Run: pnpm test (via vitest)
|
||||
*/
|
||||
import { describe, it, expect } from 'vitest'
|
||||
import fs from 'node:fs'
|
||||
import path from 'node:path'
|
||||
|
||||
const CONTENT_DIR = path.resolve(__dirname, '../content/docs')
|
||||
const CONFIG_PATH = path.resolve(__dirname, '../config/docs.ts')
|
||||
|
||||
/** Extract all href values from docs.ts config */
|
||||
function extractUrlsFromConfig(): string[] {
|
||||
const content = fs.readFileSync(CONFIG_PATH, 'utf-8')
|
||||
const hrefRegex = /href:\s*["'`]([^"'`]+)["'`]/g
|
||||
const urls: string[] = []
|
||||
let match
|
||||
while ((match = hrefRegex.exec(content)) !== null) {
|
||||
urls.push(match[1])
|
||||
}
|
||||
return urls.filter(u => u.startsWith('/docs'))
|
||||
}
|
||||
|
||||
/** Convert a /docs/... URL to its expected MDX file path */
|
||||
function urlToMdxPath(url: string): string[] {
|
||||
// /docs -> index.mdx
|
||||
// /docs/charts/bar -> charts/bar.mdx OR charts/bar/index.mdx
|
||||
const slug = url.replace(/^\/docs\/?/, '') || ''
|
||||
if (!slug) {
|
||||
return [path.join(CONTENT_DIR, 'index.mdx')]
|
||||
}
|
||||
return [
|
||||
path.join(CONTENT_DIR, `${slug}.mdx`),
|
||||
path.join(CONTENT_DIR, slug, 'index.mdx'),
|
||||
]
|
||||
}
|
||||
|
||||
/** Recursively find all MDX files in content/docs */
|
||||
function findAllMdxFiles(dir: string, prefix = ''): string[] {
|
||||
const results: string[] = []
|
||||
if (!fs.existsSync(dir)) return results
|
||||
|
||||
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
|
||||
const fullPath = path.join(dir, entry.name)
|
||||
if (entry.isDirectory()) {
|
||||
results.push(...findAllMdxFiles(fullPath, `${prefix}${entry.name}/`))
|
||||
} else if (entry.name.endsWith('.mdx')) {
|
||||
const slug = entry.name === 'index.mdx'
|
||||
? prefix.replace(/\/$/, '')
|
||||
: `${prefix}${entry.name.replace('.mdx', '')}`
|
||||
if (slug) {
|
||||
results.push(`/docs/${slug}`)
|
||||
} else {
|
||||
results.push('/docs')
|
||||
}
|
||||
}
|
||||
}
|
||||
return results
|
||||
}
|
||||
|
||||
describe('Documentation pages — zero 404s', () => {
|
||||
const configUrls = extractUrlsFromConfig()
|
||||
const mdxPages = findAllMdxFiles(CONTENT_DIR)
|
||||
|
||||
it('should find docs config with URLs', () => {
|
||||
expect(configUrls.length).toBeGreaterThan(100)
|
||||
})
|
||||
|
||||
it('should find MDX content files', () => {
|
||||
expect(mdxPages.length).toBeGreaterThan(100)
|
||||
})
|
||||
|
||||
describe('every config URL has a matching MDX file', () => {
|
||||
for (const url of configUrls) {
|
||||
it(`${url}`, () => {
|
||||
const possiblePaths = urlToMdxPath(url)
|
||||
const exists = possiblePaths.some(p => fs.existsSync(p))
|
||||
if (!exists) {
|
||||
throw new Error(
|
||||
`Missing MDX file for ${url}. Expected one of:\n` +
|
||||
possiblePaths.map(p => ` - ${p}`).join('\n')
|
||||
)
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
describe('every MDX file is reachable from config', () => {
|
||||
const configSet = new Set(configUrls)
|
||||
const unreachable: string[] = []
|
||||
|
||||
for (const page of mdxPages) {
|
||||
if (!configSet.has(page)) {
|
||||
unreachable.push(page)
|
||||
}
|
||||
}
|
||||
|
||||
it('should have no orphaned MDX files (or document known exceptions)', () => {
|
||||
// Allow a small number of known orphans (category index pages, etc.)
|
||||
const KNOWN_ORPHANS = new Set([
|
||||
'/docs/ai/voice-settings',
|
||||
'/docs/components/animation-components',
|
||||
])
|
||||
const unexpected = unreachable.filter(u => !KNOWN_ORPHANS.has(u))
|
||||
if (unexpected.length > 0) {
|
||||
console.warn(`Orphaned MDX files (not in nav config):\n${unexpected.map(u => ` ${u}`).join('\n')}`)
|
||||
}
|
||||
// Warn but don't fail for orphans — they just won't be in nav
|
||||
expect(true).toBe(true)
|
||||
})
|
||||
})
|
||||
|
||||
it('summary: config URLs vs MDX files', () => {
|
||||
const configSet = new Set(configUrls)
|
||||
const mdxSet = new Set(mdxPages)
|
||||
|
||||
const missingMdx = configUrls.filter(u => {
|
||||
const paths = urlToMdxPath(u)
|
||||
return !paths.some(p => fs.existsSync(p))
|
||||
})
|
||||
const orphanedMdx = mdxPages.filter(p => !configSet.has(p))
|
||||
|
||||
console.log(`\n📊 Docs coverage report:`)
|
||||
console.log(` Config URLs: ${configUrls.length}`)
|
||||
console.log(` MDX files: ${mdxPages.length}`)
|
||||
console.log(` Missing MDX: ${missingMdx.length}`)
|
||||
console.log(` Orphaned: ${orphanedMdx.length}`)
|
||||
|
||||
if (missingMdx.length > 0) {
|
||||
console.error(`\n❌ URLs with no MDX file:\n${missingMdx.map(u => ` ${u}`).join('\n')}`)
|
||||
}
|
||||
if (orphanedMdx.length > 0) {
|
||||
console.warn(`\n⚠️ MDX files not in nav:\n${orphanedMdx.map(u => ` ${u}`).join('\n')}`)
|
||||
}
|
||||
|
||||
// This is the hard check — every config URL must have content
|
||||
expect(missingMdx).toEqual([])
|
||||
})
|
||||
})
|
||||
@@ -42,8 +42,8 @@ describe("@hanzo/ui namespace imports - Type Resolution", () => {
|
||||
* import { GridPattern } from '@hanzo/ui/pattern/grid'
|
||||
* ```
|
||||
*
|
||||
* The package exports are properly configured in pkg/ui/package.json.
|
||||
* Build artifacts exist in pkg/ui/dist/{code,3d,pattern}/.
|
||||
* The package exports are properly configured in pkgs/ui/package.json.
|
||||
* Build artifacts exist in pkgs/ui/dist/{code,3d,pattern}/.
|
||||
*
|
||||
* Vitest runtime resolution fails due to internal cross-package imports
|
||||
* (e.g., CodeBlock imports from @hanzo/ui/lib/utils), which require
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
import { Suspense } from "react"
|
||||
|
||||
import { getAllBlockIds } from "@/lib/blocks"
|
||||
import { registryCategories } from "@/lib/categories"
|
||||
import { BlockDisplay } from "@/components/block-display"
|
||||
import { SkeletonCardGrid } from "@/components/skeleton-card"
|
||||
|
||||
export const revalidate = false
|
||||
export const dynamic = "force-static"
|
||||
@@ -12,19 +15,30 @@ export async function generateStaticParams() {
|
||||
}))
|
||||
}
|
||||
|
||||
async function BlockGallery({ categories }: { categories: string[] }) {
|
||||
const blocks = await getAllBlockIds(["registry:block"], categories)
|
||||
|
||||
return (
|
||||
<>
|
||||
{blocks.map((name) => (
|
||||
<BlockDisplay name={name} key={name} />
|
||||
))}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default async function BlocksPage({
|
||||
params,
|
||||
}: {
|
||||
params: Promise<{ categories?: string[] }>
|
||||
}) {
|
||||
const { categories = [] } = await params
|
||||
const blocks = await getAllBlockIds(["registry:block"], categories)
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-12 md:gap-24">
|
||||
{blocks.map((name) => (
|
||||
<BlockDisplay name={name} key={name} />
|
||||
))}
|
||||
<Suspense fallback={<SkeletonCardGrid count={9} />}>
|
||||
<BlockGallery categories={categories} />
|
||||
</Suspense>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,311 @@
|
||||
"use client"
|
||||
|
||||
import * as React from "react"
|
||||
import { Index } from "@/__registry__"
|
||||
import { AlertCircle, RefreshCw } from "lucide-react"
|
||||
|
||||
import { Badge } from "@/registry/default/ui/badge"
|
||||
import { Button } from "@/registry/default/ui/button"
|
||||
import { Card } from "@/registry/default/ui/card"
|
||||
import { Input } from "@/registry/default/ui/input"
|
||||
import { ScrollArea } from "@/registry/default/ui/scroll-area"
|
||||
import { Skeleton } from "@/registry/default/ui/skeleton"
|
||||
import { Spinner } from "@/registry/default/ui/spinner"
|
||||
|
||||
interface IframeState {
|
||||
loaded: boolean
|
||||
error: boolean
|
||||
showContent: boolean
|
||||
}
|
||||
|
||||
function BlockPreview({ blockName }: { blockName: string }) {
|
||||
const [state, setState] = React.useState<IframeState>({
|
||||
loaded: false,
|
||||
error: false,
|
||||
showContent: false,
|
||||
})
|
||||
const [isInView, setIsInView] = React.useState(false)
|
||||
const iframeRef = React.useRef<HTMLIFrameElement>(null)
|
||||
const containerRef = React.useRef<HTMLDivElement>(null)
|
||||
const minLoadTimeRef = React.useRef<NodeJS.Timeout | undefined>(undefined)
|
||||
|
||||
// Intersection Observer for lazy loading
|
||||
React.useEffect(() => {
|
||||
const observer = new IntersectionObserver(
|
||||
(entries) => {
|
||||
entries.forEach((entry) => {
|
||||
if (entry.isIntersecting) {
|
||||
setIsInView(true)
|
||||
}
|
||||
})
|
||||
},
|
||||
{
|
||||
rootMargin: "100px", // Start loading 100px before entering viewport
|
||||
}
|
||||
)
|
||||
|
||||
if (containerRef.current) {
|
||||
observer.observe(containerRef.current)
|
||||
}
|
||||
|
||||
return () => {
|
||||
observer.disconnect()
|
||||
}
|
||||
}, [])
|
||||
|
||||
const handleLoad = React.useCallback(() => {
|
||||
setState((prev) => ({ ...prev, loaded: true, error: false }))
|
||||
// Ensure minimum loading time of 200ms for smooth UX
|
||||
minLoadTimeRef.current = setTimeout(() => {
|
||||
setState((prev) => ({ ...prev, showContent: true }))
|
||||
}, 200)
|
||||
}, [])
|
||||
|
||||
const handleError = React.useCallback(() => {
|
||||
setState({ loaded: true, error: true, showContent: true })
|
||||
}, [])
|
||||
|
||||
const handleRetry = React.useCallback((e: React.MouseEvent) => {
|
||||
e.preventDefault()
|
||||
e.stopPropagation()
|
||||
setState({ loaded: false, error: false, showContent: false })
|
||||
if (iframeRef.current) {
|
||||
// Force reload by resetting src
|
||||
const src = iframeRef.current.src
|
||||
iframeRef.current.src = ""
|
||||
setTimeout(() => {
|
||||
if (iframeRef.current) {
|
||||
iframeRef.current.src = src
|
||||
}
|
||||
}, 0)
|
||||
}
|
||||
}, [])
|
||||
|
||||
React.useEffect(() => {
|
||||
return () => {
|
||||
if (minLoadTimeRef.current) {
|
||||
clearTimeout(minLoadTimeRef.current)
|
||||
}
|
||||
}
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={containerRef}
|
||||
className="relative aspect-[4/3] min-h-[400px] bg-muted/50"
|
||||
>
|
||||
{/* Loading skeleton */}
|
||||
{!state.showContent && (
|
||||
<div className="absolute inset-0 flex items-center justify-center">
|
||||
<div className="flex flex-col items-center gap-2">
|
||||
<Spinner className="size-6" />
|
||||
<p className="text-sm text-muted-foreground">
|
||||
{isInView ? "Loading preview..." : "Scroll to load..."}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Error state */}
|
||||
{state.error && (
|
||||
<div className="absolute inset-0 flex items-center justify-center">
|
||||
<div className="flex flex-col items-center gap-3 p-4 text-center">
|
||||
<AlertCircle className="size-8 text-destructive" />
|
||||
<div>
|
||||
<p className="text-sm font-medium">Failed to load preview</p>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
The block could not be loaded
|
||||
</p>
|
||||
</div>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={handleRetry}
|
||||
className="gap-2"
|
||||
>
|
||||
<RefreshCw className="size-3" />
|
||||
Retry
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Iframe with lazy loading and fade-in animation */}
|
||||
{isInView && (
|
||||
<iframe
|
||||
ref={iframeRef}
|
||||
src={`/view/${blockName}`}
|
||||
className={`h-full w-full border-0 transition-opacity duration-300 ${
|
||||
state.showContent && !state.error ? "opacity-100" : "opacity-0"
|
||||
}`}
|
||||
title={blockName}
|
||||
onLoad={handleLoad}
|
||||
onError={handleError}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default function BuilderPage() {
|
||||
const [filter, setFilter] = React.useState("")
|
||||
const [blocks, setBlocks] = React.useState<string[]>([])
|
||||
const [isLoadingBlocks, setIsLoadingBlocks] = React.useState(true)
|
||||
|
||||
React.useEffect(() => {
|
||||
// Simulate async block loading with minimum display time
|
||||
const loadBlocks = async () => {
|
||||
setIsLoadingBlocks(true)
|
||||
const startTime = Date.now()
|
||||
|
||||
// Get blocks from registry (flat structure)
|
||||
const blockIds = Object.keys(Index).filter((key) => {
|
||||
const item = Index[key]
|
||||
return item?.type === "components:block"
|
||||
})
|
||||
|
||||
// Ensure minimum loading time of 200ms
|
||||
const elapsed = Date.now() - startTime
|
||||
if (elapsed < 200) {
|
||||
await new Promise((resolve) => setTimeout(resolve, 200 - elapsed))
|
||||
}
|
||||
|
||||
setBlocks(blockIds.sort())
|
||||
setIsLoadingBlocks(false)
|
||||
}
|
||||
|
||||
loadBlocks()
|
||||
}, [])
|
||||
|
||||
const filteredBlocks = React.useMemo(() => {
|
||||
if (!filter) return blocks
|
||||
return blocks.filter((block) =>
|
||||
block.toLowerCase().includes(filter.toLowerCase())
|
||||
)
|
||||
}, [blocks, filter])
|
||||
|
||||
return (
|
||||
<div className="flex h-screen flex-col">
|
||||
{/* Header with loading state */}
|
||||
<div className="border-b p-4">
|
||||
<div className="flex items-center gap-4">
|
||||
<div className="flex items-center gap-2">
|
||||
<h1 className="text-lg font-semibold">Blocks</h1>
|
||||
{isLoadingBlocks && <Spinner className="size-4" />}
|
||||
</div>
|
||||
<Input
|
||||
placeholder="Filter blocks..."
|
||||
value={filter}
|
||||
onChange={(e) => setFilter(e.target.value)}
|
||||
className="max-w-xs"
|
||||
disabled={isLoadingBlocks}
|
||||
/>
|
||||
<span className="text-sm text-muted-foreground">
|
||||
{isLoadingBlocks ? "Loading..." : `${filteredBlocks.length} blocks`}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Grid with loading and empty states */}
|
||||
<ScrollArea className="flex-1">
|
||||
<div className="grid grid-cols-1 gap-4 p-6 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4">
|
||||
{/* Loading skeleton */}
|
||||
{isLoadingBlocks &&
|
||||
Array.from({ length: 8 }).map((_, i) => (
|
||||
<Card
|
||||
key={i}
|
||||
className="overflow-hidden rounded-lg border-2 border-border"
|
||||
>
|
||||
<Skeleton className="aspect-[4/3] min-h-[400px] w-full rounded-none" />
|
||||
<div className="border-t bg-card p-4">
|
||||
<Skeleton className="mb-2 h-5 w-3/4" />
|
||||
<Skeleton className="mb-1 h-4 w-20" />
|
||||
<Skeleton className="mt-2 h-4 w-full" />
|
||||
<Skeleton className="mt-1 h-4 w-2/3" />
|
||||
</div>
|
||||
</Card>
|
||||
))}
|
||||
|
||||
{/* Empty state */}
|
||||
{!isLoadingBlocks && filteredBlocks.length === 0 && (
|
||||
<div className="col-span-full flex min-h-[400px] items-center justify-center">
|
||||
<div className="text-center">
|
||||
<p className="text-lg font-medium text-muted-foreground">
|
||||
No blocks found
|
||||
</p>
|
||||
<p className="mt-1 text-sm text-muted-foreground">
|
||||
{filter
|
||||
? `No blocks match "${filter}"`
|
||||
: "No blocks available"}
|
||||
</p>
|
||||
{filter && (
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() => setFilter("")}
|
||||
className="mt-4"
|
||||
>
|
||||
Clear filter
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Block cards */}
|
||||
{!isLoadingBlocks &&
|
||||
filteredBlocks.map((blockName) => {
|
||||
const block = Index[blockName]
|
||||
return (
|
||||
<a
|
||||
key={blockName}
|
||||
href={`/view/${blockName}`}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="group"
|
||||
>
|
||||
<Card className="overflow-hidden rounded-lg border-2 border-border shadow-sm transition-all duration-300 hover:scale-[1.02] hover:border-primary hover:shadow-lg hover:bg-accent/5">
|
||||
{/* Preview with loading states */}
|
||||
<BlockPreview blockName={blockName} />
|
||||
|
||||
{/* Block info - hidden until hover */}
|
||||
<div className="border-t bg-card p-4 opacity-0 group-hover:opacity-100 transition-opacity duration-300">
|
||||
<div className="mb-2 flex items-start justify-between gap-2">
|
||||
<h3 className="text-base font-semibold leading-tight">
|
||||
{blockName}
|
||||
</h3>
|
||||
{block?.category && block?.category !== "undefined" && (
|
||||
<Badge
|
||||
variant="outline"
|
||||
className="shrink-0 text-xs font-medium"
|
||||
>
|
||||
{block.category}
|
||||
</Badge>
|
||||
)}
|
||||
</div>
|
||||
{block?.subcategory &&
|
||||
block?.subcategory !== "undefined" && (
|
||||
<div className="mb-2">
|
||||
<Badge
|
||||
variant="secondary"
|
||||
className="text-xs font-normal"
|
||||
>
|
||||
{block.subcategory}
|
||||
</Badge>
|
||||
</div>
|
||||
)}
|
||||
{block?.description && (
|
||||
<p className="mt-2 text-sm text-muted-foreground line-clamp-2">
|
||||
{block.description}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
</Card>
|
||||
</a>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
</ScrollArea>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -1,6 +1,8 @@
|
||||
import { Suspense } from "react"
|
||||
import Link from "next/link"
|
||||
|
||||
import { BlockDisplay } from "@/components/block-display"
|
||||
import { SkeletonCardGrid } from "@/components/skeleton-card"
|
||||
import { Button } from "@/registry/default/ui/button"
|
||||
|
||||
export const dynamic = "force-static"
|
||||
@@ -18,9 +20,11 @@ const FEATURED_BLOCKS = [
|
||||
export default function BlocksPage() {
|
||||
return (
|
||||
<div className="flex flex-col gap-12 md:gap-24">
|
||||
{FEATURED_BLOCKS.map((name) => (
|
||||
<BlockDisplay name={name} key={name} />
|
||||
))}
|
||||
<Suspense fallback={<SkeletonCardGrid count={5} />}>
|
||||
{FEATURED_BLOCKS.map((name) => (
|
||||
<BlockDisplay name={name} key={name} />
|
||||
))}
|
||||
</Suspense>
|
||||
<div className="container-wrapper">
|
||||
<div className="container flex justify-center py-6">
|
||||
<Button asChild variant="outline">
|
||||
|
||||
@@ -16,7 +16,24 @@ import {
|
||||
verticalListSortingStrategy,
|
||||
} from "@dnd-kit/sortable"
|
||||
import { CSS } from "@dnd-kit/utilities"
|
||||
import { Download, GripVertical, Plus, Settings2, Trash2 } from "lucide-react"
|
||||
import {
|
||||
Copy as CopyIcon,
|
||||
Download,
|
||||
Eye,
|
||||
GripVertical,
|
||||
Maximize2,
|
||||
Minimize2,
|
||||
Monitor,
|
||||
PanelLeftClose,
|
||||
PanelLeftOpen,
|
||||
PanelRightClose,
|
||||
PanelRightOpen,
|
||||
Plus,
|
||||
Settings2,
|
||||
Smartphone,
|
||||
Tablet,
|
||||
Trash2,
|
||||
} from "lucide-react"
|
||||
|
||||
import { BuilderPreview } from "@/components/builder-preview"
|
||||
import { OpenInHButton } from "@/components/open-in-h-button"
|
||||
@@ -45,11 +62,12 @@ interface PageItem {
|
||||
type: "block" | "component" | "container"
|
||||
containerType?: "div" | "section" | "article"
|
||||
layoutType?: "flex" | "grid" | "stack"
|
||||
gridClass?: string
|
||||
children?: PageItem[]
|
||||
props?: Record<string, any>
|
||||
}
|
||||
|
||||
export default function EnhancedBuilder() {
|
||||
export function EnhancedBuilderClient() {
|
||||
const [blocks, setBlocks] = React.useState<string[]>([])
|
||||
const [components, setComponents] = React.useState<string[]>([])
|
||||
const [pageItems, setPageItems] = React.useState<PageItem[]>([])
|
||||
@@ -60,18 +78,19 @@ export default function EnhancedBuilder() {
|
||||
"desktop" | "tablet" | "mobile"
|
||||
>("desktop")
|
||||
const [selectedItem, setSelectedItem] = React.useState<string | null>(null)
|
||||
const [isFullscreen, setIsFullscreen] = React.useState(false)
|
||||
|
||||
React.useEffect(() => {
|
||||
// Get blocks from registry
|
||||
const blockIds = Object.keys(Index.default || {}).filter((key) => {
|
||||
const item = Index.default[key]
|
||||
const blockIds = Object.keys(Index).filter((key) => {
|
||||
const item = Index[key]
|
||||
return item?.type === "components:block"
|
||||
})
|
||||
setBlocks(blockIds.sort())
|
||||
|
||||
// Get UI components from registry
|
||||
const componentIds = Object.keys(Index.default || {}).filter((key) => {
|
||||
const item = Index.default[key]
|
||||
const componentIds = Object.keys(Index).filter((key) => {
|
||||
const item = Index[key]
|
||||
return item?.type === "components:ui"
|
||||
})
|
||||
setComponents(componentIds.sort())
|
||||
@@ -175,14 +194,19 @@ export default function EnhancedBuilder() {
|
||||
)
|
||||
}, [filteredComponents])
|
||||
|
||||
const addItem = (name: string, type: "block" | "component" | "container") => {
|
||||
const addItem = (
|
||||
name: string,
|
||||
type: "block" | "component" | "container",
|
||||
gridClass?: string
|
||||
) => {
|
||||
const newItem: PageItem = {
|
||||
id: crypto.randomUUID(),
|
||||
name,
|
||||
type,
|
||||
children: type === "container" ? [] : undefined,
|
||||
containerType: type === "container" ? "div" : undefined,
|
||||
layoutType: type === "container" ? "flex" : undefined,
|
||||
layoutType: type === "container" ? "grid" : undefined,
|
||||
gridClass: gridClass || undefined,
|
||||
}
|
||||
setPageItems([...pageItems, newItem])
|
||||
}
|
||||
@@ -281,11 +305,12 @@ export default function EnhancedBuilder() {
|
||||
} else if (item.type === "container") {
|
||||
const Tag = item.containerType || "div"
|
||||
const layoutClass =
|
||||
item.layoutType === "flex"
|
||||
item.gridClass ||
|
||||
(item.layoutType === "flex"
|
||||
? "flex flex-col gap-4"
|
||||
: item.layoutType === "grid"
|
||||
? "grid grid-cols-1 md:grid-cols-2 gap-4"
|
||||
: "space-y-4"
|
||||
: "space-y-4")
|
||||
|
||||
const customClass = item.props?.className || ""
|
||||
const fullClassName = [layoutClass, customClass]
|
||||
@@ -403,89 +428,143 @@ ${renderItems(pageItems, 3)}
|
||||
|
||||
const selectedItemData = pageItems.find((item) => item.id === selectedItem)
|
||||
|
||||
const layoutTemplates = [
|
||||
{ name: "Single Column", grid: "grid grid-cols-1 gap-1", icon: "▭" },
|
||||
{ name: "Two Columns", grid: "grid grid-cols-2 gap-1", icon: "▭▭" },
|
||||
{ name: "Three Columns", grid: "grid grid-cols-3 gap-1", icon: "▭▭▭" },
|
||||
{
|
||||
name: "Sidebar Left",
|
||||
grid: "grid grid-cols-[250px_1fr] gap-1",
|
||||
icon: "▌▭",
|
||||
},
|
||||
{
|
||||
name: "Sidebar Right",
|
||||
grid: "grid grid-cols-[1fr_250px] gap-1",
|
||||
icon: "▭▐",
|
||||
},
|
||||
{ name: "Hero + 2 Col", grid: "grid grid-cols-1 gap-0", icon: "▬" },
|
||||
]
|
||||
|
||||
return (
|
||||
<div className="flex h-screen max-h-screen gap-4 p-6">
|
||||
<div className="flex h-screen max-h-screen gap-1 p-2">
|
||||
{/* Left Sidebar - Component/Block Library */}
|
||||
<div className="w-80 space-y-4">
|
||||
<div className="w-64 space-y-1">
|
||||
<div>
|
||||
<h2 className="text-lg font-semibold">Component Library</h2>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
<h2 className="text-sm font-semibold">Component Library</h2>
|
||||
<p className="text-[10px] text-muted-foreground">
|
||||
Add blocks, components, and layouts
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<Tabs value={activeTab} onValueChange={setActiveTab}>
|
||||
<TabsList className="grid w-full grid-cols-2">
|
||||
<TabsTrigger value="blocks">Blocks</TabsTrigger>
|
||||
<TabsTrigger value="components">Components</TabsTrigger>
|
||||
<TabsList className="grid w-full grid-cols-3 h-7">
|
||||
<TabsTrigger value="blocks" className="text-[10px]">
|
||||
Blocks
|
||||
</TabsTrigger>
|
||||
<TabsTrigger value="components" className="text-[10px]">
|
||||
Components
|
||||
</TabsTrigger>
|
||||
<TabsTrigger value="layouts" className="text-[10px]">
|
||||
Layouts
|
||||
</TabsTrigger>
|
||||
</TabsList>
|
||||
|
||||
<div className="mt-4 space-y-4">
|
||||
<div className="mt-1 space-y-1">
|
||||
<Input
|
||||
placeholder={`Filter ${activeTab}...`}
|
||||
value={filter}
|
||||
onChange={(e) => setFilter(e.target.value)}
|
||||
className="h-7 text-xs"
|
||||
/>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() => addItem("container", "container")}
|
||||
className="w-full"
|
||||
>
|
||||
<Plus className="mr-2 h-4 w-4" />
|
||||
Add Container
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<ScrollArea className="h-[calc(100vh-320px)]">
|
||||
<TabsContent value="blocks" className="mt-0 space-y-4">
|
||||
<ScrollArea className="h-[calc(100vh-160px)]">
|
||||
<TabsContent value="blocks" className="mt-0 space-y-1 pr-2">
|
||||
{filteredBlocks.map((block) => (
|
||||
<Card
|
||||
key={block}
|
||||
className="cursor-pointer overflow-hidden transition-colors hover:bg-muted"
|
||||
className="group cursor-pointer overflow-hidden border transition-all hover:border-primary/50 hover:shadow-sm"
|
||||
onClick={() => addItem(block, "block")}
|
||||
>
|
||||
<div className="relative h-32 overflow-hidden bg-muted/50">
|
||||
<div className="pointer-events-none">
|
||||
<BuilderPreview
|
||||
name={block}
|
||||
type="block"
|
||||
scale={0.25}
|
||||
/>
|
||||
</div>
|
||||
<div className="absolute inset-0 flex items-center justify-center bg-background/0 opacity-0 transition-opacity hover:bg-background/80 hover:opacity-100">
|
||||
<div className="flex items-center gap-2">
|
||||
<Plus className="h-5 w-5" />
|
||||
<span className="text-sm font-medium">
|
||||
Add to page
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="border-b px-2 py-1 bg-muted/30">
|
||||
<p className="truncate text-[10px] font-medium">
|
||||
{block}
|
||||
</p>
|
||||
</div>
|
||||
<div className="border-t p-2">
|
||||
<p className="truncate text-xs font-medium">{block}</p>
|
||||
<div className="relative h-16 overflow-hidden bg-background flex items-center justify-center">
|
||||
<div className="text-[10px] text-muted-foreground">
|
||||
{block} preview
|
||||
</div>
|
||||
<div className="absolute inset-0 flex items-center justify-center bg-primary/0 opacity-0 transition-all group-hover:bg-primary/10 group-hover:opacity-100">
|
||||
<Plus className="h-3.5 w-3.5" />
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
))}
|
||||
</TabsContent>
|
||||
|
||||
<TabsContent value="components" className="mt-0 space-y-2">
|
||||
{filteredComponents.map((component) => (
|
||||
<TabsContent value="components" className="mt-0 pr-2">
|
||||
<div className="columns-1 gap-1 space-y-1">
|
||||
{filteredComponents.map((component) => {
|
||||
const isFullWidth =
|
||||
component.includes("nav") ||
|
||||
component.includes("header") ||
|
||||
component.includes("footer") ||
|
||||
component.includes("bar")
|
||||
const isSmall =
|
||||
component.includes("button") ||
|
||||
component.includes("badge") ||
|
||||
component.includes("avatar") ||
|
||||
component.includes("switch") ||
|
||||
component.includes("checkbox")
|
||||
|
||||
return (
|
||||
<Card
|
||||
key={component}
|
||||
className="group cursor-pointer break-inside-avoid overflow-hidden border transition-all hover:border-primary/50 hover:shadow-sm"
|
||||
onClick={() => addItem(component, "component")}
|
||||
>
|
||||
<div className="border-b px-2 py-1 bg-muted/30">
|
||||
<p className="truncate text-[10px] font-medium">
|
||||
{component}
|
||||
</p>
|
||||
</div>
|
||||
<div
|
||||
className={`relative overflow-hidden bg-background flex items-center justify-center ${isFullWidth ? "h-14" : isSmall ? "h-10" : "h-12"}`}
|
||||
>
|
||||
<div className="text-[10px] text-muted-foreground">
|
||||
{component}
|
||||
</div>
|
||||
<div className="absolute inset-0 flex items-center justify-center bg-primary/0 opacity-0 transition-all group-hover:bg-primary/10 group-hover:opacity-100">
|
||||
<Plus className="h-3 w-3" />
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
</TabsContent>
|
||||
|
||||
<TabsContent value="layouts" className="mt-0 space-y-1 pr-2">
|
||||
{layoutTemplates.map((layout) => (
|
||||
<Card
|
||||
key={component}
|
||||
className="cursor-pointer p-3 transition-colors hover:bg-muted"
|
||||
onClick={() => addItem(component, "component")}
|
||||
key={layout.name}
|
||||
className="group cursor-pointer overflow-hidden border transition-all hover:border-primary/50 hover:shadow-sm"
|
||||
onClick={() =>
|
||||
addItem(layout.name, "container", layout.grid)
|
||||
}
|
||||
>
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<p className="text-sm font-medium">{component}</p>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
UI Component
|
||||
</p>
|
||||
<div className="border-b px-2 py-1 bg-muted/30">
|
||||
<p className="truncate text-[10px] font-medium">
|
||||
{layout.name}
|
||||
</p>
|
||||
</div>
|
||||
<div className="relative h-16 overflow-hidden bg-background flex items-center justify-center">
|
||||
<div className="text-2xl text-muted-foreground/50">
|
||||
{layout.icon}
|
||||
</div>
|
||||
<div className="absolute inset-0 flex items-center justify-center bg-primary/0 opacity-0 transition-all group-hover:bg-primary/10 group-hover:opacity-100">
|
||||
<Plus className="h-3.5 w-3.5" />
|
||||
</div>
|
||||
<Plus className="h-4 w-4 text-muted-foreground" />
|
||||
</div>
|
||||
</Card>
|
||||
))}
|
||||
@@ -498,83 +577,94 @@ ${renderItems(pageItems, 3)}
|
||||
<Separator orientation="vertical" />
|
||||
|
||||
{/* Center - Page Builder Canvas */}
|
||||
<div className="flex-1 space-y-4">
|
||||
<div className="flex-1 space-y-1">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<h2 className="text-lg font-semibold">Page Builder</h2>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
<h2 className="text-sm font-semibold">Page Builder</h2>
|
||||
<p className="text-[10px] text-muted-foreground">
|
||||
{pageItems.length} items in page
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="flex items-center gap-1">
|
||||
{/* Viewport Controls */}
|
||||
<div className="flex rounded-lg border">
|
||||
<div className="flex rounded-md border h-7">
|
||||
<Button
|
||||
variant={viewport === "mobile" ? "default" : "ghost"}
|
||||
size="sm"
|
||||
size="icon"
|
||||
onClick={() => setViewport("mobile")}
|
||||
className="rounded-r-none"
|
||||
className="h-6 w-6 rounded-r-none"
|
||||
title="Mobile (375px)"
|
||||
>
|
||||
Mobile
|
||||
<Smartphone className="h-3.5 w-3.5" />
|
||||
</Button>
|
||||
<Button
|
||||
variant={viewport === "tablet" ? "default" : "ghost"}
|
||||
size="sm"
|
||||
size="icon"
|
||||
onClick={() => setViewport("tablet")}
|
||||
className="rounded-none border-x"
|
||||
className="h-6 w-6 rounded-none border-x"
|
||||
title="Tablet (768px)"
|
||||
>
|
||||
Tablet
|
||||
<Tablet className="h-3.5 w-3.5" />
|
||||
</Button>
|
||||
<Button
|
||||
variant={viewport === "desktop" ? "default" : "ghost"}
|
||||
size="sm"
|
||||
size="icon"
|
||||
onClick={() => setViewport("desktop")}
|
||||
className="rounded-l-none"
|
||||
className="h-6 w-6 rounded-l-none"
|
||||
title="Desktop (100%)"
|
||||
>
|
||||
Desktop
|
||||
<Monitor className="h-3.5 w-3.5" />
|
||||
</Button>
|
||||
</div>
|
||||
<Separator orientation="vertical" className="h-8" />
|
||||
<OpenInHButton name="builder" />
|
||||
<Separator orientation="vertical" className="h-8" />
|
||||
<div className="flex gap-2">
|
||||
<Button
|
||||
variant="outline"
|
||||
size="icon"
|
||||
onClick={() => setIsFullscreen(!isFullscreen)}
|
||||
title={isFullscreen ? "Exit fullscreen" : "Enter fullscreen"}
|
||||
className="h-7 w-7"
|
||||
>
|
||||
{isFullscreen ? (
|
||||
<Minimize2 className="h-3.5 w-3.5" />
|
||||
) : (
|
||||
<Maximize2 className="h-3.5 w-3.5" />
|
||||
)}
|
||||
</Button>
|
||||
<Separator orientation="vertical" className="h-6" />
|
||||
<div className="flex gap-1">
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
size="icon"
|
||||
onClick={copyCode}
|
||||
disabled={pageItems.length === 0}
|
||||
title="Copy code"
|
||||
className="h-7 w-7"
|
||||
>
|
||||
Copy Code
|
||||
<CopyIcon className="h-3 w-3" />
|
||||
</Button>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
size="icon"
|
||||
onClick={downloadCode}
|
||||
disabled={pageItems.length === 0}
|
||||
title="Download"
|
||||
className="h-7 w-7"
|
||||
>
|
||||
<Download className="mr-2 h-4 w-4" />
|
||||
Download
|
||||
</Button>
|
||||
<Button
|
||||
variant="default"
|
||||
size="sm"
|
||||
onClick={deployWithHanzo}
|
||||
disabled={pageItems.length === 0}
|
||||
>
|
||||
Deploy with Hanzo
|
||||
<Download className="h-3 w-3" />
|
||||
</Button>
|
||||
</div>
|
||||
<OpenInHButton name="builder" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<ScrollArea className="h-[calc(100vh-140px)] rounded-lg border bg-background">
|
||||
<div className="flex min-h-full items-start justify-center p-4">
|
||||
<ScrollArea className="h-[calc(100vh-160px)] rounded-lg border bg-background">
|
||||
<div className="flex min-h-full items-start justify-center px-16 py-4">
|
||||
<div
|
||||
style={{
|
||||
width: viewportWidths[viewport],
|
||||
maxWidth: "100%",
|
||||
transition: "width 0.3s ease",
|
||||
}}
|
||||
className="relative"
|
||||
>
|
||||
<DndContext
|
||||
collisionDetection={closestCenter}
|
||||
@@ -585,14 +675,14 @@ ${renderItems(pageItems, 3)}
|
||||
items={pageItems.map((item) => item.id)}
|
||||
strategy={verticalListSortingStrategy}
|
||||
>
|
||||
<div className="relative min-h-[600px] bg-background pl-16">
|
||||
<div className="relative min-h-[600px] bg-background">
|
||||
{pageItems.length === 0 ? (
|
||||
<div className="flex h-96 items-center justify-center rounded-lg border border-dashed text-center -ml-16">
|
||||
<div className="space-y-2">
|
||||
<p className="text-sm text-muted-foreground">
|
||||
<div className="flex h-96 items-center justify-center rounded-lg border border-dashed text-center">
|
||||
<div className="space-y-1">
|
||||
<p className="text-xs text-muted-foreground">
|
||||
Your page is empty
|
||||
</p>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
<p className="text-[10px] text-muted-foreground">
|
||||
Add blocks, components, or containers from the left
|
||||
</p>
|
||||
</div>
|
||||
@@ -630,11 +720,11 @@ ${renderItems(pageItems, 3)}
|
||||
{selectedItemData && (
|
||||
<>
|
||||
<Separator orientation="vertical" />
|
||||
<div className="w-80 space-y-4">
|
||||
<div className="w-64 space-y-1">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<h2 className="text-lg font-semibold">Properties</h2>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
<h2 className="text-sm font-semibold">Properties</h2>
|
||||
<p className="text-[10px] text-muted-foreground">
|
||||
{selectedItemData.name}
|
||||
</p>
|
||||
</div>
|
||||
@@ -642,43 +732,44 @@ ${renderItems(pageItems, 3)}
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={() => setSelectedItem(null)}
|
||||
className="h-7 text-[10px]"
|
||||
>
|
||||
Close
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<ScrollArea className="h-[calc(100vh-120px)]">
|
||||
<div className="space-y-6 pr-4">
|
||||
<ScrollArea className="h-[calc(100vh-160px)]">
|
||||
<div className="space-y-1 pr-2">
|
||||
{/* Basic Settings */}
|
||||
<div className="space-y-3">
|
||||
<h3 className="text-sm font-semibold">Basic Settings</h3>
|
||||
<div className="space-y-2">
|
||||
<div className="space-y-1">
|
||||
<h3 className="text-xs font-semibold">Basic Settings</h3>
|
||||
<div className="space-y-1">
|
||||
<div>
|
||||
<label className="text-xs font-medium text-muted-foreground">
|
||||
<label className="text-[10px] font-medium text-muted-foreground">
|
||||
Type
|
||||
</label>
|
||||
<p className="text-sm capitalize">
|
||||
<p className="text-xs capitalize">
|
||||
{selectedItemData.type}
|
||||
</p>
|
||||
</div>
|
||||
<div>
|
||||
<label className="text-xs font-medium text-muted-foreground">
|
||||
<label className="text-[10px] font-medium text-muted-foreground">
|
||||
Name
|
||||
</label>
|
||||
<p className="text-sm">{selectedItemData.name}</p>
|
||||
<p className="text-xs">{selectedItemData.name}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Container Settings */}
|
||||
{selectedItemData.type === "container" && (
|
||||
<div className="space-y-3">
|
||||
<h3 className="text-sm font-semibold">
|
||||
<div className="space-y-1">
|
||||
<h3 className="text-xs font-semibold">
|
||||
Container Settings
|
||||
</h3>
|
||||
<div className="space-y-3">
|
||||
<div className="space-y-2">
|
||||
<label className="text-xs font-medium text-muted-foreground">
|
||||
<div className="space-y-1">
|
||||
<div className="space-y-1">
|
||||
<label className="text-[10px] font-medium text-muted-foreground">
|
||||
HTML Tag
|
||||
</label>
|
||||
<Select
|
||||
@@ -692,7 +783,7 @@ ${renderItems(pageItems, 3)}
|
||||
})
|
||||
}
|
||||
>
|
||||
<SelectTrigger>
|
||||
<SelectTrigger className="h-7 text-xs">
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
@@ -703,8 +794,8 @@ ${renderItems(pageItems, 3)}
|
||||
</Select>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<label className="text-xs font-medium text-muted-foreground">
|
||||
<div className="space-y-1">
|
||||
<label className="text-[10px] font-medium text-muted-foreground">
|
||||
Layout Type
|
||||
</label>
|
||||
<Select
|
||||
@@ -715,7 +806,7 @@ ${renderItems(pageItems, 3)}
|
||||
})
|
||||
}
|
||||
>
|
||||
<SelectTrigger>
|
||||
<SelectTrigger className="h-7 text-xs">
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
@@ -732,10 +823,10 @@ ${renderItems(pageItems, 3)}
|
||||
)}
|
||||
|
||||
{/* Styling */}
|
||||
<div className="space-y-3">
|
||||
<h3 className="text-sm font-semibold">Styling</h3>
|
||||
<div className="space-y-2">
|
||||
<label className="text-xs font-medium text-muted-foreground">
|
||||
<div className="space-y-1">
|
||||
<h3 className="text-xs font-semibold">Styling</h3>
|
||||
<div className="space-y-1">
|
||||
<label className="text-[10px] font-medium text-muted-foreground">
|
||||
Custom Classes
|
||||
</label>
|
||||
<Input
|
||||
@@ -746,74 +837,23 @@ ${renderItems(pageItems, 3)}
|
||||
className: e.target.value,
|
||||
})
|
||||
}
|
||||
className="h-7 text-xs"
|
||||
/>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
<p className="text-[10px] text-muted-foreground">
|
||||
Add Tailwind CSS classes
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Container Children */}
|
||||
{selectedItemData.type === "container" && (
|
||||
<div className="space-y-3">
|
||||
<h3 className="text-sm font-semibold">
|
||||
Container Children (
|
||||
{selectedItemData.children?.length || 0})
|
||||
</h3>
|
||||
{selectedItemData.children &&
|
||||
selectedItemData.children.length > 0 ? (
|
||||
<div className="space-y-2">
|
||||
{selectedItemData.children.map((child, index) => (
|
||||
<div
|
||||
key={child.id}
|
||||
className="flex items-center justify-between rounded-md border bg-card p-2"
|
||||
>
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-xs text-muted-foreground">
|
||||
#{index + 1}
|
||||
</span>
|
||||
<div>
|
||||
<p className="text-sm font-medium">
|
||||
{child.name}
|
||||
</p>
|
||||
<p className="text-xs text-muted-foreground capitalize">
|
||||
{child.type}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={() =>
|
||||
removeChildFromContainer(
|
||||
selectedItemData.id,
|
||||
child.id
|
||||
)
|
||||
}
|
||||
>
|
||||
<Trash2 className="h-3 w-3" />
|
||||
</Button>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
) : (
|
||||
<p className="text-xs text-muted-foreground">
|
||||
No children yet. Children can be added in a future
|
||||
update.
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Component Props */}
|
||||
{selectedItemData.type === "component" && (
|
||||
<div className="space-y-3">
|
||||
<h3 className="text-sm font-semibold">
|
||||
<div className="space-y-1">
|
||||
<h3 className="text-xs font-semibold">
|
||||
Component Properties
|
||||
</h3>
|
||||
<div className="space-y-3">
|
||||
<div className="space-y-2">
|
||||
<label className="text-xs font-medium text-muted-foreground">
|
||||
<div className="space-y-1">
|
||||
<div className="space-y-1">
|
||||
<label className="text-[10px] font-medium text-muted-foreground">
|
||||
Variant
|
||||
</label>
|
||||
<Input
|
||||
@@ -824,10 +864,11 @@ ${renderItems(pageItems, 3)}
|
||||
variant: e.target.value,
|
||||
})
|
||||
}
|
||||
className="h-7 text-xs"
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<label className="text-xs font-medium text-muted-foreground">
|
||||
<div className="space-y-1">
|
||||
<label className="text-[10px] font-medium text-muted-foreground">
|
||||
Size
|
||||
</label>
|
||||
<Input
|
||||
@@ -838,6 +879,7 @@ ${renderItems(pageItems, 3)}
|
||||
size: e.target.value,
|
||||
})
|
||||
}
|
||||
className="h-7 text-xs"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
@@ -845,13 +887,13 @@ ${renderItems(pageItems, 3)}
|
||||
)}
|
||||
|
||||
{/* Advanced */}
|
||||
<div className="space-y-3">
|
||||
<h3 className="text-sm font-semibold">Advanced</h3>
|
||||
<div className="space-y-2">
|
||||
<div className="space-y-1">
|
||||
<h3 className="text-xs font-semibold">Advanced</h3>
|
||||
<div className="space-y-1">
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
className="w-full"
|
||||
className="w-full h-7 text-[10px]"
|
||||
onClick={() => {
|
||||
const code = JSON.stringify(selectedItemData, null, 2)
|
||||
navigator.clipboard.writeText(code)
|
||||
@@ -862,22 +904,22 @@ ${renderItems(pageItems, 3)}
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
className="w-full text-destructive hover:text-destructive"
|
||||
className="w-full h-7 text-[10px] text-destructive hover:text-destructive"
|
||||
onClick={() => {
|
||||
removeItem(selectedItemData.id)
|
||||
setSelectedItem(null)
|
||||
}}
|
||||
>
|
||||
<Trash2 className="mr-2 h-4 w-4" />
|
||||
<Trash2 className="mr-1 h-3 w-3" />
|
||||
Delete Item
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Component Info */}
|
||||
<div className="rounded-lg border bg-muted/50 p-3 text-xs">
|
||||
<div className="rounded-lg border bg-muted/50 p-2 text-[10px]">
|
||||
<p className="font-medium">About this item:</p>
|
||||
<ul className="mt-2 space-y-1 text-muted-foreground">
|
||||
<ul className="mt-1 space-y-0.5 text-muted-foreground">
|
||||
<li>• ID: {selectedItemData.id.slice(0, 8)}...</li>
|
||||
<li>• Type: {selectedItemData.type}</li>
|
||||
{selectedItemData.children && (
|
||||
@@ -924,14 +966,14 @@ function SortableItem({
|
||||
<div
|
||||
ref={setNodeRef}
|
||||
style={style}
|
||||
className={`group relative ${isSelected ? "ring-2 ring-primary" : ""}`}
|
||||
className="group relative"
|
||||
onClick={onSelect}
|
||||
>
|
||||
<div className="absolute -left-12 top-2 z-10 flex flex-col items-center gap-2">
|
||||
<div className="absolute -left-14 top-2 z-10 flex flex-col items-center gap-1 opacity-0 transition-opacity group-hover:opacity-100">
|
||||
<button
|
||||
{...attributes}
|
||||
{...listeners}
|
||||
className="cursor-grab rounded bg-card p-1 shadow-sm hover:shadow active:cursor-grabbing"
|
||||
className="cursor-grab rounded bg-card p-1 shadow-sm hover:shadow backdrop-blur active:cursor-grabbing"
|
||||
>
|
||||
<GripVertical className="h-4 w-4 text-muted-foreground" />
|
||||
</button>
|
||||
@@ -942,47 +984,43 @@ function SortableItem({
|
||||
e.stopPropagation()
|
||||
onRemove()
|
||||
}}
|
||||
className="h-6 w-6 bg-background/80 opacity-0 backdrop-blur transition-opacity group-hover:opacity-100"
|
||||
className="h-6 w-6 bg-destructive/10 hover:bg-destructive/20 backdrop-blur"
|
||||
>
|
||||
<Trash2 className="h-3 w-3" />
|
||||
<Trash2 className="h-3 w-3 text-destructive" />
|
||||
</Button>
|
||||
{isSelected && (
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="h-6 w-6 bg-background/80 backdrop-blur"
|
||||
>
|
||||
<Settings2 className="h-3 w-3" />
|
||||
</Button>
|
||||
<div className="h-6 w-6 flex items-center justify-center rounded bg-primary/20 backdrop-blur">
|
||||
<Settings2 className="h-3 w-3 text-primary" />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="relative overflow-hidden border-b last:border-b-0">
|
||||
<div className="relative overflow-hidden">
|
||||
{item.type === "container" ? (
|
||||
<div className="min-h-[100px] bg-muted/20 p-4">
|
||||
<div className="mb-2 flex items-center gap-2 border-b border-dashed pb-2">
|
||||
<p className="text-xs font-medium text-muted-foreground uppercase tracking-wide">
|
||||
Container ({item.containerType || "div"}) -{" "}
|
||||
{item.layoutType || "flex"}
|
||||
<div className="min-h-[100px] bg-muted/10 p-2">
|
||||
<div className="mb-1 flex items-center gap-1 opacity-0 group-hover:opacity-100 transition-opacity">
|
||||
<p className="text-[10px] font-medium text-muted-foreground uppercase tracking-wide">
|
||||
{item.name}
|
||||
</p>
|
||||
</div>
|
||||
{item.children && item.children.length > 0 ? (
|
||||
<div
|
||||
className={
|
||||
item.layoutType === "flex"
|
||||
? "flex flex-col gap-4"
|
||||
item.gridClass ||
|
||||
(item.layoutType === "flex"
|
||||
? "flex flex-col gap-1"
|
||||
: item.layoutType === "grid"
|
||||
? "grid grid-cols-1 md:grid-cols-2 gap-4"
|
||||
: "space-y-4"
|
||||
? "grid grid-cols-1 md:grid-cols-2 gap-1"
|
||||
: "space-y-1")
|
||||
}
|
||||
>
|
||||
{item.children.map((child) => (
|
||||
<div
|
||||
key={child.id}
|
||||
className="border-l-2 border-primary/50 pl-3"
|
||||
className="border-l border-primary/30 pl-1"
|
||||
>
|
||||
{child.type === "container" ? (
|
||||
<div className="text-xs text-muted-foreground">
|
||||
<div className="text-[10px] text-muted-foreground">
|
||||
Nested Container: {child.name}
|
||||
</div>
|
||||
) : (
|
||||
@@ -997,8 +1035,8 @@ function SortableItem({
|
||||
</div>
|
||||
) : (
|
||||
<div className="flex min-h-[80px] items-center justify-center">
|
||||
<p className="text-xs text-muted-foreground">
|
||||
Empty container - Add items from the left sidebar
|
||||
<p className="text-[10px] text-muted-foreground">
|
||||
Empty container
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
+80
-158
@@ -16,19 +16,12 @@ import {
|
||||
verticalListSortingStrategy,
|
||||
} from "@dnd-kit/sortable"
|
||||
import { CSS } from "@dnd-kit/utilities"
|
||||
import { ChevronLeft, ChevronRight, Copy, Download, Eye, GripVertical, Maximize2, Minimize2, Monitor, Palette, PanelLeftClose, PanelLeftOpen, PanelRightClose, PanelRightOpen, Plus, Settings2, Smartphone, Tablet, Trash2 } from "lucide-react"
|
||||
import { Download, GripVertical, Plus, Settings2, Trash2 } from "lucide-react"
|
||||
|
||||
import { BuilderPreview } from "@/components/builder-preview"
|
||||
import { OpenInHButton } from "@/components/open-in-h-button"
|
||||
import { Button } from "@/registry/default/ui/button"
|
||||
import { Card } from "@/registry/default/ui/card"
|
||||
import {
|
||||
ContextMenu,
|
||||
ContextMenuContent,
|
||||
ContextMenuItem,
|
||||
ContextMenuSeparator,
|
||||
ContextMenuTrigger,
|
||||
} from "@/registry/default/ui/context-menu"
|
||||
import { Input } from "@/registry/default/ui/input"
|
||||
import { ScrollArea } from "@/registry/default/ui/scroll-area"
|
||||
import {
|
||||
@@ -67,20 +60,17 @@ export default function EnhancedBuilder() {
|
||||
"desktop" | "tablet" | "mobile"
|
||||
>("desktop")
|
||||
const [selectedItem, setSelectedItem] = React.useState<string | null>(null)
|
||||
const [isFullscreen, setIsFullscreen] = React.useState(false)
|
||||
const [leftSidebarCollapsed, setLeftSidebarCollapsed] = React.useState(false)
|
||||
const [rightSidebarCollapsed, setRightSidebarCollapsed] = React.useState(false)
|
||||
|
||||
React.useEffect(() => {
|
||||
// Get blocks from registry
|
||||
const blockIds = Object.keys(Index).filter((key) => {
|
||||
const blockIds = Object.keys(Index || {}).filter((key) => {
|
||||
const item = Index[key]
|
||||
return item?.type === "components:block"
|
||||
})
|
||||
setBlocks(blockIds.sort())
|
||||
|
||||
// Get UI components from registry
|
||||
const componentIds = Object.keys(Index).filter((key) => {
|
||||
const componentIds = Object.keys(Index || {}).filter((key) => {
|
||||
const item = Index[key]
|
||||
return item?.type === "components:ui"
|
||||
})
|
||||
@@ -414,79 +404,88 @@ ${renderItems(pageItems, 3)}
|
||||
const selectedItemData = pageItems.find((item) => item.id === selectedItem)
|
||||
|
||||
return (
|
||||
<div className="flex h-screen max-h-screen">
|
||||
<div className="flex h-screen max-h-screen gap-4 p-6">
|
||||
{/* Left Sidebar - Component/Block Library */}
|
||||
<div className={`relative flex-shrink-0 transition-all duration-200 ${isFullscreen || leftSidebarCollapsed ? "w-0" : "w-72"} ${isFullscreen || leftSidebarCollapsed ? "hidden" : "border-r p-3 space-y-3"}`}>
|
||||
<div className="space-y-1">
|
||||
<h2 className="text-base font-semibold">Library</h2>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
Blocks & Components
|
||||
<div className="w-80 space-y-4">
|
||||
<div>
|
||||
<h2 className="text-lg font-semibold">Component Library</h2>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
Add blocks, components, and layouts
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<Tabs value={activeTab} onValueChange={setActiveTab}>
|
||||
<TabsList className="grid w-full grid-cols-2 h-8">
|
||||
<TabsTrigger value="blocks" className="text-xs">Blocks</TabsTrigger>
|
||||
<TabsTrigger value="components" className="text-xs">Components</TabsTrigger>
|
||||
<TabsList className="grid w-full grid-cols-2">
|
||||
<TabsTrigger value="blocks">Blocks</TabsTrigger>
|
||||
<TabsTrigger value="components">Components</TabsTrigger>
|
||||
</TabsList>
|
||||
|
||||
<div className="mt-3 space-y-2">
|
||||
<div className="mt-4 space-y-4">
|
||||
<Input
|
||||
placeholder={`Search ${activeTab}...`}
|
||||
placeholder={`Filter ${activeTab}...`}
|
||||
value={filter}
|
||||
onChange={(e) => setFilter(e.target.value)}
|
||||
className="h-8 text-xs"
|
||||
/>
|
||||
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() => addItem("container", "container")}
|
||||
className="w-full h-7 text-xs"
|
||||
>
|
||||
<Plus className="mr-1.5 h-3.5 w-3.5" />
|
||||
Container
|
||||
</Button>
|
||||
<div className="space-y-2">
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() => addItem("container", "container")}
|
||||
className="w-full"
|
||||
>
|
||||
<Plus className="mr-2 h-4 w-4" />
|
||||
Add Container
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<ScrollArea className="h-[calc(100vh-200px)]">
|
||||
<TabsContent value="blocks" className="mt-0 space-y-2">
|
||||
<ScrollArea className="h-[calc(100vh-320px)]">
|
||||
<TabsContent value="blocks" className="mt-0 space-y-4">
|
||||
{filteredBlocks.map((block) => (
|
||||
<Card
|
||||
key={block}
|
||||
className="group cursor-pointer overflow-hidden transition-all hover:border-primary/50 hover:shadow-sm"
|
||||
className="cursor-pointer overflow-hidden transition-colors hover:bg-muted"
|
||||
onClick={() => addItem(block, "block")}
|
||||
>
|
||||
<div className="relative h-24 overflow-hidden bg-muted/30">
|
||||
<div className="pointer-events-none scale-75">
|
||||
<div className="relative h-32 overflow-hidden bg-muted/50">
|
||||
<div className="pointer-events-none">
|
||||
<BuilderPreview
|
||||
name={block}
|
||||
type="block"
|
||||
scale={0.2}
|
||||
scale={0.25}
|
||||
/>
|
||||
</div>
|
||||
<div className="absolute inset-0 flex items-center justify-center bg-background/0 opacity-0 transition-opacity group-hover:bg-background/90 group-hover:opacity-100">
|
||||
<Plus className="h-4 w-4" />
|
||||
<div className="absolute inset-0 flex items-center justify-center bg-background/0 opacity-0 transition-opacity hover:bg-background/80 hover:opacity-100">
|
||||
<div className="flex items-center gap-2">
|
||||
<Plus className="h-5 w-5" />
|
||||
<span className="text-sm font-medium">
|
||||
Add to page
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="border-t px-2 py-1.5">
|
||||
<p className="truncate text-[11px] font-medium">{block}</p>
|
||||
<div className="border-t p-2">
|
||||
<p className="truncate text-xs font-medium">{block}</p>
|
||||
</div>
|
||||
</Card>
|
||||
))}
|
||||
</TabsContent>
|
||||
|
||||
<TabsContent value="components" className="mt-0 space-y-1.5">
|
||||
<TabsContent value="components" className="mt-0 space-y-2">
|
||||
{filteredComponents.map((component) => (
|
||||
<Card
|
||||
key={component}
|
||||
className="group cursor-pointer px-2.5 py-2 transition-all hover:border-primary/50 hover:shadow-sm"
|
||||
className="cursor-pointer p-3 transition-colors hover:bg-muted"
|
||||
onClick={() => addItem(component, "component")}
|
||||
>
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex-1 min-w-0">
|
||||
<p className="text-xs font-medium truncate">{component}</p>
|
||||
<div>
|
||||
<p className="text-sm font-medium">{component}</p>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
UI Component
|
||||
</p>
|
||||
</div>
|
||||
<Plus className="h-3.5 w-3.5 text-muted-foreground opacity-0 group-hover:opacity-100 transition-opacity flex-shrink-0 ml-2" />
|
||||
<Plus className="h-4 w-4 text-muted-foreground" />
|
||||
</div>
|
||||
</Card>
|
||||
))}
|
||||
@@ -496,90 +495,46 @@ ${renderItems(pageItems, 3)}
|
||||
</Tabs>
|
||||
</div>
|
||||
|
||||
<Separator orientation="vertical" />
|
||||
|
||||
{/* Center - Page Builder Canvas */}
|
||||
<div className="flex-1 flex flex-col">
|
||||
{/* Header - Hidden in fullscreen */}
|
||||
<div className={`flex items-center justify-between border-b px-3 py-2 ${isFullscreen ? "hidden" : ""}`}>
|
||||
<div className="flex items-baseline gap-2">
|
||||
<h2 className="text-sm font-semibold">Canvas</h2>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
{pageItems.length} {pageItems.length === 1 ? 'item' : 'items'}
|
||||
<div className="flex-1 space-y-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<h2 className="text-lg font-semibold">Page Builder</h2>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
{pageItems.length} items in page
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex items-center gap-1.5">
|
||||
{/* Sidebar Collapse Buttons */}
|
||||
{!isFullscreen && (
|
||||
<>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
onClick={() => setLeftSidebarCollapsed(!leftSidebarCollapsed)}
|
||||
className="h-7 w-7"
|
||||
title={leftSidebarCollapsed ? "Show library" : "Hide library"}
|
||||
>
|
||||
{leftSidebarCollapsed ? <PanelLeftOpen className="h-3.5 w-3.5" /> : <PanelLeftClose className="h-3.5 w-3.5" />}
|
||||
</Button>
|
||||
<Separator orientation="vertical" className="h-6" />
|
||||
</>
|
||||
)}
|
||||
<div className="flex items-center gap-2">
|
||||
{/* Viewport Controls */}
|
||||
<div className="flex rounded-md border h-7">
|
||||
<div className="flex rounded-lg border">
|
||||
<Button
|
||||
variant={viewport === "mobile" ? "default" : "ghost"}
|
||||
size="icon"
|
||||
size="sm"
|
||||
onClick={() => setViewport("mobile")}
|
||||
className="h-6 w-6 rounded-r-none"
|
||||
title="Mobile (375px)"
|
||||
className="rounded-r-none"
|
||||
>
|
||||
<Smartphone className="h-3.5 w-3.5" />
|
||||
Mobile
|
||||
</Button>
|
||||
<Button
|
||||
variant={viewport === "tablet" ? "default" : "ghost"}
|
||||
size="icon"
|
||||
size="sm"
|
||||
onClick={() => setViewport("tablet")}
|
||||
className="h-6 w-6 rounded-none border-x"
|
||||
title="Tablet (768px)"
|
||||
className="rounded-none border-x"
|
||||
>
|
||||
<Tablet className="h-3.5 w-3.5" />
|
||||
Tablet
|
||||
</Button>
|
||||
<Button
|
||||
variant={viewport === "desktop" ? "default" : "ghost"}
|
||||
size="icon"
|
||||
size="sm"
|
||||
onClick={() => setViewport("desktop")}
|
||||
className="h-6 w-6 rounded-l-none"
|
||||
title="Desktop (100%)"
|
||||
className="rounded-l-none"
|
||||
>
|
||||
<Monitor className="h-3.5 w-3.5" />
|
||||
Desktop
|
||||
</Button>
|
||||
</div>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="icon"
|
||||
onClick={() => setIsFullscreen(!isFullscreen)}
|
||||
title={isFullscreen ? "Exit fullscreen" : "Enter fullscreen"}
|
||||
className="h-7 w-7"
|
||||
>
|
||||
{isFullscreen ? (
|
||||
<Minimize2 className="h-3.5 w-3.5" />
|
||||
) : (
|
||||
<Maximize2 className="h-3.5 w-3.5" />
|
||||
)}
|
||||
</Button>
|
||||
<Separator orientation="vertical" className="h-6" />
|
||||
{!isFullscreen && selectedItemData && (
|
||||
<>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
onClick={() => setRightSidebarCollapsed(!rightSidebarCollapsed)}
|
||||
className="h-7 w-7"
|
||||
title={rightSidebarCollapsed ? "Show properties" : "Hide properties"}
|
||||
>
|
||||
{rightSidebarCollapsed ? <PanelRightOpen className="h-3.5 w-3.5" /> : <PanelRightClose className="h-3.5 w-3.5" />}
|
||||
</Button>
|
||||
<Separator orientation="vertical" className="h-6" />
|
||||
</>
|
||||
)}
|
||||
<Separator orientation="vertical" className="h-8" />
|
||||
<OpenInHButton name="builder" />
|
||||
<Separator orientation="vertical" className="h-8" />
|
||||
<div className="flex gap-2">
|
||||
@@ -612,8 +567,8 @@ ${renderItems(pageItems, 3)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<ScrollArea className={`flex-1 bg-background ${isFullscreen ? "" : "border-t"}`}>
|
||||
<div className="flex min-h-full items-start justify-center">
|
||||
<ScrollArea className="h-[calc(100vh-140px)] rounded-lg border bg-background">
|
||||
<div className="flex min-h-full items-start justify-center p-4">
|
||||
<div
|
||||
style={{
|
||||
width: viewportWidths[viewport],
|
||||
@@ -630,7 +585,7 @@ ${renderItems(pageItems, 3)}
|
||||
items={pageItems.map((item) => item.id)}
|
||||
strategy={verticalListSortingStrategy}
|
||||
>
|
||||
<div className="relative min-h-screen bg-background pl-16">
|
||||
<div className="relative min-h-[600px] bg-background pl-16">
|
||||
{pageItems.length === 0 ? (
|
||||
<div className="flex h-96 items-center justify-center rounded-lg border border-dashed text-center -ml-16">
|
||||
<div className="space-y-2">
|
||||
@@ -672,9 +627,10 @@ ${renderItems(pageItems, 3)}
|
||||
</div>
|
||||
|
||||
{/* Right Sidebar - Property Editor */}
|
||||
{selectedItemData && !isFullscreen && !rightSidebarCollapsed && (
|
||||
{selectedItemData && (
|
||||
<>
|
||||
<div className="relative flex-shrink-0 w-80 border-l p-3 space-y-3">
|
||||
<Separator orientation="vertical" />
|
||||
<div className="w-80 space-y-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<h2 className="text-lg font-semibold">Properties</h2>
|
||||
@@ -965,14 +921,12 @@ function SortableItem({
|
||||
}
|
||||
|
||||
return (
|
||||
<ContextMenu>
|
||||
<ContextMenuTrigger asChild>
|
||||
<div
|
||||
ref={setNodeRef}
|
||||
style={style}
|
||||
className={`group relative ${isSelected ? "ring-2 ring-primary" : ""}`}
|
||||
onClick={onSelect}
|
||||
>
|
||||
<div
|
||||
ref={setNodeRef}
|
||||
style={style}
|
||||
className={`group relative ${isSelected ? "ring-2 ring-primary" : ""}`}
|
||||
onClick={onSelect}
|
||||
>
|
||||
<div className="absolute -left-12 top-2 z-10 flex flex-col items-center gap-2">
|
||||
<button
|
||||
{...attributes}
|
||||
@@ -1058,37 +1012,5 @@ function SortableItem({
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</ContextMenuTrigger>
|
||||
<ContextMenuContent className="w-56">
|
||||
<ContextMenuItem onClick={onSelect}>
|
||||
<Eye className="mr-2 h-4 w-4" />
|
||||
Inspect Element
|
||||
</ContextMenuItem>
|
||||
<ContextMenuItem onClick={onSelect}>
|
||||
<Palette className="mr-2 h-4 w-4" />
|
||||
Edit Styles
|
||||
</ContextMenuItem>
|
||||
<ContextMenuItem
|
||||
onClick={() => {
|
||||
const code = JSON.stringify(item, null, 2)
|
||||
navigator.clipboard.writeText(code)
|
||||
}}
|
||||
>
|
||||
<Copy className="mr-2 h-4 w-4" />
|
||||
Copy JSON
|
||||
</ContextMenuItem>
|
||||
<ContextMenuSeparator />
|
||||
<ContextMenuItem
|
||||
onClick={(e) => {
|
||||
e.stopPropagation()
|
||||
onRemove()
|
||||
}}
|
||||
className="text-destructive focus:text-destructive"
|
||||
>
|
||||
<Trash2 className="mr-2 h-4 w-4" />
|
||||
Delete
|
||||
</ContextMenuItem>
|
||||
</ContextMenuContent>
|
||||
</ContextMenu>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,427 +0,0 @@
|
||||
"use client"
|
||||
|
||||
import * as React from "react"
|
||||
import dynamic from "next/dynamic"
|
||||
import { Index } from "@/__registry__"
|
||||
import {
|
||||
closestCenter,
|
||||
DndContext,
|
||||
DragEndEvent,
|
||||
DragOverlay,
|
||||
DragStartEvent,
|
||||
} from "@dnd-kit/core"
|
||||
import {
|
||||
arrayMove,
|
||||
SortableContext,
|
||||
useSortable,
|
||||
verticalListSortingStrategy,
|
||||
} from "@dnd-kit/sortable"
|
||||
import { CSS } from "@dnd-kit/utilities"
|
||||
import { Download, Eye, GripVertical, Plus, Trash2 } from "lucide-react"
|
||||
|
||||
import { Button } from "@/registry/new-york/ui/button"
|
||||
import { Card } from "@/registry/new-york/ui/card"
|
||||
import { Input } from "@/registry/new-york/ui/input"
|
||||
import { ScrollArea } from "@/registry/new-york/ui/scroll-area"
|
||||
import { Separator } from "@/registry/new-york/ui/separator"
|
||||
|
||||
// Dynamic block component loader
|
||||
const DynamicBlock = ({ blockName, scale = 1 }: { blockName: string; scale?: number }) => {
|
||||
const [BlockComponent, setBlockComponent] = React.useState<React.ComponentType | null>(null)
|
||||
const [error, setError] = React.useState(false)
|
||||
|
||||
React.useEffect(() => {
|
||||
import(`@/registry/default/block/${blockName}`)
|
||||
.then((mod) => {
|
||||
setBlockComponent(() => mod.default)
|
||||
setError(false)
|
||||
})
|
||||
.catch((err) => {
|
||||
console.error(`Failed to load block ${blockName}:`, err)
|
||||
setError(true)
|
||||
})
|
||||
}, [blockName])
|
||||
|
||||
if (error) {
|
||||
return (
|
||||
<div className="flex h-full items-center justify-center bg-muted/50 p-4 text-center">
|
||||
<p className="text-xs text-muted-foreground">Failed to load {blockName}</p>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
if (!BlockComponent) {
|
||||
return (
|
||||
<div className="flex h-full items-center justify-center bg-muted/50">
|
||||
<div className="h-4 w-4 animate-spin rounded-full border-2 border-primary border-t-transparent" />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<div style={{ transform: `scale(${scale})`, transformOrigin: "top left" }}>
|
||||
<BlockComponent />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
interface PageBlock {
|
||||
id: string
|
||||
blockName: string
|
||||
}
|
||||
|
||||
export default function PageBuilder() {
|
||||
const [blocks, setBlocks] = React.useState<string[]>([])
|
||||
const [availableBlocks, setAvailableBlocks] = React.useState<string[]>([])
|
||||
const [pageBlocks, setPageBlocks] = React.useState<PageBlock[]>([])
|
||||
const [activeId, setActiveId] = React.useState<string | null>(null)
|
||||
const [filter, setFilter] = React.useState("")
|
||||
const [viewport, setViewport] = React.useState<"desktop" | "tablet" | "mobile">("desktop")
|
||||
|
||||
React.useEffect(() => {
|
||||
// Get block IDs from the registry index
|
||||
const blockIds = Object.keys(Index.default || {}).filter((key) => {
|
||||
const item = Index.default[key]
|
||||
return item?.type === "components:block"
|
||||
})
|
||||
setBlocks(blockIds)
|
||||
setAvailableBlocks(blockIds)
|
||||
}, [])
|
||||
|
||||
const filteredBlocks = availableBlocks.filter((block) =>
|
||||
block.toLowerCase().includes(filter.toLowerCase())
|
||||
)
|
||||
|
||||
const addBlock = (blockName: string) => {
|
||||
setPageBlocks([...pageBlocks, { id: crypto.randomUUID(), blockName }])
|
||||
}
|
||||
|
||||
const removeBlock = (id: string) => {
|
||||
setPageBlocks(pageBlocks.filter((b) => b.id !== id))
|
||||
}
|
||||
|
||||
const handleDragStart = (event: DragStartEvent) => {
|
||||
setActiveId(event.active.id as string)
|
||||
}
|
||||
|
||||
const handleDragEnd = (event: DragEndEvent) => {
|
||||
const { active, over } = event
|
||||
setActiveId(null)
|
||||
|
||||
if (!over || active.id === over.id) return
|
||||
|
||||
setPageBlocks((items) => {
|
||||
const oldIndex = items.findIndex((item) => item.id === active.id)
|
||||
const newIndex = items.findIndex((item) => item.id === over.id)
|
||||
return arrayMove(items, oldIndex, newIndex)
|
||||
})
|
||||
}
|
||||
|
||||
const generatePageCode = () => {
|
||||
const imports = pageBlocks
|
||||
.map((block) => `import ${toPascalCase(block.blockName)} from "@/registry/default/block/${block.blockName}"`)
|
||||
.join("\n")
|
||||
|
||||
const components = pageBlocks
|
||||
.map((block) => ` <${toPascalCase(block.blockName)} />`)
|
||||
.join("\n")
|
||||
|
||||
return `"use client"
|
||||
|
||||
import * as React from "react"
|
||||
${imports}
|
||||
|
||||
export default function CustomPage() {
|
||||
return (
|
||||
<div className="flex min-h-screen flex-col">
|
||||
${components}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
`
|
||||
}
|
||||
|
||||
const copyCode = async () => {
|
||||
const code = generatePageCode()
|
||||
await navigator.clipboard.writeText(code)
|
||||
// TODO: Show toast notification
|
||||
}
|
||||
|
||||
const downloadCode = () => {
|
||||
const code = generatePageCode()
|
||||
const blob = new Blob([code], { type: "text/typescript" })
|
||||
const url = URL.createObjectURL(blob)
|
||||
const a = document.createElement("a")
|
||||
a.href = url
|
||||
a.download = "page.tsx"
|
||||
a.click()
|
||||
URL.revokeObjectURL(url)
|
||||
}
|
||||
|
||||
const deployWithHanzo = () => {
|
||||
// TODO: Integrate with Hanzo deployment API
|
||||
const code = generatePageCode()
|
||||
console.log("Deploying with Hanzo:", code)
|
||||
// This would call hanzo deployment service
|
||||
window.open("https://hanzo.ai/deploy", "_blank")
|
||||
}
|
||||
|
||||
const toPascalCase = (str: string) => {
|
||||
return str
|
||||
.split("-")
|
||||
.map((word) => word.charAt(0).toUpperCase() + word.slice(1))
|
||||
.join("")
|
||||
}
|
||||
|
||||
const viewportWidths = {
|
||||
desktop: "100%",
|
||||
tablet: "768px",
|
||||
mobile: "375px",
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex h-screen max-h-screen gap-4 p-6">
|
||||
{/* Left Sidebar - Block Library */}
|
||||
<div className="w-64 space-y-4">
|
||||
<div>
|
||||
<h2 className="text-lg font-semibold">Block Library</h2>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
Drag blocks to build your page
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<Input
|
||||
placeholder="Filter blocks..."
|
||||
value={filter}
|
||||
onChange={(e) => setFilter(e.target.value)}
|
||||
/>
|
||||
|
||||
<ScrollArea className="h-[calc(100vh-200px)]">
|
||||
<div className="space-y-4">
|
||||
{filteredBlocks.map((block) => (
|
||||
<Card
|
||||
key={block}
|
||||
className="cursor-grab overflow-hidden transition-colors hover:bg-muted"
|
||||
onClick={() => addBlock(block)}
|
||||
>
|
||||
{/* 1/4 Scale Block Preview */}
|
||||
<div className="relative h-32 overflow-hidden bg-muted/50">
|
||||
<div className="pointer-events-none">
|
||||
<DynamicBlock blockName={block} scale={0.25} />
|
||||
</div>
|
||||
{/* Overlay with block name and add button */}
|
||||
<div className="absolute inset-0 flex items-center justify-center bg-background/0 opacity-0 transition-opacity hover:bg-background/80 hover:opacity-100">
|
||||
<div className="flex items-center gap-2">
|
||||
<Plus className="h-5 w-5" />
|
||||
<span className="text-sm font-medium">Add to page</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="border-t p-2">
|
||||
<p className="truncate text-xs font-medium">{block}</p>
|
||||
</div>
|
||||
</Card>
|
||||
))}
|
||||
</div>
|
||||
</ScrollArea>
|
||||
</div>
|
||||
|
||||
<Separator orientation="vertical" />
|
||||
|
||||
{/* Center - Page Builder Canvas */}
|
||||
<div className="flex-1 space-y-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<h2 className="text-lg font-semibold">Page Builder</h2>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
{pageBlocks.length} blocks in page
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
{/* Viewport Controls */}
|
||||
<div className="flex rounded-lg border">
|
||||
<Button
|
||||
variant={viewport === "mobile" ? "default" : "ghost"}
|
||||
size="sm"
|
||||
onClick={() => setViewport("mobile")}
|
||||
className="rounded-r-none"
|
||||
>
|
||||
Mobile
|
||||
</Button>
|
||||
<Button
|
||||
variant={viewport === "tablet" ? "default" : "ghost"}
|
||||
size="sm"
|
||||
onClick={() => setViewport("tablet")}
|
||||
className="rounded-none border-x"
|
||||
>
|
||||
Tablet
|
||||
</Button>
|
||||
<Button
|
||||
variant={viewport === "desktop" ? "default" : "ghost"}
|
||||
size="sm"
|
||||
onClick={() => setViewport("desktop")}
|
||||
className="rounded-l-none"
|
||||
>
|
||||
Desktop
|
||||
</Button>
|
||||
</div>
|
||||
<Separator orientation="vertical" className="h-8" />
|
||||
<div className="flex gap-2">
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={copyCode}
|
||||
disabled={pageBlocks.length === 0}
|
||||
>
|
||||
<svg
|
||||
className="mr-2 h-4 w-4"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<rect width="13" height="13" x="9" y="9" rx="2" ry="2" />
|
||||
<path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1" />
|
||||
</svg>
|
||||
Copy Code
|
||||
</Button>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={downloadCode}
|
||||
disabled={pageBlocks.length === 0}
|
||||
>
|
||||
<Download className="mr-2 h-4 w-4" />
|
||||
Download
|
||||
</Button>
|
||||
<Button
|
||||
variant="default"
|
||||
size="sm"
|
||||
onClick={deployWithHanzo}
|
||||
disabled={pageBlocks.length === 0}
|
||||
>
|
||||
<svg
|
||||
className="mr-2 h-4 w-4"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path d="M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10z" />
|
||||
</svg>
|
||||
Deploy with Hanzo
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<ScrollArea className="h-[calc(100vh-140px)] rounded-lg border bg-background">
|
||||
<div className="flex min-h-full items-start justify-center p-4">
|
||||
<div
|
||||
style={{
|
||||
width: viewportWidths[viewport],
|
||||
maxWidth: "100%",
|
||||
transition: "width 0.3s ease",
|
||||
}}
|
||||
>
|
||||
<DndContext
|
||||
collisionDetection={closestCenter}
|
||||
onDragStart={handleDragStart}
|
||||
onDragEnd={handleDragEnd}
|
||||
>
|
||||
<SortableContext
|
||||
items={pageBlocks.map((b) => b.id)}
|
||||
strategy={verticalListSortingStrategy}
|
||||
>
|
||||
<div className="min-h-[600px] bg-background">
|
||||
{pageBlocks.length === 0 ? (
|
||||
<div className="flex h-96 items-center justify-center rounded-lg border border-dashed text-center">
|
||||
<div className="space-y-2">
|
||||
<p className="text-sm text-muted-foreground">
|
||||
Your page is empty
|
||||
</p>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
Click blocks from the left to add them
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
pageBlocks.map((block) => (
|
||||
<SortableBlock
|
||||
key={block.id}
|
||||
id={block.id}
|
||||
blockName={block.blockName}
|
||||
onRemove={() => removeBlock(block.id)}
|
||||
/>
|
||||
))
|
||||
)}
|
||||
</div>
|
||||
</SortableContext>
|
||||
|
||||
<DragOverlay>
|
||||
{activeId ? (
|
||||
<div className="rounded-lg border bg-card p-4 shadow-lg">
|
||||
<p className="text-sm font-medium">
|
||||
{pageBlocks.find((b) => b.id === activeId)?.blockName}
|
||||
</p>
|
||||
</div>
|
||||
) : null}
|
||||
</DragOverlay>
|
||||
</DndContext>
|
||||
</div>
|
||||
</div>
|
||||
</ScrollArea>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function SortableBlock({
|
||||
id,
|
||||
blockName,
|
||||
onRemove,
|
||||
}: {
|
||||
id: string
|
||||
blockName: string
|
||||
onRemove: () => void
|
||||
}) {
|
||||
const {
|
||||
attributes,
|
||||
listeners,
|
||||
setNodeRef,
|
||||
transform,
|
||||
transition,
|
||||
isDragging,
|
||||
} = useSortable({ id })
|
||||
|
||||
const style = {
|
||||
transform: CSS.Transform.toString(transform),
|
||||
transition,
|
||||
opacity: isDragging ? 0.5 : 1,
|
||||
}
|
||||
|
||||
return (
|
||||
<div ref={setNodeRef} style={style} className="group relative">
|
||||
<div className="absolute -left-12 top-2 z-10 flex flex-col items-center gap-2">
|
||||
<button
|
||||
{...attributes}
|
||||
{...listeners}
|
||||
className="cursor-grab rounded bg-card p-1 shadow-sm hover:shadow active:cursor-grabbing"
|
||||
>
|
||||
<GripVertical className="h-4 w-4 text-muted-foreground" />
|
||||
</button>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
onClick={onRemove}
|
||||
className="h-6 w-6 bg-background/80 opacity-0 backdrop-blur transition-opacity group-hover:opacity-100"
|
||||
>
|
||||
<Trash2 className="h-3 w-3" />
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{/* Block Preview - No gaps between blocks */}
|
||||
<div className="relative overflow-hidden border-b last:border-b-0">
|
||||
<DynamicBlock blockName={blockName} scale={1} />
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,304 @@
|
||||
"use client"
|
||||
|
||||
import { ChartDisplay } from "@/components/chart-display"
|
||||
import { Separator } from "@/registry/default/ui/separator"
|
||||
|
||||
import * as Charts from "./charts"
|
||||
|
||||
export function ChartsClient() {
|
||||
return (
|
||||
<div className="container mx-auto py-10">
|
||||
<div className="space-y-4">
|
||||
<div>
|
||||
<h1 className="text-3xl font-bold tracking-tight">Charts</h1>
|
||||
<p className="text-muted-foreground">
|
||||
Beautiful charts built with Recharts. Copy and paste into your apps.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="grid gap-4">
|
||||
<div className="gap-6 md:flex md:flex-row-reverse md:items-start">
|
||||
<div className="grid flex-1 gap-12">
|
||||
<h2 className="sr-only">Examples</h2>
|
||||
<div
|
||||
id="examples"
|
||||
className="grid flex-1 scroll-mt-20 items-start gap-10 md:grid-cols-2 md:gap-6 lg:grid-cols-3 xl:gap-10"
|
||||
>
|
||||
<ChartDisplay name="chart-area-stacked">
|
||||
<Charts.ChartAreaStacked />
|
||||
</ChartDisplay>
|
||||
<ChartDisplay name="chart-bar-multiple">
|
||||
<Charts.ChartBarMultiple />
|
||||
</ChartDisplay>
|
||||
<ChartDisplay
|
||||
name="chart-pie-donut-text"
|
||||
className="[&_[data-chart]]:xl:max-h-[243px]"
|
||||
>
|
||||
<Charts.ChartPieDonutText />
|
||||
</ChartDisplay>
|
||||
</div>
|
||||
<Separator />
|
||||
<div
|
||||
id="area-chart"
|
||||
className="grid flex-1 scroll-mt-20 items-start gap-10 md:grid-cols-2 md:gap-6 lg:grid-cols-3 xl:gap-10"
|
||||
>
|
||||
<ChartDisplay name="chart-area-default">
|
||||
<Charts.ChartAreaDefault />
|
||||
</ChartDisplay>
|
||||
<ChartDisplay name="chart-area-linear">
|
||||
<Charts.ChartAreaLinear />
|
||||
</ChartDisplay>
|
||||
<ChartDisplay name="chart-area-step">
|
||||
<Charts.ChartAreaStep />
|
||||
</ChartDisplay>
|
||||
<ChartDisplay name="chart-area-stacked">
|
||||
<Charts.ChartAreaStacked />
|
||||
</ChartDisplay>
|
||||
<ChartDisplay name="chart-area-stacked-expand">
|
||||
<Charts.ChartAreaStackedExpand />
|
||||
</ChartDisplay>
|
||||
<ChartDisplay name="chart-area-legend">
|
||||
<Charts.ChartAreaLegend />
|
||||
</ChartDisplay>
|
||||
<ChartDisplay name="chart-area-icons">
|
||||
<Charts.ChartAreaIcons />
|
||||
</ChartDisplay>
|
||||
<ChartDisplay name="chart-area-gradient">
|
||||
<Charts.ChartAreaGradient />
|
||||
</ChartDisplay>
|
||||
<ChartDisplay name="chart-area-axes">
|
||||
<Charts.ChartAreaAxes />
|
||||
</ChartDisplay>
|
||||
<div className="md:col-span-2 lg:col-span-3">
|
||||
<ChartDisplay name="chart-area-interactive">
|
||||
<Charts.ChartAreaInteractive />
|
||||
</ChartDisplay>
|
||||
</div>
|
||||
</div>
|
||||
<Separator />
|
||||
<div
|
||||
id="bar-chart"
|
||||
className="grid flex-1 scroll-mt-20 items-start gap-10 md:grid-cols-2 md:gap-6 lg:grid-cols-3 xl:gap-10"
|
||||
>
|
||||
<ChartDisplay name="chart-bar-default">
|
||||
<Charts.ChartBarDefault />
|
||||
</ChartDisplay>
|
||||
<ChartDisplay name="chart-bar-horizontal">
|
||||
<Charts.ChartBarHorizontal />
|
||||
</ChartDisplay>
|
||||
<ChartDisplay name="chart-bar-multiple">
|
||||
<Charts.ChartBarMultiple />
|
||||
</ChartDisplay>
|
||||
<ChartDisplay name="chart-bar-label">
|
||||
<Charts.ChartBarLabel />
|
||||
</ChartDisplay>
|
||||
<ChartDisplay name="chart-bar-label-custom">
|
||||
<Charts.ChartBarLabelCustom />
|
||||
</ChartDisplay>
|
||||
<ChartDisplay name="chart-bar-mixed">
|
||||
<Charts.ChartBarMixed />
|
||||
</ChartDisplay>
|
||||
<ChartDisplay name="chart-bar-stacked">
|
||||
<Charts.ChartBarStacked />
|
||||
</ChartDisplay>
|
||||
<ChartDisplay name="chart-bar-active">
|
||||
<Charts.ChartBarActive />
|
||||
</ChartDisplay>
|
||||
<ChartDisplay name="chart-bar-negative">
|
||||
<Charts.ChartBarNegative />
|
||||
</ChartDisplay>
|
||||
<div className="md:col-span-2 lg:col-span-3">
|
||||
<ChartDisplay name="chart-bar-interactive">
|
||||
<Charts.ChartBarInteractive />
|
||||
</ChartDisplay>
|
||||
</div>
|
||||
</div>
|
||||
<Separator />
|
||||
<div
|
||||
id="line-chart"
|
||||
className="grid flex-1 scroll-mt-20 items-start gap-10 md:grid-cols-2 md:gap-6 lg:grid-cols-3 xl:gap-10"
|
||||
>
|
||||
<ChartDisplay name="chart-line-default">
|
||||
<Charts.ChartLineDefault />
|
||||
</ChartDisplay>
|
||||
<ChartDisplay name="chart-line-linear">
|
||||
<Charts.ChartLineLinear />
|
||||
</ChartDisplay>
|
||||
<ChartDisplay name="chart-line-step">
|
||||
<Charts.ChartLineStep />
|
||||
</ChartDisplay>
|
||||
<ChartDisplay name="chart-line-multiple">
|
||||
<Charts.ChartLineMultiple />
|
||||
</ChartDisplay>
|
||||
<ChartDisplay name="chart-line-dots">
|
||||
<Charts.ChartLineDots />
|
||||
</ChartDisplay>
|
||||
<ChartDisplay name="chart-line-dots-custom">
|
||||
<Charts.ChartLineDotsCustom />
|
||||
</ChartDisplay>
|
||||
<ChartDisplay name="chart-line-dots-colors">
|
||||
<Charts.ChartLineDotsColors />
|
||||
</ChartDisplay>
|
||||
<ChartDisplay name="chart-line-label">
|
||||
<Charts.ChartLineLabel />
|
||||
</ChartDisplay>
|
||||
<ChartDisplay name="chart-line-label-custom">
|
||||
<Charts.ChartLineLabelCustom />
|
||||
</ChartDisplay>
|
||||
<div className="md:col-span-2 lg:col-span-3">
|
||||
<ChartDisplay name="chart-line-interactive">
|
||||
<Charts.ChartLineInteractive />
|
||||
</ChartDisplay>
|
||||
</div>
|
||||
</div>
|
||||
<Separator />
|
||||
<div
|
||||
id="pie-chart"
|
||||
className="grid flex-1 scroll-mt-20 items-start gap-10 md:grid-cols-2 md:gap-6 lg:grid-cols-3 xl:gap-10"
|
||||
>
|
||||
<ChartDisplay name="chart-pie-simple">
|
||||
<Charts.ChartPieSimple />
|
||||
</ChartDisplay>
|
||||
<ChartDisplay name="chart-pie-separator-none">
|
||||
<Charts.ChartPieSeparatorNone />
|
||||
</ChartDisplay>
|
||||
<ChartDisplay name="chart-pie-label">
|
||||
<Charts.ChartPieLabel />
|
||||
</ChartDisplay>
|
||||
<ChartDisplay name="chart-pie-label-custom">
|
||||
<Charts.ChartPieLabelCustom />
|
||||
</ChartDisplay>
|
||||
<ChartDisplay name="chart-pie-label-list">
|
||||
<Charts.ChartPieLabelList />
|
||||
</ChartDisplay>
|
||||
<ChartDisplay name="chart-pie-legend">
|
||||
<Charts.ChartPieLegend />
|
||||
</ChartDisplay>
|
||||
<ChartDisplay name="chart-pie-donut">
|
||||
<Charts.ChartPieDonut />
|
||||
</ChartDisplay>
|
||||
<ChartDisplay name="chart-pie-donut-active">
|
||||
<Charts.ChartPieDonutActive />
|
||||
</ChartDisplay>
|
||||
<ChartDisplay name="chart-pie-donut-text">
|
||||
<Charts.ChartPieDonutText />
|
||||
</ChartDisplay>
|
||||
<ChartDisplay name="chart-pie-stacked">
|
||||
<Charts.ChartPieStacked />
|
||||
</ChartDisplay>
|
||||
<ChartDisplay name="chart-pie-interactive">
|
||||
<Charts.ChartPieInteractive />
|
||||
</ChartDisplay>
|
||||
</div>
|
||||
<Separator />
|
||||
<div
|
||||
id="radar-chart"
|
||||
className="grid flex-1 scroll-mt-20 gap-6 md:grid-cols-2 lg:grid-cols-3 xl:gap-10"
|
||||
>
|
||||
<ChartDisplay name="chart-radar-default">
|
||||
<Charts.ChartRadarDefault />
|
||||
</ChartDisplay>
|
||||
<ChartDisplay name="chart-radar-dots">
|
||||
<Charts.ChartRadarDots />
|
||||
</ChartDisplay>
|
||||
<ChartDisplay name="chart-radar-multiple">
|
||||
<Charts.ChartRadarMultiple />
|
||||
</ChartDisplay>
|
||||
<ChartDisplay name="chart-radar-lines-only">
|
||||
<Charts.ChartRadarLinesOnly />
|
||||
</ChartDisplay>
|
||||
<ChartDisplay name="chart-radar-label-custom">
|
||||
<Charts.ChartRadarLabelCustom />
|
||||
</ChartDisplay>
|
||||
<ChartDisplay name="chart-radar-radius">
|
||||
<Charts.ChartRadarRadius />
|
||||
</ChartDisplay>
|
||||
<ChartDisplay name="chart-radar-grid-custom">
|
||||
<Charts.ChartRadarGridCustom />
|
||||
</ChartDisplay>
|
||||
<ChartDisplay name="chart-radar-grid-fill">
|
||||
<Charts.ChartRadarGridFill />
|
||||
</ChartDisplay>
|
||||
<ChartDisplay name="chart-radar-grid-none">
|
||||
<Charts.ChartRadarGridNone />
|
||||
</ChartDisplay>
|
||||
<ChartDisplay name="chart-radar-grid-circle">
|
||||
<Charts.ChartRadarGridCircle />
|
||||
</ChartDisplay>
|
||||
<ChartDisplay name="chart-radar-grid-circle-no-lines">
|
||||
<Charts.ChartRadarGridCircleNoLines />
|
||||
</ChartDisplay>
|
||||
<ChartDisplay name="chart-radar-grid-circle-fill">
|
||||
<Charts.ChartRadarGridCircleFill />
|
||||
</ChartDisplay>
|
||||
<ChartDisplay name="chart-radar-legend">
|
||||
<Charts.ChartRadarLegend />
|
||||
</ChartDisplay>
|
||||
<ChartDisplay name="chart-radar-icons">
|
||||
<Charts.ChartRadarIcons />
|
||||
</ChartDisplay>
|
||||
</div>
|
||||
<Separator />
|
||||
<div
|
||||
id="radial-chart"
|
||||
className="grid flex-1 scroll-mt-20 gap-6 md:grid-cols-2 lg:grid-cols-3 xl:gap-10"
|
||||
>
|
||||
<ChartDisplay name="chart-radial-simple">
|
||||
<Charts.ChartRadialSimple />
|
||||
</ChartDisplay>
|
||||
<ChartDisplay name="chart-radial-label">
|
||||
<Charts.ChartRadialLabel />
|
||||
</ChartDisplay>
|
||||
<ChartDisplay name="chart-radial-grid">
|
||||
<Charts.ChartRadialGrid />
|
||||
</ChartDisplay>
|
||||
<ChartDisplay name="chart-radial-text">
|
||||
<Charts.ChartRadialText />
|
||||
</ChartDisplay>
|
||||
<ChartDisplay name="chart-radial-shape">
|
||||
<Charts.ChartRadialShape />
|
||||
</ChartDisplay>
|
||||
<ChartDisplay name="chart-radial-stacked">
|
||||
<Charts.ChartRadialStacked />
|
||||
</ChartDisplay>
|
||||
</div>
|
||||
<Separator />
|
||||
<div
|
||||
id="tooltip"
|
||||
className="chart-wrapper grid flex-1 scroll-mt-20 items-start gap-10 md:grid-cols-2 md:gap-6 lg:grid-cols-3 xl:gap-10"
|
||||
>
|
||||
<ChartDisplay name="chart-tooltip-default">
|
||||
<Charts.ChartTooltipDefault />
|
||||
</ChartDisplay>
|
||||
<ChartDisplay name="chart-tooltip-indicator-line">
|
||||
<Charts.ChartTooltipIndicatorLine />
|
||||
</ChartDisplay>
|
||||
<ChartDisplay name="chart-tooltip-indicator-none">
|
||||
<Charts.ChartTooltipIndicatorNone />
|
||||
</ChartDisplay>
|
||||
<ChartDisplay name="chart-tooltip-label-custom">
|
||||
<Charts.ChartTooltipLabelCustom />
|
||||
</ChartDisplay>
|
||||
<ChartDisplay name="chart-tooltip-label-formatter">
|
||||
<Charts.ChartTooltipLabelFormatter />
|
||||
</ChartDisplay>
|
||||
<ChartDisplay name="chart-tooltip-label-none">
|
||||
<Charts.ChartTooltipLabelNone />
|
||||
</ChartDisplay>
|
||||
<ChartDisplay name="chart-tooltip-formatter">
|
||||
<Charts.ChartTooltipFormatter />
|
||||
</ChartDisplay>
|
||||
<ChartDisplay name="chart-tooltip-icons">
|
||||
<Charts.ChartTooltipIcons />
|
||||
</ChartDisplay>
|
||||
<ChartDisplay name="chart-tooltip-advanced">
|
||||
<Charts.ChartTooltipAdvanced />
|
||||
</ChartDisplay>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
+20
-296
@@ -1,304 +1,28 @@
|
||||
"use client"
|
||||
|
||||
import { ChartDisplay } from "@/components/chart-display"
|
||||
import { Separator } from "@/registry/default/ui/separator"
|
||||
import dynamic from "next/dynamic"
|
||||
|
||||
import * as Charts from "./charts"
|
||||
import { SkeletonCardGrid } from "@/components/skeleton-card"
|
||||
|
||||
export default function ChartsPage() {
|
||||
return (
|
||||
<div className="container mx-auto py-10">
|
||||
<div className="space-y-4">
|
||||
<div>
|
||||
<h1 className="text-3xl font-bold tracking-tight">Charts</h1>
|
||||
<p className="text-muted-foreground">
|
||||
Beautiful charts built with Recharts. Copy and paste into your apps.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="grid gap-4">
|
||||
<div className="gap-6 md:flex md:flex-row-reverse md:items-start">
|
||||
<div className="grid flex-1 gap-12">
|
||||
<h2 className="sr-only">Examples</h2>
|
||||
<div
|
||||
id="examples"
|
||||
className="grid flex-1 scroll-mt-20 items-start gap-10 md:grid-cols-2 md:gap-6 lg:grid-cols-3 xl:gap-10"
|
||||
>
|
||||
<ChartDisplay name="chart-area-stacked">
|
||||
<Charts.ChartAreaStacked />
|
||||
</ChartDisplay>
|
||||
<ChartDisplay name="chart-bar-multiple">
|
||||
<Charts.ChartBarMultiple />
|
||||
</ChartDisplay>
|
||||
<ChartDisplay
|
||||
name="chart-pie-donut-text"
|
||||
className="[&_[data-chart]]:xl:max-h-[243px]"
|
||||
>
|
||||
<Charts.ChartPieDonutText />
|
||||
</ChartDisplay>
|
||||
</div>
|
||||
<Separator />
|
||||
<div
|
||||
id="area-chart"
|
||||
className="grid flex-1 scroll-mt-20 items-start gap-10 md:grid-cols-2 md:gap-6 lg:grid-cols-3 xl:gap-10"
|
||||
>
|
||||
<ChartDisplay name="chart-area-default">
|
||||
<Charts.ChartAreaDefault />
|
||||
</ChartDisplay>
|
||||
<ChartDisplay name="chart-area-linear">
|
||||
<Charts.ChartAreaLinear />
|
||||
</ChartDisplay>
|
||||
<ChartDisplay name="chart-area-step">
|
||||
<Charts.ChartAreaStep />
|
||||
</ChartDisplay>
|
||||
<ChartDisplay name="chart-area-stacked">
|
||||
<Charts.ChartAreaStacked />
|
||||
</ChartDisplay>
|
||||
<ChartDisplay name="chart-area-stacked-expand">
|
||||
<Charts.ChartAreaStackedExpand />
|
||||
</ChartDisplay>
|
||||
<ChartDisplay name="chart-area-legend">
|
||||
<Charts.ChartAreaLegend />
|
||||
</ChartDisplay>
|
||||
<ChartDisplay name="chart-area-icons">
|
||||
<Charts.ChartAreaIcons />
|
||||
</ChartDisplay>
|
||||
<ChartDisplay name="chart-area-gradient">
|
||||
<Charts.ChartAreaGradient />
|
||||
</ChartDisplay>
|
||||
<ChartDisplay name="chart-area-axes">
|
||||
<Charts.ChartAreaAxes />
|
||||
</ChartDisplay>
|
||||
<div className="md:col-span-2 lg:col-span-3">
|
||||
<ChartDisplay name="chart-area-interactive">
|
||||
<Charts.ChartAreaInteractive />
|
||||
</ChartDisplay>
|
||||
</div>
|
||||
</div>
|
||||
<Separator />
|
||||
<div
|
||||
id="bar-chart"
|
||||
className="grid flex-1 scroll-mt-20 items-start gap-10 md:grid-cols-2 md:gap-6 lg:grid-cols-3 xl:gap-10"
|
||||
>
|
||||
<ChartDisplay name="chart-bar-default">
|
||||
<Charts.ChartBarDefault />
|
||||
</ChartDisplay>
|
||||
<ChartDisplay name="chart-bar-horizontal">
|
||||
<Charts.ChartBarHorizontal />
|
||||
</ChartDisplay>
|
||||
<ChartDisplay name="chart-bar-multiple">
|
||||
<Charts.ChartBarMultiple />
|
||||
</ChartDisplay>
|
||||
<ChartDisplay name="chart-bar-label">
|
||||
<Charts.ChartBarLabel />
|
||||
</ChartDisplay>
|
||||
<ChartDisplay name="chart-bar-label-custom">
|
||||
<Charts.ChartBarLabelCustom />
|
||||
</ChartDisplay>
|
||||
<ChartDisplay name="chart-bar-mixed">
|
||||
<Charts.ChartBarMixed />
|
||||
</ChartDisplay>
|
||||
<ChartDisplay name="chart-bar-stacked">
|
||||
<Charts.ChartBarStacked />
|
||||
</ChartDisplay>
|
||||
<ChartDisplay name="chart-bar-active">
|
||||
<Charts.ChartBarActive />
|
||||
</ChartDisplay>
|
||||
<ChartDisplay name="chart-bar-negative">
|
||||
<Charts.ChartBarNegative />
|
||||
</ChartDisplay>
|
||||
<div className="md:col-span-2 lg:col-span-3">
|
||||
<ChartDisplay name="chart-bar-interactive">
|
||||
<Charts.ChartBarInteractive />
|
||||
</ChartDisplay>
|
||||
</div>
|
||||
</div>
|
||||
<Separator />
|
||||
<div
|
||||
id="line-chart"
|
||||
className="grid flex-1 scroll-mt-20 items-start gap-10 md:grid-cols-2 md:gap-6 lg:grid-cols-3 xl:gap-10"
|
||||
>
|
||||
<ChartDisplay name="chart-line-default">
|
||||
<Charts.ChartLineDefault />
|
||||
</ChartDisplay>
|
||||
<ChartDisplay name="chart-line-linear">
|
||||
<Charts.ChartLineLinear />
|
||||
</ChartDisplay>
|
||||
<ChartDisplay name="chart-line-step">
|
||||
<Charts.ChartLineStep />
|
||||
</ChartDisplay>
|
||||
<ChartDisplay name="chart-line-multiple">
|
||||
<Charts.ChartLineMultiple />
|
||||
</ChartDisplay>
|
||||
<ChartDisplay name="chart-line-dots">
|
||||
<Charts.ChartLineDots />
|
||||
</ChartDisplay>
|
||||
<ChartDisplay name="chart-line-dots-custom">
|
||||
<Charts.ChartLineDotsCustom />
|
||||
</ChartDisplay>
|
||||
<ChartDisplay name="chart-line-dots-colors">
|
||||
<Charts.ChartLineDotsColors />
|
||||
</ChartDisplay>
|
||||
<ChartDisplay name="chart-line-label">
|
||||
<Charts.ChartLineLabel />
|
||||
</ChartDisplay>
|
||||
<ChartDisplay name="chart-line-label-custom">
|
||||
<Charts.ChartLineLabelCustom />
|
||||
</ChartDisplay>
|
||||
<div className="md:col-span-2 lg:col-span-3">
|
||||
<ChartDisplay name="chart-line-interactive">
|
||||
<Charts.ChartLineInteractive />
|
||||
</ChartDisplay>
|
||||
</div>
|
||||
</div>
|
||||
<Separator />
|
||||
<div
|
||||
id="pie-chart"
|
||||
className="grid flex-1 scroll-mt-20 items-start gap-10 md:grid-cols-2 md:gap-6 lg:grid-cols-3 xl:gap-10"
|
||||
>
|
||||
<ChartDisplay name="chart-pie-simple">
|
||||
<Charts.ChartPieSimple />
|
||||
</ChartDisplay>
|
||||
<ChartDisplay name="chart-pie-separator-none">
|
||||
<Charts.ChartPieSeparatorNone />
|
||||
</ChartDisplay>
|
||||
<ChartDisplay name="chart-pie-label">
|
||||
<Charts.ChartPieLabel />
|
||||
</ChartDisplay>
|
||||
<ChartDisplay name="chart-pie-label-custom">
|
||||
<Charts.ChartPieLabelCustom />
|
||||
</ChartDisplay>
|
||||
<ChartDisplay name="chart-pie-label-list">
|
||||
<Charts.ChartPieLabelList />
|
||||
</ChartDisplay>
|
||||
<ChartDisplay name="chart-pie-legend">
|
||||
<Charts.ChartPieLegend />
|
||||
</ChartDisplay>
|
||||
<ChartDisplay name="chart-pie-donut">
|
||||
<Charts.ChartPieDonut />
|
||||
</ChartDisplay>
|
||||
<ChartDisplay name="chart-pie-donut-active">
|
||||
<Charts.ChartPieDonutActive />
|
||||
</ChartDisplay>
|
||||
<ChartDisplay name="chart-pie-donut-text">
|
||||
<Charts.ChartPieDonutText />
|
||||
</ChartDisplay>
|
||||
<ChartDisplay name="chart-pie-stacked">
|
||||
<Charts.ChartPieStacked />
|
||||
</ChartDisplay>
|
||||
<ChartDisplay name="chart-pie-interactive">
|
||||
<Charts.ChartPieInteractive />
|
||||
</ChartDisplay>
|
||||
</div>
|
||||
<Separator />
|
||||
<div
|
||||
id="radar-chart"
|
||||
className="grid flex-1 scroll-mt-20 gap-6 md:grid-cols-2 lg:grid-cols-3 xl:gap-10"
|
||||
>
|
||||
<ChartDisplay name="chart-radar-default">
|
||||
<Charts.ChartRadarDefault />
|
||||
</ChartDisplay>
|
||||
<ChartDisplay name="chart-radar-dots">
|
||||
<Charts.ChartRadarDots />
|
||||
</ChartDisplay>
|
||||
<ChartDisplay name="chart-radar-multiple">
|
||||
<Charts.ChartRadarMultiple />
|
||||
</ChartDisplay>
|
||||
<ChartDisplay name="chart-radar-lines-only">
|
||||
<Charts.ChartRadarLinesOnly />
|
||||
</ChartDisplay>
|
||||
<ChartDisplay name="chart-radar-label-custom">
|
||||
<Charts.ChartRadarLabelCustom />
|
||||
</ChartDisplay>
|
||||
<ChartDisplay name="chart-radar-radius">
|
||||
<Charts.ChartRadarRadius />
|
||||
</ChartDisplay>
|
||||
<ChartDisplay name="chart-radar-grid-custom">
|
||||
<Charts.ChartRadarGridCustom />
|
||||
</ChartDisplay>
|
||||
<ChartDisplay name="chart-radar-grid-fill">
|
||||
<Charts.ChartRadarGridFill />
|
||||
</ChartDisplay>
|
||||
<ChartDisplay name="chart-radar-grid-none">
|
||||
<Charts.ChartRadarGridNone />
|
||||
</ChartDisplay>
|
||||
<ChartDisplay name="chart-radar-grid-circle">
|
||||
<Charts.ChartRadarGridCircle />
|
||||
</ChartDisplay>
|
||||
<ChartDisplay name="chart-radar-grid-circle-no-lines">
|
||||
<Charts.ChartRadarGridCircleNoLines />
|
||||
</ChartDisplay>
|
||||
<ChartDisplay name="chart-radar-grid-circle-fill">
|
||||
<Charts.ChartRadarGridCircleFill />
|
||||
</ChartDisplay>
|
||||
<ChartDisplay name="chart-radar-legend">
|
||||
<Charts.ChartRadarLegend />
|
||||
</ChartDisplay>
|
||||
<ChartDisplay name="chart-radar-icons">
|
||||
<Charts.ChartRadarIcons />
|
||||
</ChartDisplay>
|
||||
</div>
|
||||
<Separator />
|
||||
<div
|
||||
id="radial-chart"
|
||||
className="grid flex-1 scroll-mt-20 gap-6 md:grid-cols-2 lg:grid-cols-3 xl:gap-10"
|
||||
>
|
||||
<ChartDisplay name="chart-radial-simple">
|
||||
<Charts.ChartRadialSimple />
|
||||
</ChartDisplay>
|
||||
<ChartDisplay name="chart-radial-label">
|
||||
<Charts.ChartRadialLabel />
|
||||
</ChartDisplay>
|
||||
<ChartDisplay name="chart-radial-grid">
|
||||
<Charts.ChartRadialGrid />
|
||||
</ChartDisplay>
|
||||
<ChartDisplay name="chart-radial-text">
|
||||
<Charts.ChartRadialText />
|
||||
</ChartDisplay>
|
||||
<ChartDisplay name="chart-radial-shape">
|
||||
<Charts.ChartRadialShape />
|
||||
</ChartDisplay>
|
||||
<ChartDisplay name="chart-radial-stacked">
|
||||
<Charts.ChartRadialStacked />
|
||||
</ChartDisplay>
|
||||
</div>
|
||||
<Separator />
|
||||
<div
|
||||
id="tooltip"
|
||||
className="chart-wrapper grid flex-1 scroll-mt-20 items-start gap-10 md:grid-cols-2 md:gap-6 lg:grid-cols-3 xl:gap-10"
|
||||
>
|
||||
<ChartDisplay name="chart-tooltip-default">
|
||||
<Charts.ChartTooltipDefault />
|
||||
</ChartDisplay>
|
||||
<ChartDisplay name="chart-tooltip-indicator-line">
|
||||
<Charts.ChartTooltipIndicatorLine />
|
||||
</ChartDisplay>
|
||||
<ChartDisplay name="chart-tooltip-indicator-none">
|
||||
<Charts.ChartTooltipIndicatorNone />
|
||||
</ChartDisplay>
|
||||
<ChartDisplay name="chart-tooltip-label-custom">
|
||||
<Charts.ChartTooltipLabelCustom />
|
||||
</ChartDisplay>
|
||||
<ChartDisplay name="chart-tooltip-label-formatter">
|
||||
<Charts.ChartTooltipLabelFormatter />
|
||||
</ChartDisplay>
|
||||
<ChartDisplay name="chart-tooltip-label-none">
|
||||
<Charts.ChartTooltipLabelNone />
|
||||
</ChartDisplay>
|
||||
<ChartDisplay name="chart-tooltip-formatter">
|
||||
<Charts.ChartTooltipFormatter />
|
||||
</ChartDisplay>
|
||||
<ChartDisplay name="chart-tooltip-icons">
|
||||
<Charts.ChartTooltipIcons />
|
||||
</ChartDisplay>
|
||||
<ChartDisplay name="chart-tooltip-advanced">
|
||||
<Charts.ChartTooltipAdvanced />
|
||||
</ChartDisplay>
|
||||
</div>
|
||||
</div>
|
||||
// Dynamically import heavy chart components
|
||||
const ChartsClient = dynamic(
|
||||
() => import("./charts-client").then((mod) => mod.ChartsClient),
|
||||
{
|
||||
loading: () => (
|
||||
<div className="container mx-auto py-10">
|
||||
<div className="space-y-6">
|
||||
<div>
|
||||
<div className="h-10 w-48 bg-muted animate-pulse rounded" />
|
||||
<div className="h-6 w-96 bg-muted animate-pulse rounded mt-2" />
|
||||
</div>
|
||||
<SkeletonCardGrid count={9} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
),
|
||||
ssr: false, // Charts are interactive with Recharts
|
||||
}
|
||||
)
|
||||
|
||||
export default function ChartsPage() {
|
||||
return <ChartsClient />
|
||||
}
|
||||
|
||||
@@ -0,0 +1,606 @@
|
||||
"use client"
|
||||
|
||||
import * as React from "react"
|
||||
import {
|
||||
addEdge,
|
||||
Background,
|
||||
Connection,
|
||||
Controls,
|
||||
Edge,
|
||||
MiniMap,
|
||||
Node,
|
||||
NodeTypes,
|
||||
Panel,
|
||||
ReactFlow,
|
||||
useEdgesState,
|
||||
useNodesState,
|
||||
} from "@xyflow/react"
|
||||
import yaml from "js-yaml"
|
||||
// import "@xyflow/react/dist/style.css" // Causes build issues, styles inlined in component
|
||||
import {
|
||||
Download,
|
||||
HardDrive,
|
||||
Library,
|
||||
Network,
|
||||
Plus,
|
||||
Settings2,
|
||||
} from "lucide-react"
|
||||
|
||||
import { Button } from "@/registry/default/ui/button"
|
||||
import { Card } from "@/registry/default/ui/card"
|
||||
import { Input } from "@/registry/default/ui/input"
|
||||
import { Label } from "@/registry/default/ui/label"
|
||||
import { ScrollArea } from "@/registry/default/ui/scroll-area"
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from "@/registry/default/ui/select"
|
||||
import { Separator } from "@/registry/default/ui/separator"
|
||||
import {
|
||||
Sheet,
|
||||
SheetContent,
|
||||
SheetHeader,
|
||||
SheetTitle,
|
||||
SheetTrigger,
|
||||
} from "@/registry/default/ui/sheet"
|
||||
import {
|
||||
Tabs,
|
||||
TabsContent,
|
||||
TabsList,
|
||||
TabsTrigger,
|
||||
} from "@/registry/default/ui/tabs"
|
||||
|
||||
const COMMON_IMAGES = [
|
||||
{ name: "nginx", image: "nginx:alpine", category: "proxy" },
|
||||
{ name: "postgres", image: "postgres:16-alpine", category: "database" },
|
||||
{ name: "mysql", image: "mysql:8.4", category: "database" },
|
||||
{ name: "mongodb", image: "mongo:7", category: "database" },
|
||||
{ name: "redis", image: "redis:7-alpine", category: "cache" },
|
||||
{ name: "node", image: "node:20-alpine", category: "runtime" },
|
||||
{ name: "php", image: "php:8.3-fpm-alpine", category: "runtime" },
|
||||
{ name: "ruby", image: "ruby:3.2-alpine", category: "runtime" },
|
||||
{ name: "python", image: "python:3.12-alpine", category: "runtime" },
|
||||
{
|
||||
name: "clickhouse",
|
||||
image: "clickhouse/clickhouse-server:latest",
|
||||
category: "analytics",
|
||||
},
|
||||
{
|
||||
name: "kafka",
|
||||
image: "confluentinc/cp-kafka:latest",
|
||||
category: "streaming",
|
||||
},
|
||||
{ name: "grafana", image: "grafana/grafana:latest", category: "monitoring" },
|
||||
]
|
||||
|
||||
interface ComposeService {
|
||||
image: string
|
||||
ports?: string[]
|
||||
environment?: Record<string, string>
|
||||
networks?: string[]
|
||||
volumes?: string[]
|
||||
depends_on?: string[]
|
||||
}
|
||||
|
||||
function ServiceNode({ data }: { data: any }) {
|
||||
return (
|
||||
<div className="rounded-lg border-2 bg-card p-4 shadow-lg min-w-[200px]">
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="rounded bg-primary/10 p-2">
|
||||
<Settings2 className="h-4 w-4 text-primary" />
|
||||
</div>
|
||||
<div className="flex-1">
|
||||
<div className="font-semibold">{data.name}</div>
|
||||
<div className="text-xs text-muted-foreground">{data.image}</div>
|
||||
</div>
|
||||
</div>
|
||||
{data.ports && (
|
||||
<div className="mt-2 text-xs text-muted-foreground">
|
||||
📡 {data.ports[0]}
|
||||
</div>
|
||||
)}
|
||||
{data.networks && (
|
||||
<div className="mt-1 flex gap-1">
|
||||
{data.networks.map((net: string) => (
|
||||
<span
|
||||
key={net}
|
||||
className="rounded bg-muted px-1.5 py-0.5 text-[10px]"
|
||||
>
|
||||
{net}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
const nodeTypes: NodeTypes = {
|
||||
service: ServiceNode,
|
||||
}
|
||||
|
||||
export function ComposeClient() {
|
||||
const [projectName, setProjectName] = React.useState("my-stack")
|
||||
const [services, setServices] = React.useState<
|
||||
Record<string, ComposeService>
|
||||
>({})
|
||||
const [networks, setNetworks] = React.useState<string[]>(["default"])
|
||||
const [volumes, setVolumes] = React.useState<string[]>([])
|
||||
const [selectedService, setSelectedService] = React.useState<string | null>(
|
||||
null
|
||||
)
|
||||
|
||||
const [nodes, setNodes, onNodesChange] = useNodesState<Node>([])
|
||||
const [edges, setEdges, onEdgesChange] = useEdgesState<Edge>([])
|
||||
|
||||
// Update nodes when services change
|
||||
React.useEffect(() => {
|
||||
const newNodes: Node[] = Object.entries(services).map(
|
||||
([name, config], i) => ({
|
||||
id: name,
|
||||
type: "service",
|
||||
position: { x: 100 + (i % 4) * 250, y: 100 + Math.floor(i / 4) * 150 },
|
||||
data: { name, ...config },
|
||||
})
|
||||
)
|
||||
|
||||
const newEdges: Edge[] = []
|
||||
Object.entries(services).forEach(([name, config]) => {
|
||||
config.depends_on?.forEach((dep) => {
|
||||
newEdges.push({
|
||||
id: `${dep}-${name}`,
|
||||
source: dep,
|
||||
target: name,
|
||||
animated: true,
|
||||
style: { stroke: "hsl(var(--primary))" },
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
setNodes(newNodes)
|
||||
setEdges(newEdges)
|
||||
}, [services, setNodes, setEdges])
|
||||
|
||||
const addService = (template: (typeof COMMON_IMAGES)[0]) => {
|
||||
const serviceName = `${template.name}_${Object.keys(services).length + 1}`
|
||||
setServices({
|
||||
...services,
|
||||
[serviceName]: {
|
||||
image: template.image,
|
||||
networks: ["default"],
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
const onConnect = React.useCallback(
|
||||
(params: Connection) => {
|
||||
// Add dependency when connecting nodes
|
||||
if (params.source && params.target) {
|
||||
setServices((prev) => ({
|
||||
...prev,
|
||||
[params.target!]: {
|
||||
...prev[params.target!],
|
||||
depends_on: [
|
||||
...(prev[params.target!]?.depends_on || []),
|
||||
params.source!,
|
||||
],
|
||||
},
|
||||
}))
|
||||
}
|
||||
setEdges((eds) => addEdge(params, eds))
|
||||
},
|
||||
[setEdges]
|
||||
)
|
||||
|
||||
const generateYAML = () => {
|
||||
const compose = {
|
||||
version: "3.8",
|
||||
services,
|
||||
networks: networks.reduce((acc, net) => ({ ...acc, [net]: {} }), {}),
|
||||
volumes: volumes.reduce((acc, vol) => ({ ...acc, [vol]: {} }), {}),
|
||||
}
|
||||
return yaml.dump(compose, { indent: 2 })
|
||||
}
|
||||
|
||||
const downloadCompose = () => {
|
||||
const yamlContent = generateYAML()
|
||||
const blob = new Blob([yamlContent], { type: "text/yaml" })
|
||||
const url = URL.createObjectURL(blob)
|
||||
const a = document.createElement("a")
|
||||
a.href = url
|
||||
a.download = "docker-compose.yml"
|
||||
a.click()
|
||||
}
|
||||
|
||||
const updateServiceConfig = (
|
||||
serviceName: string,
|
||||
key: string,
|
||||
value: any
|
||||
) => {
|
||||
setServices((prev) => ({
|
||||
...prev,
|
||||
[serviceName]: {
|
||||
...prev[serviceName],
|
||||
[key]: value,
|
||||
},
|
||||
}))
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex h-screen">
|
||||
{/* Main Canvas */}
|
||||
<div className="flex-1">
|
||||
<div className="flex h-full flex-col">
|
||||
{/* Top Toolbar */}
|
||||
<div className="flex items-center justify-between border-b bg-card p-4">
|
||||
<div className="flex items-center gap-2">
|
||||
<h1 className="text-xl font-semibold">Docker Compose Editor</h1>
|
||||
<span className="rounded bg-muted px-2 py-1 text-xs">
|
||||
{Object.keys(services).length} services
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div className="flex gap-2">
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() => {
|
||||
setServices({})
|
||||
setNetworks(["default"])
|
||||
setVolumes([])
|
||||
}}
|
||||
>
|
||||
Clear
|
||||
</Button>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() => {
|
||||
const input = document.createElement("input")
|
||||
input.type = "file"
|
||||
input.accept = ".yml,.yaml"
|
||||
input.onchange = (e: any) => {
|
||||
const file = e.target.files[0]
|
||||
const reader = new FileReader()
|
||||
reader.onload = (e) => {
|
||||
const content = e.target?.result as string
|
||||
const parsed = yaml.load(content) as any
|
||||
setServices(parsed.services || {})
|
||||
setNetworks(
|
||||
Object.keys(parsed.networks || { default: {} })
|
||||
)
|
||||
setVolumes(Object.keys(parsed.volumes || {}))
|
||||
}
|
||||
reader.readAsText(file)
|
||||
}
|
||||
input.click()
|
||||
}}
|
||||
>
|
||||
Import
|
||||
</Button>
|
||||
<Sheet>
|
||||
<SheetTrigger asChild>
|
||||
<Button size="sm">
|
||||
<Plus className="mr-2 h-4 w-4" />
|
||||
Add service
|
||||
</Button>
|
||||
</SheetTrigger>
|
||||
<SheetContent>
|
||||
<SheetHeader>
|
||||
<SheetTitle>Add Service</SheetTitle>
|
||||
</SheetHeader>
|
||||
<ScrollArea className="mt-4 h-[calc(100vh-100px)]">
|
||||
<div className="space-y-2">
|
||||
{COMMON_IMAGES.map((template) => (
|
||||
<Button
|
||||
key={template.image}
|
||||
variant="outline"
|
||||
className="w-full justify-start"
|
||||
onClick={() => addService(template)}
|
||||
>
|
||||
<Plus className="mr-2 h-4 w-4" />
|
||||
<div className="flex-1 text-left">
|
||||
<div className="font-medium">{template.name}</div>
|
||||
<div className="text-xs text-muted-foreground">
|
||||
{template.image}
|
||||
</div>
|
||||
</div>
|
||||
</Button>
|
||||
))}
|
||||
</div>
|
||||
</ScrollArea>
|
||||
</SheetContent>
|
||||
</Sheet>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Canvas */}
|
||||
<div className="flex-1 bg-background">
|
||||
{Object.keys(services).length === 0 ? (
|
||||
<div className="flex h-full items-center justify-center">
|
||||
<div className="text-center">
|
||||
<Settings2 className="mx-auto h-12 w-12 text-muted-foreground/50" />
|
||||
<h3 className="mt-4 text-lg font-semibold">
|
||||
No services yet
|
||||
</h3>
|
||||
<p className="mt-2 text-sm text-muted-foreground">
|
||||
Click "Add service" to start building your stack
|
||||
</p>
|
||||
<Button
|
||||
className="mt-4"
|
||||
onClick={() => addService(COMMON_IMAGES[1])}
|
||||
>
|
||||
<Plus className="mr-2 h-4 w-4" />
|
||||
Add your first service
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<ReactFlow
|
||||
nodes={nodes}
|
||||
edges={edges}
|
||||
onNodesChange={onNodesChange}
|
||||
onEdgesChange={onEdgesChange}
|
||||
onConnect={onConnect}
|
||||
nodeTypes={nodeTypes}
|
||||
onNodeClick={(_, node) => setSelectedService(node.id)}
|
||||
fitView
|
||||
className="bg-background"
|
||||
>
|
||||
<Background
|
||||
gap={16}
|
||||
color="hsl(var(--muted-foreground) / 0.3)"
|
||||
/>
|
||||
<Controls className="border-border bg-card" />
|
||||
<Panel position="top-right" className="space-x-2">
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() =>
|
||||
setNetworks([...networks, `network_${networks.length}`])
|
||||
}
|
||||
>
|
||||
<Network className="mr-2 h-4 w-4" />
|
||||
Add network
|
||||
</Button>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() =>
|
||||
setVolumes([...volumes, `vol_${volumes.length}`])
|
||||
}
|
||||
>
|
||||
<HardDrive className="mr-2 h-4 w-4" />
|
||||
Add volume
|
||||
</Button>
|
||||
</Panel>
|
||||
</ReactFlow>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Right Sidebar - Configuration */}
|
||||
<div className="flex w-80 flex-col border-l bg-card">
|
||||
<div className="border-b p-4">
|
||||
<h2 className="text-lg font-semibold">Compose</h2>
|
||||
<div className="mt-4 space-y-2">
|
||||
<Label htmlFor="project-name">Project Name</Label>
|
||||
<Input
|
||||
id="project-name"
|
||||
value={projectName}
|
||||
onChange={(e) => setProjectName(e.target.value)}
|
||||
placeholder="my-stack"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Tabs defaultValue="services" className="flex-1">
|
||||
<TabsList className="w-full rounded-none">
|
||||
<TabsTrigger value="services" className="flex-1">
|
||||
<Settings2 className="mr-2 h-4 w-4" />
|
||||
Services
|
||||
</TabsTrigger>
|
||||
<TabsTrigger value="config" className="flex-1">
|
||||
<Network className="mr-2 h-4 w-4" />
|
||||
Config
|
||||
</TabsTrigger>
|
||||
</TabsList>
|
||||
|
||||
<TabsContent value="services" className="mt-0 flex-1">
|
||||
<ScrollArea className="h-[calc(100vh-220px)]">
|
||||
<div className="space-y-2 p-4">
|
||||
{selectedService ? (
|
||||
<div className="space-y-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<h3 className="font-semibold">{selectedService}</h3>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={() => setSelectedService(null)}
|
||||
>
|
||||
✕
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label>Image</Label>
|
||||
<Input
|
||||
value={services[selectedService]?.image || ""}
|
||||
onChange={(e) =>
|
||||
updateServiceConfig(
|
||||
selectedService,
|
||||
"image",
|
||||
e.target.value
|
||||
)
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label>Ports</Label>
|
||||
<Input
|
||||
placeholder="80:80"
|
||||
onBlur={(e) => {
|
||||
if (e.target.value) {
|
||||
updateServiceConfig(selectedService, "ports", [
|
||||
e.target.value,
|
||||
])
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label>Networks</Label>
|
||||
<Select
|
||||
onValueChange={(value) => {
|
||||
updateServiceConfig(selectedService, "networks", [
|
||||
value,
|
||||
])
|
||||
}}
|
||||
>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder="Select network" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{networks.map((net) => (
|
||||
<SelectItem key={net} value={net}>
|
||||
{net}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div className="text-center text-sm text-muted-foreground">
|
||||
Click a service node to configure
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</ScrollArea>
|
||||
</TabsContent>
|
||||
|
||||
<TabsContent value="config" className="mt-0 flex-1">
|
||||
<div className="space-y-4 p-4">
|
||||
<div>
|
||||
<div className="flex items-center justify-between">
|
||||
<Label>Networks</Label>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={() => {
|
||||
const name = prompt("Network name:")
|
||||
if (name) setNetworks([...networks, name])
|
||||
}}
|
||||
>
|
||||
<Plus className="h-3 w-3" />
|
||||
</Button>
|
||||
</div>
|
||||
<div className="mt-2 space-y-1">
|
||||
{networks.map((net) => (
|
||||
<div
|
||||
key={net}
|
||||
className="rounded bg-muted px-2 py-1 text-sm"
|
||||
>
|
||||
{net}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<div className="flex items-center justify-between">
|
||||
<Label>Volumes</Label>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={() => {
|
||||
const name = prompt("Volume name:")
|
||||
if (name) setVolumes([...volumes, name])
|
||||
}}
|
||||
>
|
||||
<Plus className="h-3 w-3" />
|
||||
</Button>
|
||||
</div>
|
||||
<div className="mt-2 space-y-1">
|
||||
{volumes.map((vol) => (
|
||||
<div
|
||||
key={vol}
|
||||
className="rounded bg-muted px-2 py-1 text-sm"
|
||||
>
|
||||
{vol}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</TabsContent>
|
||||
</Tabs>
|
||||
|
||||
<Separator />
|
||||
|
||||
<div className="space-y-2 p-4">
|
||||
<Button onClick={downloadCompose} className="w-full">
|
||||
<Download className="mr-2 h-4 w-4" />
|
||||
Download compose
|
||||
</Button>
|
||||
|
||||
<Sheet>
|
||||
<SheetTrigger asChild>
|
||||
<Button variant="outline" className="w-full">
|
||||
<Library className="mr-2 h-4 w-4" />
|
||||
Library
|
||||
</Button>
|
||||
</SheetTrigger>
|
||||
<SheetContent>
|
||||
<SheetHeader>
|
||||
<SheetTitle>Service Library</SheetTitle>
|
||||
</SheetHeader>
|
||||
<ScrollArea className="mt-4 h-[calc(100vh-100px)]">
|
||||
<div className="space-y-4">
|
||||
{[
|
||||
"proxy",
|
||||
"database",
|
||||
"cache",
|
||||
"runtime",
|
||||
"analytics",
|
||||
"streaming",
|
||||
"monitoring",
|
||||
].map((category) => (
|
||||
<div key={category}>
|
||||
<h4 className="mb-2 text-sm font-semibold capitalize">
|
||||
{category}
|
||||
</h4>
|
||||
<div className="space-y-2">
|
||||
{COMMON_IMAGES.filter(
|
||||
(img) => img.category === category
|
||||
).map((template) => (
|
||||
<Button
|
||||
key={template.image}
|
||||
variant="outline"
|
||||
className="w-full justify-start"
|
||||
onClick={() => {
|
||||
addService(template)
|
||||
}}
|
||||
>
|
||||
<Plus className="mr-2 h-4 w-4" />
|
||||
{template.name}
|
||||
</Button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</ScrollArea>
|
||||
</SheetContent>
|
||||
</Sheet>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
+19
-596
@@ -1,606 +1,29 @@
|
||||
"use client"
|
||||
|
||||
import * as React from "react"
|
||||
import {
|
||||
addEdge,
|
||||
Background,
|
||||
Connection,
|
||||
Controls,
|
||||
Edge,
|
||||
MiniMap,
|
||||
Node,
|
||||
NodeTypes,
|
||||
Panel,
|
||||
ReactFlow,
|
||||
useEdgesState,
|
||||
useNodesState,
|
||||
} from "@xyflow/react"
|
||||
import yaml from "js-yaml"
|
||||
// import "@xyflow/react/dist/style.css" // Causes build issues, styles inlined in component
|
||||
import {
|
||||
Download,
|
||||
HardDrive,
|
||||
Library,
|
||||
Network,
|
||||
Plus,
|
||||
Settings2,
|
||||
} from "lucide-react"
|
||||
import dynamic from "next/dynamic"
|
||||
|
||||
import { Button } from "@/registry/default/ui/button"
|
||||
import { Card } from "@/registry/default/ui/card"
|
||||
import { Input } from "@/registry/default/ui/input"
|
||||
import { Label } from "@/registry/default/ui/label"
|
||||
import { ScrollArea } from "@/registry/default/ui/scroll-area"
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from "@/registry/default/ui/select"
|
||||
import { Separator } from "@/registry/default/ui/separator"
|
||||
import {
|
||||
Sheet,
|
||||
SheetContent,
|
||||
SheetHeader,
|
||||
SheetTitle,
|
||||
SheetTrigger,
|
||||
} from "@/registry/default/ui/sheet"
|
||||
import {
|
||||
Tabs,
|
||||
TabsContent,
|
||||
TabsList,
|
||||
TabsTrigger,
|
||||
} from "@/registry/default/ui/tabs"
|
||||
import { SkeletonCard } from "@/components/skeleton-card"
|
||||
|
||||
const COMMON_IMAGES = [
|
||||
{ name: "nginx", image: "nginx:alpine", category: "proxy" },
|
||||
{ name: "postgres", image: "postgres:16-alpine", category: "database" },
|
||||
{ name: "mysql", image: "mysql:8.4", category: "database" },
|
||||
{ name: "mongodb", image: "mongo:7", category: "database" },
|
||||
{ name: "redis", image: "redis:7-alpine", category: "cache" },
|
||||
{ name: "node", image: "node:20-alpine", category: "runtime" },
|
||||
{ name: "php", image: "php:8.3-fpm-alpine", category: "runtime" },
|
||||
{ name: "ruby", image: "ruby:3.2-alpine", category: "runtime" },
|
||||
{ name: "python", image: "python:3.12-alpine", category: "runtime" },
|
||||
// Dynamically import ReactFlow and heavy compose editor
|
||||
const ComposeClient = dynamic(
|
||||
() => import("./compose-client").then((mod) => mod.ComposeClient),
|
||||
{
|
||||
name: "clickhouse",
|
||||
image: "clickhouse/clickhouse-server:latest",
|
||||
category: "analytics",
|
||||
},
|
||||
{
|
||||
name: "kafka",
|
||||
image: "confluentinc/cp-kafka:latest",
|
||||
category: "streaming",
|
||||
},
|
||||
{ name: "grafana", image: "grafana/grafana:latest", category: "monitoring" },
|
||||
]
|
||||
|
||||
interface ComposeService {
|
||||
image: string
|
||||
ports?: string[]
|
||||
environment?: Record<string, string>
|
||||
networks?: string[]
|
||||
volumes?: string[]
|
||||
depends_on?: string[]
|
||||
}
|
||||
|
||||
function ServiceNode({ data }: { data: any }) {
|
||||
return (
|
||||
<div className="rounded-lg border-2 bg-card p-4 shadow-lg min-w-[200px]">
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="rounded bg-primary/10 p-2">
|
||||
<Settings2 className="h-4 w-4 text-primary" />
|
||||
</div>
|
||||
<div className="flex-1">
|
||||
<div className="font-semibold">{data.name}</div>
|
||||
<div className="text-xs text-muted-foreground">{data.image}</div>
|
||||
</div>
|
||||
</div>
|
||||
{data.ports && (
|
||||
<div className="mt-2 text-xs text-muted-foreground">
|
||||
📡 {data.ports[0]}
|
||||
</div>
|
||||
)}
|
||||
{data.networks && (
|
||||
<div className="mt-1 flex gap-1">
|
||||
{data.networks.map((net: string) => (
|
||||
<span
|
||||
key={net}
|
||||
className="rounded bg-muted px-1.5 py-0.5 text-[10px]"
|
||||
>
|
||||
{net}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
const nodeTypes: NodeTypes = {
|
||||
service: ServiceNode,
|
||||
}
|
||||
|
||||
export default function ComposeSpecPage() {
|
||||
const [projectName, setProjectName] = React.useState("my-stack")
|
||||
const [services, setServices] = React.useState<
|
||||
Record<string, ComposeService>
|
||||
>({})
|
||||
const [networks, setNetworks] = React.useState<string[]>(["default"])
|
||||
const [volumes, setVolumes] = React.useState<string[]>([])
|
||||
const [selectedService, setSelectedService] = React.useState<string | null>(
|
||||
null
|
||||
)
|
||||
|
||||
const [nodes, setNodes, onNodesChange] = useNodesState<Node>([])
|
||||
const [edges, setEdges, onEdgesChange] = useEdgesState<Edge>([])
|
||||
|
||||
// Update nodes when services change
|
||||
React.useEffect(() => {
|
||||
const newNodes: Node[] = Object.entries(services).map(
|
||||
([name, config], i) => ({
|
||||
id: name,
|
||||
type: "service",
|
||||
position: { x: 100 + (i % 4) * 250, y: 100 + Math.floor(i / 4) * 150 },
|
||||
data: { name, ...config },
|
||||
})
|
||||
)
|
||||
|
||||
const newEdges: Edge[] = []
|
||||
Object.entries(services).forEach(([name, config]) => {
|
||||
config.depends_on?.forEach((dep) => {
|
||||
newEdges.push({
|
||||
id: `${dep}-${name}`,
|
||||
source: dep,
|
||||
target: name,
|
||||
animated: true,
|
||||
style: { stroke: "hsl(var(--primary))" },
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
setNodes(newNodes)
|
||||
setEdges(newEdges)
|
||||
}, [services, setNodes, setEdges])
|
||||
|
||||
const addService = (template: (typeof COMMON_IMAGES)[0]) => {
|
||||
const serviceName = `${template.name}_${Object.keys(services).length + 1}`
|
||||
setServices({
|
||||
...services,
|
||||
[serviceName]: {
|
||||
image: template.image,
|
||||
networks: ["default"],
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
const onConnect = React.useCallback(
|
||||
(params: Connection) => {
|
||||
// Add dependency when connecting nodes
|
||||
if (params.source && params.target) {
|
||||
setServices((prev) => ({
|
||||
...prev,
|
||||
[params.target!]: {
|
||||
...prev[params.target!],
|
||||
depends_on: [
|
||||
...(prev[params.target!]?.depends_on || []),
|
||||
params.source!,
|
||||
],
|
||||
},
|
||||
}))
|
||||
}
|
||||
setEdges((eds) => addEdge(params, eds))
|
||||
},
|
||||
[setEdges]
|
||||
)
|
||||
|
||||
const generateYAML = () => {
|
||||
const compose = {
|
||||
version: "3.8",
|
||||
services,
|
||||
networks: networks.reduce((acc, net) => ({ ...acc, [net]: {} }), {}),
|
||||
volumes: volumes.reduce((acc, vol) => ({ ...acc, [vol]: {} }), {}),
|
||||
}
|
||||
return yaml.dump(compose, { indent: 2 })
|
||||
}
|
||||
|
||||
const downloadCompose = () => {
|
||||
const yamlContent = generateYAML()
|
||||
const blob = new Blob([yamlContent], { type: "text/yaml" })
|
||||
const url = URL.createObjectURL(blob)
|
||||
const a = document.createElement("a")
|
||||
a.href = url
|
||||
a.download = "docker-compose.yml"
|
||||
a.click()
|
||||
}
|
||||
|
||||
const updateServiceConfig = (
|
||||
serviceName: string,
|
||||
key: string,
|
||||
value: any
|
||||
) => {
|
||||
setServices((prev) => ({
|
||||
...prev,
|
||||
[serviceName]: {
|
||||
...prev[serviceName],
|
||||
[key]: value,
|
||||
},
|
||||
}))
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex h-screen">
|
||||
{/* Main Canvas */}
|
||||
<div className="flex-1">
|
||||
<div className="flex h-full flex-col">
|
||||
{/* Top Toolbar */}
|
||||
<div className="flex items-center justify-between border-b bg-card p-4">
|
||||
<div className="flex items-center gap-2">
|
||||
<h1 className="text-xl font-semibold">Docker Compose Editor</h1>
|
||||
<span className="rounded bg-muted px-2 py-1 text-xs">
|
||||
{Object.keys(services).length} services
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div className="flex gap-2">
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() => {
|
||||
setServices({})
|
||||
setNetworks(["default"])
|
||||
setVolumes([])
|
||||
}}
|
||||
>
|
||||
Clear
|
||||
</Button>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() => {
|
||||
const input = document.createElement("input")
|
||||
input.type = "file"
|
||||
input.accept = ".yml,.yaml"
|
||||
input.onchange = (e: any) => {
|
||||
const file = e.target.files[0]
|
||||
const reader = new FileReader()
|
||||
reader.onload = (e) => {
|
||||
const content = e.target?.result as string
|
||||
const parsed = yaml.load(content) as any
|
||||
setServices(parsed.services || {})
|
||||
setNetworks(
|
||||
Object.keys(parsed.networks || { default: {} })
|
||||
)
|
||||
setVolumes(Object.keys(parsed.volumes || {}))
|
||||
}
|
||||
reader.readAsText(file)
|
||||
}
|
||||
input.click()
|
||||
}}
|
||||
>
|
||||
Import
|
||||
</Button>
|
||||
<Sheet>
|
||||
<SheetTrigger asChild>
|
||||
<Button size="sm">
|
||||
<Plus className="mr-2 h-4 w-4" />
|
||||
Add service
|
||||
</Button>
|
||||
</SheetTrigger>
|
||||
<SheetContent>
|
||||
<SheetHeader>
|
||||
<SheetTitle>Add Service</SheetTitle>
|
||||
</SheetHeader>
|
||||
<ScrollArea className="mt-4 h-[calc(100vh-100px)]">
|
||||
<div className="space-y-2">
|
||||
{COMMON_IMAGES.map((template) => (
|
||||
<Button
|
||||
key={template.image}
|
||||
variant="outline"
|
||||
className="w-full justify-start"
|
||||
onClick={() => addService(template)}
|
||||
>
|
||||
<Plus className="mr-2 h-4 w-4" />
|
||||
<div className="flex-1 text-left">
|
||||
<div className="font-medium">{template.name}</div>
|
||||
<div className="text-xs text-muted-foreground">
|
||||
{template.image}
|
||||
</div>
|
||||
</div>
|
||||
</Button>
|
||||
))}
|
||||
</div>
|
||||
</ScrollArea>
|
||||
</SheetContent>
|
||||
</Sheet>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Canvas */}
|
||||
<div className="flex-1 bg-background">
|
||||
{Object.keys(services).length === 0 ? (
|
||||
<div className="flex h-full items-center justify-center">
|
||||
<div className="text-center">
|
||||
<Settings2 className="mx-auto h-12 w-12 text-muted-foreground/50" />
|
||||
<h3 className="mt-4 text-lg font-semibold">
|
||||
No services yet
|
||||
</h3>
|
||||
<p className="mt-2 text-sm text-muted-foreground">
|
||||
Click "Add service" to start building your stack
|
||||
</p>
|
||||
<Button
|
||||
className="mt-4"
|
||||
onClick={() => addService(COMMON_IMAGES[1])}
|
||||
>
|
||||
<Plus className="mr-2 h-4 w-4" />
|
||||
Add your first service
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<ReactFlow
|
||||
nodes={nodes}
|
||||
edges={edges}
|
||||
onNodesChange={onNodesChange}
|
||||
onEdgesChange={onEdgesChange}
|
||||
onConnect={onConnect}
|
||||
nodeTypes={nodeTypes}
|
||||
onNodeClick={(_, node) => setSelectedService(node.id)}
|
||||
fitView
|
||||
className="bg-background"
|
||||
>
|
||||
<Background
|
||||
gap={16}
|
||||
color="hsl(var(--muted-foreground) / 0.3)"
|
||||
/>
|
||||
<Controls className="border-border bg-card" />
|
||||
<Panel position="top-right" className="space-x-2">
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() =>
|
||||
setNetworks([...networks, `network_${networks.length}`])
|
||||
}
|
||||
>
|
||||
<Network className="mr-2 h-4 w-4" />
|
||||
Add network
|
||||
</Button>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() =>
|
||||
setVolumes([...volumes, `vol_${volumes.length}`])
|
||||
}
|
||||
>
|
||||
<HardDrive className="mr-2 h-4 w-4" />
|
||||
Add volume
|
||||
</Button>
|
||||
</Panel>
|
||||
</ReactFlow>
|
||||
)}
|
||||
loading: () => (
|
||||
<div className="flex h-screen items-center justify-center">
|
||||
<div className="space-y-4 w-full max-w-md">
|
||||
<div className="h-8 w-64 bg-muted animate-pulse rounded" />
|
||||
<SkeletonCard className="h-[400px]" />
|
||||
<div className="flex gap-2">
|
||||
<div className="h-10 flex-1 bg-muted animate-pulse rounded" />
|
||||
<div className="h-10 flex-1 bg-muted animate-pulse rounded" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
),
|
||||
ssr: false, // ReactFlow requires client-side rendering
|
||||
}
|
||||
)
|
||||
|
||||
{/* Right Sidebar - Configuration */}
|
||||
<div className="flex w-80 flex-col border-l bg-card">
|
||||
<div className="border-b p-4">
|
||||
<h2 className="text-lg font-semibold">Compose</h2>
|
||||
<div className="mt-4 space-y-2">
|
||||
<Label htmlFor="project-name">Project Name</Label>
|
||||
<Input
|
||||
id="project-name"
|
||||
value={projectName}
|
||||
onChange={(e) => setProjectName(e.target.value)}
|
||||
placeholder="my-stack"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Tabs defaultValue="services" className="flex-1">
|
||||
<TabsList className="w-full rounded-none">
|
||||
<TabsTrigger value="services" className="flex-1">
|
||||
<Settings2 className="mr-2 h-4 w-4" />
|
||||
Services
|
||||
</TabsTrigger>
|
||||
<TabsTrigger value="config" className="flex-1">
|
||||
<Network className="mr-2 h-4 w-4" />
|
||||
Config
|
||||
</TabsTrigger>
|
||||
</TabsList>
|
||||
|
||||
<TabsContent value="services" className="mt-0 flex-1">
|
||||
<ScrollArea className="h-[calc(100vh-220px)]">
|
||||
<div className="space-y-2 p-4">
|
||||
{selectedService ? (
|
||||
<div className="space-y-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<h3 className="font-semibold">{selectedService}</h3>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={() => setSelectedService(null)}
|
||||
>
|
||||
✕
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label>Image</Label>
|
||||
<Input
|
||||
value={services[selectedService]?.image || ""}
|
||||
onChange={(e) =>
|
||||
updateServiceConfig(
|
||||
selectedService,
|
||||
"image",
|
||||
e.target.value
|
||||
)
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label>Ports</Label>
|
||||
<Input
|
||||
placeholder="80:80"
|
||||
onBlur={(e) => {
|
||||
if (e.target.value) {
|
||||
updateServiceConfig(selectedService, "ports", [
|
||||
e.target.value,
|
||||
])
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label>Networks</Label>
|
||||
<Select
|
||||
onValueChange={(value) => {
|
||||
updateServiceConfig(selectedService, "networks", [
|
||||
value,
|
||||
])
|
||||
}}
|
||||
>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder="Select network" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{networks.map((net) => (
|
||||
<SelectItem key={net} value={net}>
|
||||
{net}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div className="text-center text-sm text-muted-foreground">
|
||||
Click a service node to configure
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</ScrollArea>
|
||||
</TabsContent>
|
||||
|
||||
<TabsContent value="config" className="mt-0 flex-1">
|
||||
<div className="space-y-4 p-4">
|
||||
<div>
|
||||
<div className="flex items-center justify-between">
|
||||
<Label>Networks</Label>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={() => {
|
||||
const name = prompt("Network name:")
|
||||
if (name) setNetworks([...networks, name])
|
||||
}}
|
||||
>
|
||||
<Plus className="h-3 w-3" />
|
||||
</Button>
|
||||
</div>
|
||||
<div className="mt-2 space-y-1">
|
||||
{networks.map((net) => (
|
||||
<div
|
||||
key={net}
|
||||
className="rounded bg-muted px-2 py-1 text-sm"
|
||||
>
|
||||
{net}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<div className="flex items-center justify-between">
|
||||
<Label>Volumes</Label>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={() => {
|
||||
const name = prompt("Volume name:")
|
||||
if (name) setVolumes([...volumes, name])
|
||||
}}
|
||||
>
|
||||
<Plus className="h-3 w-3" />
|
||||
</Button>
|
||||
</div>
|
||||
<div className="mt-2 space-y-1">
|
||||
{volumes.map((vol) => (
|
||||
<div
|
||||
key={vol}
|
||||
className="rounded bg-muted px-2 py-1 text-sm"
|
||||
>
|
||||
{vol}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</TabsContent>
|
||||
</Tabs>
|
||||
|
||||
<Separator />
|
||||
|
||||
<div className="space-y-2 p-4">
|
||||
<Button onClick={downloadCompose} className="w-full">
|
||||
<Download className="mr-2 h-4 w-4" />
|
||||
Download compose
|
||||
</Button>
|
||||
|
||||
<Sheet>
|
||||
<SheetTrigger asChild>
|
||||
<Button variant="outline" className="w-full">
|
||||
<Library className="mr-2 h-4 w-4" />
|
||||
Library
|
||||
</Button>
|
||||
</SheetTrigger>
|
||||
<SheetContent>
|
||||
<SheetHeader>
|
||||
<SheetTitle>Service Library</SheetTitle>
|
||||
</SheetHeader>
|
||||
<ScrollArea className="mt-4 h-[calc(100vh-100px)]">
|
||||
<div className="space-y-4">
|
||||
{[
|
||||
"proxy",
|
||||
"database",
|
||||
"cache",
|
||||
"runtime",
|
||||
"analytics",
|
||||
"streaming",
|
||||
"monitoring",
|
||||
].map((category) => (
|
||||
<div key={category}>
|
||||
<h4 className="mb-2 text-sm font-semibold capitalize">
|
||||
{category}
|
||||
</h4>
|
||||
<div className="space-y-2">
|
||||
{COMMON_IMAGES.filter(
|
||||
(img) => img.category === category
|
||||
).map((template) => (
|
||||
<Button
|
||||
key={template.image}
|
||||
variant="outline"
|
||||
className="w-full justify-start"
|
||||
onClick={() => {
|
||||
addService(template)
|
||||
}}
|
||||
>
|
||||
<Plus className="mr-2 h-4 w-4" />
|
||||
{template.name}
|
||||
</Button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</ScrollArea>
|
||||
</SheetContent>
|
||||
</Sheet>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
export default function ComposePage() {
|
||||
return <ComposeClient />
|
||||
}
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
import Link from "next/link"
|
||||
import { notFound } from "next/navigation"
|
||||
import { mdxComponents } from "@/mdx-components"
|
||||
import { findNeighbour } from "@hanzo/docs-core/page-tree"
|
||||
import {
|
||||
IconArrowLeft,
|
||||
IconArrowRight,
|
||||
IconArrowUpRight,
|
||||
} from "@tabler/icons-react"
|
||||
import { findNeighbour } from "fumadocs-core/server"
|
||||
|
||||
import { source } from "@/lib/source"
|
||||
import { absoluteUrl } from "@/lib/utils"
|
||||
@@ -180,10 +180,8 @@ export default async function Page(props: {
|
||||
</div>
|
||||
<div className="sticky top-[calc(var(--header-height)+1px)] z-30 ml-auto hidden h-[calc(100svh-var(--footer-height)+2rem)] w-72 flex-col gap-4 overflow-hidden overscroll-none pb-8 xl:flex">
|
||||
<div className="h-(--top-spacing) shrink-0" />
|
||||
{/* @ts-expect-error - revisit fumadocs types. */}
|
||||
{page.data.toc?.length ? (
|
||||
<div className="no-scrollbar overflow-y-auto px-8">
|
||||
{/* @ts-expect-error - revisit fumadocs types. */}
|
||||
<DocsTableOfContents toc={page.data.toc} />
|
||||
<div className="h-12" />
|
||||
</div>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { docsConfig } from "@/config/docs"
|
||||
import { DocsSidebarNav } from "@/components/sidebar-nav"
|
||||
import { DocsNav as DocsSidebarNav } from "@/components/sidebar-nav"
|
||||
import { ScrollArea } from "@/registry/default/ui/scroll-area"
|
||||
|
||||
interface DocsLayoutProps {
|
||||
@@ -12,7 +12,7 @@ export default function DocsLayout({ children }: DocsLayoutProps) {
|
||||
<div className="container flex-1 items-start md:grid md:grid-cols-[220px_minmax(0,1fr)] md:gap-6 lg:grid-cols-[240px_minmax(0,1fr)] lg:gap-10">
|
||||
<aside className="fixed top-14 z-30 hidden h-[calc(100vh-3.5rem)] w-full shrink-0 md:sticky md:block">
|
||||
<ScrollArea className="h-full py-6 px-6 lg:py-8 lg:pl-4 lg:pr-6">
|
||||
<DocsSidebarNav items={docsConfig.sidebarNav} />
|
||||
<DocsSidebarNav config={docsConfig} />
|
||||
</ScrollArea>
|
||||
</aside>
|
||||
{children}
|
||||
|
||||
@@ -1,9 +1,4 @@
|
||||
import {
|
||||
ChevronDownIcon,
|
||||
CircleIcon,
|
||||
PlusIcon,
|
||||
StarIcon,
|
||||
} from "@radix-ui/react-icons"
|
||||
import { ChevronDown, Circle, Plus, Star } from "lucide-react"
|
||||
|
||||
import { Button } from "@/registry/default/ui/button"
|
||||
import {
|
||||
@@ -37,14 +32,14 @@ export function DemoGithub() {
|
||||
</div>
|
||||
<div className="flex items-center space-x-1 rounded-md bg-secondary text-secondary-foreground">
|
||||
<Button variant="secondary" className="px-3 shadow-none">
|
||||
<StarIcon className="mr-2 h-4 w-4" />
|
||||
<Star />
|
||||
Star
|
||||
</Button>
|
||||
<Separator orientation="vertical" className="h-[20px]" />
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button variant="secondary" className="px-2 shadow-none">
|
||||
<ChevronDownIcon className="h-4 w-4 text-secondary-foreground" />
|
||||
<ChevronDown className="text-secondary-foreground" />
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent
|
||||
@@ -62,7 +57,7 @@ export function DemoGithub() {
|
||||
<DropdownMenuCheckboxItem>Inspiration</DropdownMenuCheckboxItem>
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuItem>
|
||||
<PlusIcon className="mr-2 h-4 w-4" /> Create List
|
||||
<Plus /> Create List
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
@@ -71,11 +66,11 @@ export function DemoGithub() {
|
||||
<CardContent>
|
||||
<div className="flex space-x-4 text-sm text-muted-foreground">
|
||||
<div className="flex items-center">
|
||||
<CircleIcon className="mr-1 h-3 w-3 fill-sky-400 text-sky-400" />
|
||||
<Circle className="mr-1 h-3 w-3 fill-sky-400 text-sky-400" />
|
||||
TypeScript
|
||||
</div>
|
||||
<div className="flex items-center">
|
||||
<StarIcon className="mr-1 h-3 w-3" />
|
||||
<Star className="mr-1 h-3 w-3" />
|
||||
20k
|
||||
</div>
|
||||
<div>Updated April 2023</div>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { BellIcon, EyeNoneIcon, PersonIcon } from "@radix-ui/react-icons"
|
||||
import { Bell, EyeOff, User } from "lucide-react"
|
||||
|
||||
import {
|
||||
Card,
|
||||
@@ -19,7 +19,7 @@ export function DemoNotifications() {
|
||||
</CardHeader>
|
||||
<CardContent className="grid gap-1">
|
||||
<div className="-mx-2 flex items-start space-x-4 rounded-md p-2 transition-all hover:bg-accent hover:text-accent-foreground">
|
||||
<BellIcon className="mt-px h-5 w-5" />
|
||||
<Bell className="mt-px h-5 w-5" />
|
||||
<div className="space-y-1">
|
||||
<p className="text-sm font-medium leading-none">Everything</p>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
@@ -28,7 +28,7 @@ export function DemoNotifications() {
|
||||
</div>
|
||||
</div>
|
||||
<div className="-mx-2 flex items-start space-x-4 rounded-md bg-accent p-2 text-accent-foreground transition-all">
|
||||
<PersonIcon className="mt-px h-5 w-5" />
|
||||
<User className="mt-px h-5 w-5" />
|
||||
<div className="space-y-1">
|
||||
<p className="text-sm font-medium leading-none">Available</p>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
@@ -37,7 +37,7 @@ export function DemoNotifications() {
|
||||
</div>
|
||||
</div>
|
||||
<div className="-mx-2 flex items-start space-x-4 rounded-md p-2 transition-all hover:bg-accent hover:text-accent-foreground">
|
||||
<EyeNoneIcon className="mt-px h-5 w-5" />
|
||||
<EyeOff className="mt-px h-5 w-5" />
|
||||
<div className="space-y-1">
|
||||
<p className="text-sm font-medium leading-none">Ignoring</p>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
|
||||
@@ -41,7 +41,7 @@ export function DemoShareDocument() {
|
||||
</div>
|
||||
<Separator className="my-4" />
|
||||
<div className="space-y-4">
|
||||
<h4 className="text-sm font-medium">People with access</h4>
|
||||
<div className="text-sm font-medium">People with access</div>
|
||||
<div className="grid gap-6">
|
||||
<div className="flex items-center justify-between space-x-4">
|
||||
<div className="flex items-center space-x-4">
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { ChevronDownIcon } from "@radix-ui/react-icons"
|
||||
import { ChevronDown } from "lucide-react"
|
||||
|
||||
import {
|
||||
Avatar,
|
||||
@@ -51,8 +51,7 @@ export function DemoTeamMembers() {
|
||||
<Popover>
|
||||
<PopoverTrigger asChild>
|
||||
<Button variant="outline" className="ml-auto">
|
||||
Owner{" "}
|
||||
<ChevronDownIcon className="ml-2 h-4 w-4 text-muted-foreground" />
|
||||
Owner <ChevronDown className="text-muted-foreground" />
|
||||
</Button>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent className="p-0" align="end">
|
||||
@@ -105,8 +104,7 @@ export function DemoTeamMembers() {
|
||||
<Popover>
|
||||
<PopoverTrigger asChild>
|
||||
<Button variant="outline" className="ml-auto">
|
||||
Member{" "}
|
||||
<ChevronDownIcon className="ml-2 h-4 w-4 text-muted-foreground" />
|
||||
Member <ChevronDown className="text-muted-foreground" />
|
||||
</Button>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent className="p-0" align="end">
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
"use client"
|
||||
|
||||
import * as React from "react"
|
||||
import { CalendarIcon } from "@radix-ui/react-icons"
|
||||
import { addDays, format } from "date-fns"
|
||||
import { CalendarIcon } from "lucide-react"
|
||||
import { DateRange } from "react-day-picker"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
@@ -1,11 +1,7 @@
|
||||
"use client"
|
||||
|
||||
import * as React from "react"
|
||||
import {
|
||||
CaretSortIcon,
|
||||
CheckIcon,
|
||||
PlusCircledIcon,
|
||||
} from "@radix-ui/react-icons"
|
||||
import { Check, ChevronsUpDown, PlusCircle } from "lucide-react"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
import {
|
||||
@@ -105,13 +101,13 @@ export default function TeamSwitcher({ className }: TeamSwitcherProps) {
|
||||
<AvatarFallback>SC</AvatarFallback>
|
||||
</Avatar>
|
||||
{selectedTeam.label}
|
||||
<CaretSortIcon className="ml-auto h-4 w-4 shrink-0 opacity-50" />
|
||||
<ChevronsUpDown className="ml-auto opacity-50" />
|
||||
</Button>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent className="w-[200px] p-0">
|
||||
<Command>
|
||||
<CommandInput placeholder="Search team..." />
|
||||
<CommandList>
|
||||
<CommandInput placeholder="Search team..." />
|
||||
<CommandEmpty>No team found.</CommandEmpty>
|
||||
{groups.map((group) => (
|
||||
<CommandGroup key={group.label} heading={group.label}>
|
||||
@@ -133,9 +129,9 @@ export default function TeamSwitcher({ className }: TeamSwitcherProps) {
|
||||
<AvatarFallback>SC</AvatarFallback>
|
||||
</Avatar>
|
||||
{team.label}
|
||||
<CheckIcon
|
||||
<Check
|
||||
className={cn(
|
||||
"ml-auto h-4 w-4",
|
||||
"ml-auto",
|
||||
selectedTeam.value === team.value
|
||||
? "opacity-100"
|
||||
: "opacity-0"
|
||||
@@ -156,7 +152,7 @@ export default function TeamSwitcher({ className }: TeamSwitcherProps) {
|
||||
setShowNewTeamDialog(true)
|
||||
}}
|
||||
>
|
||||
<PlusCircledIcon className="mr-2 h-5 w-5" />
|
||||
<PlusCircle className="h-5 w-5" />
|
||||
Create Team
|
||||
</CommandItem>
|
||||
</DialogTrigger>
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
"use client"
|
||||
|
||||
import { zodResolver } from "@hookform/resolvers/zod"
|
||||
import { CalendarIcon, CaretSortIcon, CheckIcon } from "@radix-ui/react-icons"
|
||||
import { format } from "date-fns"
|
||||
import { CalendarIcon, Check, ChevronsUpDown } from "lucide-react"
|
||||
import { useForm } from "react-hook-form"
|
||||
import { z } from "zod"
|
||||
|
||||
@@ -15,6 +15,7 @@ import {
|
||||
CommandGroup,
|
||||
CommandInput,
|
||||
CommandItem,
|
||||
CommandList,
|
||||
} from "@/registry/default/ui/command"
|
||||
import {
|
||||
Form,
|
||||
@@ -169,35 +170,37 @@ export function AccountForm() {
|
||||
(language) => language.value === field.value
|
||||
)?.label
|
||||
: "Select language"}
|
||||
<CaretSortIcon className="ml-2 h-4 w-4 shrink-0 opacity-50" />
|
||||
<ChevronsUpDown className="opacity-50" />
|
||||
</Button>
|
||||
</FormControl>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent className="w-[200px] p-0">
|
||||
<Command>
|
||||
<CommandInput placeholder="Search language..." />
|
||||
<CommandEmpty>No language found.</CommandEmpty>
|
||||
<CommandGroup>
|
||||
{languages.map((language) => (
|
||||
<CommandItem
|
||||
value={language.label}
|
||||
key={language.value}
|
||||
onSelect={() => {
|
||||
form.setValue("language", language.value)
|
||||
}}
|
||||
>
|
||||
<CheckIcon
|
||||
className={cn(
|
||||
"mr-2 h-4 w-4",
|
||||
language.value === field.value
|
||||
? "opacity-100"
|
||||
: "opacity-0"
|
||||
)}
|
||||
/>
|
||||
{language.label}
|
||||
</CommandItem>
|
||||
))}
|
||||
</CommandGroup>
|
||||
<CommandList>
|
||||
<CommandEmpty>No language found.</CommandEmpty>
|
||||
<CommandGroup>
|
||||
{languages.map((language) => (
|
||||
<CommandItem
|
||||
value={language.label}
|
||||
key={language.value}
|
||||
onSelect={() => {
|
||||
form.setValue("language", language.value)
|
||||
}}
|
||||
>
|
||||
<Check
|
||||
className={cn(
|
||||
"mr-2",
|
||||
language.value === field.value
|
||||
? "opacity-100"
|
||||
: "opacity-0"
|
||||
)}
|
||||
/>
|
||||
{language.label}
|
||||
</CommandItem>
|
||||
))}
|
||||
</CommandGroup>
|
||||
</CommandList>
|
||||
</Command>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
"use client"
|
||||
|
||||
import { zodResolver } from "@hookform/resolvers/zod"
|
||||
import { ChevronDownIcon } from "@radix-ui/react-icons"
|
||||
import { ChevronDown } from "lucide-react"
|
||||
import { useForm } from "react-hook-form"
|
||||
import { z } from "zod"
|
||||
|
||||
@@ -73,7 +73,7 @@ export function AppearanceForm() {
|
||||
<option value="system">System</option>
|
||||
</select>
|
||||
</FormControl>
|
||||
<ChevronDownIcon className="absolute right-3 top-2.5 h-4 w-4 opacity-50" />
|
||||
<ChevronDown className="absolute right-3 top-2.5 h-4 w-4 opacity-50" />
|
||||
</div>
|
||||
<FormDescription>
|
||||
Set the font you want to use in the dashboard.
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import Image from "next/image"
|
||||
import { PlusCircledIcon } from "@radix-ui/react-icons"
|
||||
import { PlusCircle } from "lucide-react"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
import {
|
||||
@@ -45,6 +45,7 @@ export function AlbumArtwork({
|
||||
"h-auto w-auto object-cover transition-all hover:scale-105",
|
||||
aspectRatio === "portrait" ? "aspect-[3/4]" : "aspect-square"
|
||||
)}
|
||||
loading="lazy"
|
||||
/>
|
||||
</div>
|
||||
</ContextMenuTrigger>
|
||||
@@ -54,7 +55,7 @@ export function AlbumArtwork({
|
||||
<ContextMenuSubTrigger>Add to Playlist</ContextMenuSubTrigger>
|
||||
<ContextMenuSubContent className="w-48">
|
||||
<ContextMenuItem>
|
||||
<PlusCircledIcon className="mr-2 h-4 w-4" />
|
||||
<PlusCircle className="mr-2 h-4 w-4" />
|
||||
New Playlist
|
||||
</ContextMenuItem>
|
||||
<ContextMenuSeparator />
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { Metadata } from "next"
|
||||
import Image from "next/image"
|
||||
import { PlusCircledIcon } from "@radix-ui/react-icons"
|
||||
import { PlusCircle } from "lucide-react"
|
||||
|
||||
import { Button } from "@/registry/default/ui/button"
|
||||
import { ScrollArea, ScrollBar } from "@/registry/default/ui/scroll-area"
|
||||
@@ -64,7 +64,7 @@ export default function MusicPage() {
|
||||
</TabsList>
|
||||
<div className="ml-auto mr-4">
|
||||
<Button>
|
||||
<PlusCircledIcon className="mr-2 h-4 w-4" />
|
||||
<PlusCircle />
|
||||
Add music
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
@@ -71,13 +71,15 @@ export default function ExamplesPage() {
|
||||
<div className="aspect-video overflow-hidden">
|
||||
<img
|
||||
src={example.image}
|
||||
alt={example.name}
|
||||
alt={`${example.name} - Light mode preview`}
|
||||
className="block h-full w-full object-cover transition-transform group-hover:scale-105 dark:hidden"
|
||||
loading="lazy"
|
||||
/>
|
||||
<img
|
||||
src={example.imageDark}
|
||||
alt={example.name}
|
||||
alt={`${example.name} - Dark mode preview`}
|
||||
className="hidden h-full w-full object-cover transition-transform group-hover:scale-105 dark:block"
|
||||
loading="lazy"
|
||||
/>
|
||||
</div>
|
||||
<div className="p-4">
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
"use client"
|
||||
|
||||
import * as React from "react"
|
||||
import { CaretSortIcon, CheckIcon } from "@radix-ui/react-icons"
|
||||
import { PopoverProps } from "@radix-ui/react-popover"
|
||||
import { Check, ChevronsUpDown } from "lucide-react"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
import { useMutationObserver } from "@/hooks/use-mutation-observer"
|
||||
@@ -64,7 +64,7 @@ export function ModelSelector({ models, types, ...props }: ModelSelectorProps) {
|
||||
className="w-full justify-between"
|
||||
>
|
||||
{selectedModel ? selectedModel.name : "Select a model..."}
|
||||
<CaretSortIcon className="ml-2 h-4 w-4 shrink-0 opacity-50" />
|
||||
<ChevronsUpDown className="opacity-50" />
|
||||
</Button>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent align="end" className="w-[250px] p-0">
|
||||
@@ -135,13 +135,15 @@ function ModelItem({ model, isSelected, onSelect, onPeek }: ModelItemProps) {
|
||||
const ref = React.useRef<HTMLDivElement>(null)
|
||||
|
||||
useMutationObserver(ref, (mutations) => {
|
||||
for (const mutation of mutations) {
|
||||
if (mutation.type === "attributes") {
|
||||
if (mutation.attributeName === "aria-selected") {
|
||||
onPeek(model)
|
||||
}
|
||||
mutations.forEach((mutation) => {
|
||||
if (
|
||||
mutation.type === "attributes" &&
|
||||
mutation.attributeName === "aria-selected" &&
|
||||
ref.current?.getAttribute("aria-selected") === "true"
|
||||
) {
|
||||
onPeek(model)
|
||||
}
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
return (
|
||||
@@ -152,7 +154,7 @@ function ModelItem({ model, isSelected, onSelect, onPeek }: ModelItemProps) {
|
||||
className="aria-selected:bg-primary aria-selected:text-primary-foreground"
|
||||
>
|
||||
{model.name}
|
||||
<CheckIcon
|
||||
<Check
|
||||
className={cn(
|
||||
"ml-auto h-4 w-4",
|
||||
isSelected ? "opacity-100" : "opacity-0"
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
import * as React from "react"
|
||||
import { Dialog } from "@radix-ui/react-dialog"
|
||||
import { DotsHorizontalIcon } from "@radix-ui/react-icons"
|
||||
import { MoreHorizontal } from "lucide-react"
|
||||
|
||||
import {
|
||||
AlertDialog,
|
||||
@@ -40,9 +40,9 @@ export function PresetActions() {
|
||||
<>
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button variant="secondary">
|
||||
<Button variant="secondary" size="icon">
|
||||
<span className="sr-only">Actions</span>
|
||||
<DotsHorizontalIcon className="h-4 w-4" />
|
||||
<MoreHorizontal />
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="end">
|
||||
|
||||
@@ -2,8 +2,8 @@
|
||||
|
||||
import * as React from "react"
|
||||
import { useRouter } from "next/navigation"
|
||||
import { CaretSortIcon, CheckIcon } from "@radix-ui/react-icons"
|
||||
import { PopoverProps } from "@radix-ui/react-popover"
|
||||
import { Check, ChevronsUpDown } from "lucide-react"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
import { Button } from "@/registry/default/ui/button"
|
||||
@@ -13,6 +13,7 @@ import {
|
||||
CommandGroup,
|
||||
CommandInput,
|
||||
CommandItem,
|
||||
CommandList,
|
||||
} from "@/registry/default/ui/command"
|
||||
import {
|
||||
Popover,
|
||||
@@ -42,39 +43,41 @@ export function PresetSelector({ presets, ...props }: PresetSelectorProps) {
|
||||
className="flex-1 justify-between md:max-w-[200px] lg:max-w-[300px]"
|
||||
>
|
||||
{selectedPreset ? selectedPreset.name : "Load a preset..."}
|
||||
<CaretSortIcon className="ml-2 h-4 w-4 shrink-0 opacity-50" />
|
||||
<ChevronsUpDown className="opacity-50" />
|
||||
</Button>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent className="w-[300px] p-0">
|
||||
<Command>
|
||||
<CommandInput placeholder="Search presets..." />
|
||||
<CommandEmpty>No presets found.</CommandEmpty>
|
||||
<CommandGroup heading="Examples">
|
||||
{presets.map((preset) => (
|
||||
<CommandItem
|
||||
key={preset.id}
|
||||
onSelect={() => {
|
||||
setSelectedPreset(preset)
|
||||
setOpen(false)
|
||||
}}
|
||||
>
|
||||
{preset.name}
|
||||
<CheckIcon
|
||||
className={cn(
|
||||
"ml-auto h-4 w-4",
|
||||
selectedPreset?.id === preset.id
|
||||
? "opacity-100"
|
||||
: "opacity-0"
|
||||
)}
|
||||
/>
|
||||
<CommandList>
|
||||
<CommandEmpty>No presets found.</CommandEmpty>
|
||||
<CommandGroup heading="Examples">
|
||||
{presets.map((preset) => (
|
||||
<CommandItem
|
||||
key={preset.id}
|
||||
onSelect={() => {
|
||||
setSelectedPreset(preset)
|
||||
setOpen(false)
|
||||
}}
|
||||
>
|
||||
{preset.name}
|
||||
<Check
|
||||
className={cn(
|
||||
"ml-auto",
|
||||
selectedPreset?.id === preset.id
|
||||
? "opacity-100"
|
||||
: "opacity-0"
|
||||
)}
|
||||
/>
|
||||
</CommandItem>
|
||||
))}
|
||||
</CommandGroup>
|
||||
<CommandGroup className="pt-0">
|
||||
<CommandItem onSelect={() => router.push("/examples")}>
|
||||
More examples
|
||||
</CommandItem>
|
||||
))}
|
||||
</CommandGroup>
|
||||
<CommandGroup className="pt-0">
|
||||
<CommandItem onSelect={() => router.push("/examples")}>
|
||||
More examples
|
||||
</CommandItem>
|
||||
</CommandGroup>
|
||||
</CommandGroup>
|
||||
</CommandList>
|
||||
</Command>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { CopyIcon } from "@radix-ui/react-icons"
|
||||
import { Copy } from "lucide-react"
|
||||
|
||||
import { Button } from "@/registry/default/ui/button"
|
||||
import { Input } from "@/registry/default/ui/input"
|
||||
@@ -37,7 +37,7 @@ export function PresetShare() {
|
||||
</div>
|
||||
<Button type="submit" size="sm" className="px-3">
|
||||
<span className="sr-only">Copy</span>
|
||||
<CopyIcon className="h-4 w-4" />
|
||||
<Copy />
|
||||
</Button>
|
||||
</div>
|
||||
</PopoverContent>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { Metadata } from "next"
|
||||
import Image from "next/image"
|
||||
import { CounterClockwiseClockIcon } from "@radix-ui/react-icons"
|
||||
import { RotateCcw } from "lucide-react"
|
||||
|
||||
import { Button } from "@/registry/default/ui/button"
|
||||
import {
|
||||
@@ -265,7 +265,7 @@ export default function PlaygroundPage() {
|
||||
<Button>Submit</Button>
|
||||
<Button variant="secondary">
|
||||
<span className="sr-only">Show history</span>
|
||||
<CounterClockwiseClockIcon className="h-4 w-4" />
|
||||
<RotateCcw />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -283,7 +283,7 @@ export default function PlaygroundPage() {
|
||||
<Button>Submit</Button>
|
||||
<Button variant="secondary">
|
||||
<span className="sr-only">Show history</span>
|
||||
<CounterClockwiseClockIcon className="h-4 w-4" />
|
||||
<RotateCcw />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -314,7 +314,7 @@ export default function PlaygroundPage() {
|
||||
<Button>Submit</Button>
|
||||
<Button variant="secondary">
|
||||
<span className="sr-only">Show history</span>
|
||||
<CounterClockwiseClockIcon className="h-4 w-4" />
|
||||
<RotateCcw />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
+20
-203
@@ -1,24 +1,8 @@
|
||||
"use client"
|
||||
|
||||
import { useState } from "react"
|
||||
import {
|
||||
AdvancedChart,
|
||||
CompanyProfile,
|
||||
CryptoScreener,
|
||||
Financials,
|
||||
ForexScreener,
|
||||
MarketOverview,
|
||||
NewsTimeline,
|
||||
OrderEntry,
|
||||
OrdersHistory,
|
||||
PositionsList,
|
||||
StockScreener,
|
||||
SymbolInfo,
|
||||
TechnicalAnalysis,
|
||||
TickerTape,
|
||||
TradingPanel,
|
||||
} from "@hanzo/ui/finance"
|
||||
|
||||
import AdvancedChart from "@/registry/default/ui/advanced-chart"
|
||||
import {
|
||||
Card,
|
||||
CardContent,
|
||||
@@ -27,6 +11,20 @@ import {
|
||||
CardTitle,
|
||||
} from "@/registry/default/ui/card"
|
||||
import { Separator } from "@/registry/default/ui/separator"
|
||||
// import { CompanyProfile } from "@/registry/default/ui/company-profile" // TODO: Create this component
|
||||
// import { CryptoScreener } from "@/registry/default/ui/crypto-screener" // TODO: Create this component
|
||||
// import { Financials } from "@/registry/default/ui/financials" // TODO: Create this component
|
||||
// import { ForexScreener } from "@/registry/default/ui/forex-screener" // TODO: Create this component
|
||||
// import { MarketOverview } from "@/registry/default/ui/market-overview" // TODO: Create this component
|
||||
// import { NewsTimeline } from "@/registry/default/ui/news-timeline" // TODO: Create this component
|
||||
// import { OrderEntry } from "@/registry/default/ui/order-entry" // TODO: Create this component
|
||||
// import { OrdersHistory } from "@/registry/default/ui/orders-history" // TODO: Create this component
|
||||
// import { PositionsList } from "@/registry/default/ui/positions-list" // TODO: Create this component
|
||||
import StockScreener from "@/registry/default/ui/stock-screener"
|
||||
// import { SymbolInfo } from "@/registry/default/ui/symbol-info" // TODO: Create this component
|
||||
// import { TechnicalAnalysis } from "@/registry/default/ui/technical-analysis" // TODO: Create this component
|
||||
// import { TickerTape } from "@/registry/default/ui/ticker-tape" // TODO: Create this component
|
||||
import { TradingPanel } from "@/registry/default/ui/trading-panel"
|
||||
|
||||
export default function FinancePage() {
|
||||
const [positions, setPositions] = useState([
|
||||
@@ -113,31 +111,7 @@ export default function FinancePage() {
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Ticker Tape</CardTitle>
|
||||
<CardDescription>
|
||||
Scrolling ticker with major market indices
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<TickerTape />
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Market Overview</CardTitle>
|
||||
<CardDescription>
|
||||
Multi-asset market overview with tabs
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div style={{ height: "450px" }}>
|
||||
<MarketOverview />
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
{/* TODO: Add TickerTape and MarketOverview components */}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
@@ -148,11 +122,11 @@ export default function FinancePage() {
|
||||
<div>
|
||||
<h2 className="text-2xl font-semibold">Market Screeners</h2>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
Asset screeners for stocks, crypto, and forex
|
||||
Asset screeners for stocks (crypto and forex coming soon)
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="grid md:grid-cols-2 lg:grid-cols-3 gap-6">
|
||||
<div className="grid gap-6">
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Stock Screener</CardTitle>
|
||||
@@ -163,129 +137,18 @@ export default function FinancePage() {
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Crypto Screener</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div style={{ height: "400px" }}>
|
||||
<CryptoScreener />
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Forex Screener</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div style={{ height: "400px" }}>
|
||||
<ForexScreener />
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<Separator />
|
||||
|
||||
{/* Symbol Analysis */}
|
||||
<section className="space-y-4">
|
||||
<div>
|
||||
<h2 className="text-2xl font-semibold">Symbol Analysis</h2>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
Comprehensive symbol information and analysis tools
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="grid gap-6">
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Symbol Info</CardTitle>
|
||||
<CardDescription>
|
||||
Real-time symbol details and price information
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div style={{ height: "140px" }}>
|
||||
<SymbolInfo symbol="NASDAQ:AAPL" />
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<div className="grid md:grid-cols-2 gap-6">
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Company Profile</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div style={{ height: "400px" }}>
|
||||
<CompanyProfile symbol="NASDAQ:AAPL" />
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Technical Analysis</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div style={{ height: "400px" }}>
|
||||
<TechnicalAnalysis symbol="NASDAQ:AAPL" />
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Financials</CardTitle>
|
||||
<CardDescription>
|
||||
Financial statements and fundamental data
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div style={{ height: "500px" }}>
|
||||
<Financials symbol="NASDAQ:AAPL" />
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<Separator />
|
||||
|
||||
{/* News */}
|
||||
<section className="space-y-4">
|
||||
<div>
|
||||
<h2 className="text-2xl font-semibold">News & Timeline</h2>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
Financial news feed and updates
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>News Timeline</CardTitle>
|
||||
<CardDescription>Real-time financial news feed</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div style={{ height: "400px" }}>
|
||||
<NewsTimeline />
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</section>
|
||||
|
||||
<Separator />
|
||||
|
||||
{/* Trading Interface */}
|
||||
<section className="space-y-4">
|
||||
<div>
|
||||
<h2 className="text-2xl font-semibold">Trading Interface</h2>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
Order entry, positions, and order management components
|
||||
Trading panel component (order entry and position management
|
||||
coming soon)
|
||||
</p>
|
||||
</div>
|
||||
|
||||
@@ -301,52 +164,6 @@ export default function FinancePage() {
|
||||
<TradingPanel symbol="NASDAQ:AAPL" currentPrice={175} />
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<div className="grid lg:grid-cols-3 gap-6">
|
||||
<Card className="lg:col-span-1">
|
||||
<CardHeader>
|
||||
<CardTitle>Order Entry</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<OrderEntry
|
||||
symbol="NASDAQ:AAPL"
|
||||
accountBalance={10000}
|
||||
onPlaceOrder={handlePlaceOrder}
|
||||
/>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card className="lg:col-span-2">
|
||||
<CardHeader>
|
||||
<CardTitle>Positions</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<PositionsList
|
||||
positions={positions}
|
||||
onPositionClick={() => {}}
|
||||
/>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Orders History</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<OrdersHistory
|
||||
orders={orders}
|
||||
onOrderClick={() => {}}
|
||||
onCancelOrder={(id: string) => {
|
||||
setOrders(
|
||||
orders.map((o) =>
|
||||
o.id === id ? { ...o, status: "cancelled" } : o
|
||||
)
|
||||
)
|
||||
}}
|
||||
/>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
@@ -9,7 +9,9 @@ export default function AppLayout({ children }: AppLayoutProps) {
|
||||
return (
|
||||
<>
|
||||
<SiteHeader />
|
||||
<main className="flex-1">{children}</main>
|
||||
<main id="main-content" className="flex-1">
|
||||
{children}
|
||||
</main>
|
||||
<SiteFooter />
|
||||
</>
|
||||
)
|
||||
|
||||
+74
-20
@@ -1,13 +1,7 @@
|
||||
import { Metadata } from "next"
|
||||
import Link from "next/link"
|
||||
import { ArrowRight, CheckCircle2 } from "lucide-react"
|
||||
|
||||
import { Announcement } from "@/components/announcement"
|
||||
import {
|
||||
PageActions,
|
||||
PageHeader,
|
||||
PageHeaderDescription,
|
||||
PageHeaderHeading,
|
||||
} from "@/components/page-header"
|
||||
import { Button } from "@/registry/default/ui/button"
|
||||
|
||||
import { HomeContent } from "./home-content"
|
||||
@@ -46,19 +40,79 @@ export const metadata: Metadata = {
|
||||
export default function IndexPage() {
|
||||
return (
|
||||
<div className="flex flex-1 flex-col">
|
||||
<PageHeader>
|
||||
<Announcement />
|
||||
<PageHeaderHeading className="max-w-4xl">{title}</PageHeaderHeading>
|
||||
<PageHeaderDescription>{description}</PageHeaderDescription>
|
||||
<PageActions>
|
||||
<Button asChild size="sm">
|
||||
<Link href="/docs/installation">Get Started</Link>
|
||||
</Button>
|
||||
<Button asChild size="sm" variant="ghost">
|
||||
<Link href="/docs/components">View Components</Link>
|
||||
</Button>
|
||||
</PageActions>
|
||||
</PageHeader>
|
||||
{/* Enhanced Hero Section */}
|
||||
<section className="relative overflow-hidden">
|
||||
{/* Animated gradient background */}
|
||||
<div className="absolute inset-0 -z-10">
|
||||
<div className="absolute inset-0 bg-gradient-to-br from-primary/10 via-transparent to-accent/10 animate-gradient bg-[length:200%_200%]" />
|
||||
<div className="absolute inset-0 bg-grid-white/[0.02]" />
|
||||
</div>
|
||||
|
||||
{/* Hero content */}
|
||||
<div className="container py-16 md:py-24 lg:py-32 space-y-8 animate-fade-in-up">
|
||||
{/* Animated badge with pulse indicator */}
|
||||
<div className="flex justify-center">
|
||||
<div className="inline-flex items-center gap-2 px-3 py-1 rounded-full bg-muted text-sm font-medium border">
|
||||
<span className="relative flex h-2 w-2">
|
||||
<span className="animate-ping absolute inline-flex h-full w-full rounded-full bg-primary opacity-75" />
|
||||
<span className="relative inline-flex rounded-full h-2 w-2 bg-primary" />
|
||||
</span>
|
||||
New: 150+ components, Page Builder, and more
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Main heading with gradient text */}
|
||||
<h1 className="text-center text-4xl sm:text-5xl md:text-6xl lg:text-7xl font-bold tracking-tight leading-tight">
|
||||
<span className="bg-clip-text text-transparent bg-gradient-to-r from-foreground via-foreground to-foreground/70">
|
||||
{title}
|
||||
</span>
|
||||
</h1>
|
||||
|
||||
{/* Subtitle */}
|
||||
<p className="text-center text-lg sm:text-xl md:text-2xl text-muted-foreground max-w-3xl mx-auto leading-relaxed px-4">
|
||||
{description}
|
||||
</p>
|
||||
|
||||
{/* CTAs with clear hierarchy - larger and more prominent */}
|
||||
<div className="flex flex-col sm:flex-row gap-4 justify-center items-center pt-4">
|
||||
<Button
|
||||
asChild
|
||||
size="lg"
|
||||
className="gap-2 text-base md:text-lg h-11 md:h-12 px-6 md:px-8 min-w-[160px] shadow-lg hover:shadow-xl transition-shadow"
|
||||
>
|
||||
<Link href="/docs/installation">
|
||||
Get Started
|
||||
<ArrowRight className="h-4 w-4 md:h-5 md:w-5" />
|
||||
</Link>
|
||||
</Button>
|
||||
<Button
|
||||
asChild
|
||||
variant="outline"
|
||||
size="lg"
|
||||
className="text-base md:text-lg h-11 md:h-12 px-6 md:px-8 min-w-[160px]"
|
||||
>
|
||||
<Link href="/docs/components">View Components</Link>
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{/* Trust indicators */}
|
||||
<div className="flex flex-wrap justify-center gap-6 md:gap-8 text-sm md:text-base text-muted-foreground pt-4">
|
||||
<div className="flex items-center gap-2">
|
||||
<CheckCircle2 className="h-4 w-4 md:h-5 md:w-5 text-primary" />
|
||||
<span>Open Source</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<CheckCircle2 className="h-4 w-4 md:h-5 md:w-5 text-primary" />
|
||||
<span>TypeScript</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<CheckCircle2 className="h-4 w-4 md:h-5 md:w-5 text-primary" />
|
||||
<span>Accessible</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<div className="container-wrapper section-soft flex-1 pb-6">
|
||||
<div className="container overflow-hidden">
|
||||
<section className="theme-container">
|
||||
|
||||
@@ -0,0 +1,802 @@
|
||||
"use client"
|
||||
|
||||
import * as React from "react"
|
||||
import {
|
||||
Check,
|
||||
Copy,
|
||||
Download,
|
||||
Monitor,
|
||||
Moon,
|
||||
Palette,
|
||||
RotateCcw,
|
||||
Settings2,
|
||||
Shuffle,
|
||||
Sparkles,
|
||||
Sun,
|
||||
} from "lucide-react"
|
||||
import { HexColorInput, HexColorPicker } from "react-colorful"
|
||||
import { toast } from "sonner"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
import {
|
||||
Alert,
|
||||
AlertDescription,
|
||||
AlertTitle,
|
||||
} from "@/registry/default/ui/alert"
|
||||
import { Badge } from "@/registry/default/ui/badge"
|
||||
import { Button } from "@/registry/default/ui/button"
|
||||
import {
|
||||
Card,
|
||||
CardContent,
|
||||
CardDescription,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
} from "@/registry/default/ui/card"
|
||||
import { Checkbox } from "@/registry/default/ui/checkbox"
|
||||
import { Input } from "@/registry/default/ui/input"
|
||||
import { Label } from "@/registry/default/ui/label"
|
||||
import {
|
||||
Popover,
|
||||
PopoverContent,
|
||||
PopoverTrigger,
|
||||
} from "@/registry/default/ui/popover"
|
||||
import { Progress } from "@/registry/default/ui/progress"
|
||||
import { RadioGroup, RadioGroupItem } from "@/registry/default/ui/radio-group"
|
||||
import { ScrollArea } from "@/registry/default/ui/scroll-area"
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from "@/registry/default/ui/select"
|
||||
import { Separator } from "@/registry/default/ui/separator"
|
||||
import { Slider } from "@/registry/default/ui/slider"
|
||||
import { Switch } from "@/registry/default/ui/switch"
|
||||
import {
|
||||
Tabs,
|
||||
TabsContent,
|
||||
TabsList,
|
||||
TabsTrigger,
|
||||
} from "@/registry/default/ui/tabs"
|
||||
import { Textarea } from "@/registry/default/ui/textarea"
|
||||
|
||||
const DEFAULT_LIGHT_COLORS = {
|
||||
background: "#ffffff",
|
||||
foreground: "#0a0a0a",
|
||||
card: "#ffffff",
|
||||
"card-foreground": "#0a0a0a",
|
||||
popover: "#ffffff",
|
||||
"popover-foreground": "#0a0a0a",
|
||||
primary: "#0099ff",
|
||||
"primary-foreground": "#ffffff",
|
||||
secondary: "#f4f4f5",
|
||||
"secondary-foreground": "#0a0a0a",
|
||||
muted: "#f4f4f5",
|
||||
"muted-foreground": "#71717a",
|
||||
accent: "#f4f4f5",
|
||||
"accent-foreground": "#0a0a0a",
|
||||
destructive: "#ef4444",
|
||||
"destructive-foreground": "#ffffff",
|
||||
border: "#e4e4e7",
|
||||
input: "#e4e4e7",
|
||||
ring: "#0099ff",
|
||||
}
|
||||
|
||||
const DEFAULT_DARK_COLORS = {
|
||||
background: "#0a0a0a",
|
||||
foreground: "#fafafa",
|
||||
card: "#0a0a0a",
|
||||
"card-foreground": "#fafafa",
|
||||
popover: "#0a0a0a",
|
||||
"popover-foreground": "#fafafa",
|
||||
primary: "#0099ff",
|
||||
"primary-foreground": "#ffffff",
|
||||
secondary: "#262626",
|
||||
"secondary-foreground": "#fafafa",
|
||||
muted: "#262626",
|
||||
"muted-foreground": "#a3a3a3",
|
||||
accent: "#262626",
|
||||
"accent-foreground": "#fafafa",
|
||||
destructive: "#dc2626",
|
||||
"destructive-foreground": "#ffffff",
|
||||
border: "#262626",
|
||||
input: "#262626",
|
||||
ring: "#0099ff",
|
||||
}
|
||||
|
||||
const PRESET_THEMES = {
|
||||
"Electric Blue": {
|
||||
primary: "#0099ff",
|
||||
secondary: "#1e293b",
|
||||
accent: "#06b6d4",
|
||||
},
|
||||
Forest: {
|
||||
primary: "#10b981",
|
||||
secondary: "#065f46",
|
||||
accent: "#34d399",
|
||||
},
|
||||
Sunset: {
|
||||
primary: "#f97316",
|
||||
secondary: "#dc2626",
|
||||
accent: "#fbbf24",
|
||||
},
|
||||
"Purple Haze": {
|
||||
primary: "#8b5cf6",
|
||||
secondary: "#7c3aed",
|
||||
accent: "#a78bfa",
|
||||
},
|
||||
Ocean: {
|
||||
primary: "#0ea5e9",
|
||||
secondary: "#0284c7",
|
||||
accent: "#38bdf8",
|
||||
},
|
||||
}
|
||||
|
||||
export default function ThemeGeneratorPage() {
|
||||
const [colors, setColors] = React.useState(DEFAULT_LIGHT_COLORS)
|
||||
const [radius, setRadius] = React.useState(0.5)
|
||||
const [mode, setMode] = React.useState<"light" | "dark">("light")
|
||||
const [copied, setCopied] = React.useState(false)
|
||||
const [activeColorKey, setActiveColorKey] = React.useState<string | null>(
|
||||
null
|
||||
)
|
||||
|
||||
const updateColor = (key: string, value: string) => {
|
||||
setColors((prev) => ({ ...prev, [key]: value }))
|
||||
}
|
||||
|
||||
const toggleMode = () => {
|
||||
const newMode = mode === "light" ? "dark" : "light"
|
||||
setMode(newMode)
|
||||
setColors(newMode === "light" ? DEFAULT_LIGHT_COLORS : DEFAULT_DARK_COLORS)
|
||||
}
|
||||
|
||||
const hexToHSL = (hex: string) => {
|
||||
const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex)
|
||||
if (!result) return "0 0% 0%"
|
||||
|
||||
const r = parseInt(result[1], 16) / 255
|
||||
const g = parseInt(result[2], 16) / 255
|
||||
const b = parseInt(result[3], 16) / 255
|
||||
|
||||
const max = Math.max(r, g, b)
|
||||
const min = Math.min(r, g, b)
|
||||
let h = 0,
|
||||
s = 0,
|
||||
l = (max + min) / 2
|
||||
|
||||
if (max !== min) {
|
||||
const d = max - min
|
||||
s = l > 0.5 ? d / (2 - max - min) : d / (max + min)
|
||||
switch (max) {
|
||||
case r:
|
||||
h = ((g - b) / d + (g < b ? 6 : 0)) / 6
|
||||
break
|
||||
case g:
|
||||
h = ((b - r) / d + 2) / 6
|
||||
break
|
||||
case b:
|
||||
h = ((r - g) / d + 4) / 6
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
return `${Math.round(h * 360)} ${Math.round(s * 100)}% ${Math.round(l * 100)}%`
|
||||
}
|
||||
|
||||
const generateCSS = () => {
|
||||
const css = `@layer base {
|
||||
:root {
|
||||
${Object.entries(colors)
|
||||
.map(([key, value]) => ` --${key}: ${hexToHSL(value)};`)
|
||||
.join("\n")}
|
||||
--radius: ${radius}rem;
|
||||
}
|
||||
|
||||
.dark {
|
||||
${Object.entries(colors)
|
||||
.map(([key, value]) => ` --${key}: ${hexToHSL(value)};`)
|
||||
.join("\n")}
|
||||
}
|
||||
}`
|
||||
return css
|
||||
}
|
||||
|
||||
const copyCSS = async () => {
|
||||
try {
|
||||
await navigator.clipboard.writeText(generateCSS())
|
||||
setCopied(true)
|
||||
setTimeout(() => setCopied(false), 2000)
|
||||
toast.success("CSS copied to clipboard")
|
||||
} catch (err) {
|
||||
console.error("Failed to copy CSS:", err)
|
||||
toast.error("Failed to copy CSS", {
|
||||
description: "Please try again",
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
const applyPresetTheme = (presetName: string) => {
|
||||
const preset = PRESET_THEMES[presetName as keyof typeof PRESET_THEMES]
|
||||
if (preset) {
|
||||
setColors((prev) => ({
|
||||
...prev,
|
||||
primary: preset.primary,
|
||||
"primary-foreground": mode === "dark" ? "#ffffff" : "#ffffff",
|
||||
secondary: preset.secondary,
|
||||
"secondary-foreground": mode === "dark" ? "#fafafa" : "#0a0a0a",
|
||||
accent: preset.accent,
|
||||
"accent-foreground": mode === "dark" ? "#fafafa" : "#0a0a0a",
|
||||
ring: preset.primary,
|
||||
}))
|
||||
}
|
||||
}
|
||||
|
||||
const randomizeTheme = () => {
|
||||
const randomHex = () => {
|
||||
const hue = Math.floor(Math.random() * 360)
|
||||
const saturation = 50 + Math.floor(Math.random() * 50)
|
||||
const lightness = 40 + Math.floor(Math.random() * 30)
|
||||
return hslToHex(hue, saturation, lightness)
|
||||
}
|
||||
|
||||
const primary = randomHex()
|
||||
const secondary = randomHex()
|
||||
const accent = randomHex()
|
||||
|
||||
setColors((prev) => ({
|
||||
...prev,
|
||||
primary,
|
||||
"primary-foreground": "#ffffff",
|
||||
secondary,
|
||||
"secondary-foreground": mode === "dark" ? "#fafafa" : "#0a0a0a",
|
||||
accent,
|
||||
"accent-foreground": mode === "dark" ? "#fafafa" : "#0a0a0a",
|
||||
ring: primary,
|
||||
}))
|
||||
}
|
||||
|
||||
const hslToHex = (h: number, s: number, l: number) => {
|
||||
l /= 100
|
||||
const a = (s * Math.min(l, 1 - l)) / 100
|
||||
const f = (n: number) => {
|
||||
const k = (n + h / 30) % 12
|
||||
const color = l - a * Math.max(Math.min(k - 3, 9 - k, 1), -1)
|
||||
return Math.round(255 * color)
|
||||
.toString(16)
|
||||
.padStart(2, "0")
|
||||
}
|
||||
return `#${f(0)}${f(8)}${f(4)}`
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="container max-w-[1600px] py-6">
|
||||
{/* Header */}
|
||||
<div className="mb-8 flex items-center justify-between">
|
||||
<div>
|
||||
<h1 className="text-4xl font-bold tracking-tight flex items-center gap-3">
|
||||
<Palette className="h-8 w-8 text-primary" />
|
||||
Theme Generator
|
||||
</h1>
|
||||
<p className="mt-2 text-lg text-muted-foreground">
|
||||
Create beautiful themes with live preview and instant export
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex items-center gap-4">
|
||||
<Button
|
||||
variant="outline"
|
||||
size="icon"
|
||||
onClick={toggleMode}
|
||||
className="relative"
|
||||
>
|
||||
{mode === "light" ? (
|
||||
<Moon className="h-5 w-5" />
|
||||
) : (
|
||||
<Sun className="h-5 w-5" />
|
||||
)}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="grid gap-8 lg:grid-cols-[420px_1fr]">
|
||||
{/* Enhanced Controls Sidebar */}
|
||||
<div className="space-y-6">
|
||||
{/* Preset Themes */}
|
||||
<Card className="overflow-hidden">
|
||||
<CardHeader className="bg-gradient-to-r from-primary/10 to-primary/5">
|
||||
<CardTitle className="flex items-center gap-2">
|
||||
<Sparkles className="h-5 w-5" />
|
||||
Preset Themes
|
||||
</CardTitle>
|
||||
<CardDescription>Start with a pre-designed theme</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="pt-6">
|
||||
<div className="grid grid-cols-2 gap-2">
|
||||
{Object.keys(PRESET_THEMES).map((presetName) => (
|
||||
<Button
|
||||
key={presetName}
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() => applyPresetTheme(presetName)}
|
||||
className="justify-start text-xs"
|
||||
>
|
||||
{presetName}
|
||||
</Button>
|
||||
))}
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* Color Customization */}
|
||||
<Card className="overflow-hidden">
|
||||
<CardHeader>
|
||||
<CardTitle className="flex items-center gap-2">
|
||||
<Settings2 className="h-5 w-5" />
|
||||
Color Palette
|
||||
</CardTitle>
|
||||
<CardDescription>Fine-tune your color scheme</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-3">
|
||||
<ScrollArea className="h-[400px] pr-4">
|
||||
<div className="space-y-3">
|
||||
{Object.entries(colors).map(([key, value]) => (
|
||||
<div key={key} className="space-y-1.5">
|
||||
<Label className="text-xs font-medium capitalize">
|
||||
{key.replace(/-/g, " ")}
|
||||
</Label>
|
||||
<Popover
|
||||
open={activeColorKey === key}
|
||||
onOpenChange={(open) =>
|
||||
setActiveColorKey(open ? key : null)
|
||||
}
|
||||
>
|
||||
<PopoverTrigger asChild>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
className="w-full justify-between font-mono text-xs h-9"
|
||||
>
|
||||
<div className="flex items-center gap-2">
|
||||
<div
|
||||
className="h-5 w-5 rounded-md border shadow-sm"
|
||||
style={{ backgroundColor: value }}
|
||||
/>
|
||||
<span>{value.toUpperCase()}</span>
|
||||
</div>
|
||||
<div className="text-[10px] text-muted-foreground">
|
||||
HSL: {hexToHSL(value).split(" ").join(", ")}
|
||||
</div>
|
||||
</Button>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent className="w-auto p-3" align="start">
|
||||
<div className="space-y-3">
|
||||
<HexColorPicker
|
||||
color={value}
|
||||
onChange={(v) => updateColor(key, v)}
|
||||
style={{ width: 200, height: 200 }}
|
||||
/>
|
||||
<HexColorInput
|
||||
color={value}
|
||||
onChange={(v) => updateColor(key, v)}
|
||||
className="w-full rounded-md border px-3 py-1 text-sm font-mono"
|
||||
prefixed
|
||||
/>
|
||||
</div>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</ScrollArea>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* Border Radius Control */}
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="text-sm">Border Radius</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="space-y-3">
|
||||
<Slider
|
||||
value={[radius]}
|
||||
onValueChange={(v) => setRadius(v[0])}
|
||||
min={0}
|
||||
max={1.5}
|
||||
step={0.05}
|
||||
className="w-full"
|
||||
/>
|
||||
<div className="flex justify-between text-xs text-muted-foreground">
|
||||
<span>Sharp</span>
|
||||
<span className="font-mono">{radius}rem</span>
|
||||
<span>Rounded</span>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* Action Buttons */}
|
||||
<div className="grid grid-cols-2 gap-2">
|
||||
<Button
|
||||
onClick={randomizeTheme}
|
||||
variant="outline"
|
||||
size="sm"
|
||||
className="w-full"
|
||||
>
|
||||
<Shuffle className="mr-2 h-4 w-4" />
|
||||
Randomize
|
||||
</Button>
|
||||
<Button
|
||||
onClick={() =>
|
||||
setColors(
|
||||
mode === "light" ? DEFAULT_LIGHT_COLORS : DEFAULT_DARK_COLORS
|
||||
)
|
||||
}
|
||||
variant="outline"
|
||||
size="sm"
|
||||
className="w-full"
|
||||
>
|
||||
<RotateCcw className="mr-2 h-4 w-4" />
|
||||
Reset
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<Button onClick={copyCSS} className="w-full" size="lg">
|
||||
{copied ? (
|
||||
<>
|
||||
<Check className="mr-2 h-4 w-4" />
|
||||
Copied!
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<Copy className="mr-2 h-4 w-4" />
|
||||
Copy CSS Variables
|
||||
</>
|
||||
)}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{/* Enhanced Preview Area */}
|
||||
<div className="space-y-6">
|
||||
<Tabs defaultValue="preview" className="w-full">
|
||||
<TabsList className="grid w-full grid-cols-3">
|
||||
<TabsTrigger value="preview">Live Preview</TabsTrigger>
|
||||
<TabsTrigger value="components">Components</TabsTrigger>
|
||||
<TabsTrigger value="code">Export Code</TabsTrigger>
|
||||
</TabsList>
|
||||
|
||||
<TabsContent value="preview" className="mt-6">
|
||||
<div className="theme-preview-container" data-theme="custom">
|
||||
<style
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: `
|
||||
.theme-preview-container[data-theme="custom"] {
|
||||
--background: ${hexToHSL(colors.background)};
|
||||
--foreground: ${hexToHSL(colors.foreground)};
|
||||
--card: ${hexToHSL(colors.card)};
|
||||
--card-foreground: ${hexToHSL(colors["card-foreground"])};
|
||||
--popover: ${hexToHSL(colors.popover)};
|
||||
--popover-foreground: ${hexToHSL(colors["popover-foreground"])};
|
||||
--primary: ${hexToHSL(colors.primary)};
|
||||
--primary-foreground: ${hexToHSL(colors["primary-foreground"])};
|
||||
--secondary: ${hexToHSL(colors.secondary)};
|
||||
--secondary-foreground: ${hexToHSL(colors["secondary-foreground"])};
|
||||
--muted: ${hexToHSL(colors.muted)};
|
||||
--muted-foreground: ${hexToHSL(colors["muted-foreground"])};
|
||||
--accent: ${hexToHSL(colors.accent)};
|
||||
--accent-foreground: ${hexToHSL(colors["accent-foreground"])};
|
||||
--destructive: ${hexToHSL(colors.destructive)};
|
||||
--destructive-foreground: ${hexToHSL(colors["destructive-foreground"])};
|
||||
--border: ${hexToHSL(colors.border)};
|
||||
--input: ${hexToHSL(colors.input)};
|
||||
--ring: ${hexToHSL(colors.ring)};
|
||||
--radius: ${radius}rem;
|
||||
}
|
||||
.theme-preview-container[data-theme="custom"] * {
|
||||
border-radius: calc(var(--radius) * 1);
|
||||
}
|
||||
`,
|
||||
}}
|
||||
/>
|
||||
<div
|
||||
className="rounded-lg border bg-background p-8 shadow-sm"
|
||||
style={{
|
||||
backgroundColor: `hsl(var(--background))`,
|
||||
color: `hsl(var(--foreground))`,
|
||||
borderColor: `hsl(var(--border))`,
|
||||
}}
|
||||
>
|
||||
<div className="space-y-8">
|
||||
{/* Hero Section Preview */}
|
||||
<div className="space-y-4">
|
||||
<h2 className="text-3xl font-bold">
|
||||
Welcome to Your App
|
||||
</h2>
|
||||
<p className="text-lg text-muted-foreground">
|
||||
This is how your theme looks in a real application
|
||||
context
|
||||
</p>
|
||||
<div className="flex flex-wrap gap-3">
|
||||
<Button size="lg">Get Started</Button>
|
||||
<Button size="lg" variant="secondary">
|
||||
Learn More
|
||||
</Button>
|
||||
<Button size="lg" variant="outline">
|
||||
Documentation
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Separator />
|
||||
|
||||
{/* Form Elements */}
|
||||
<div className="grid gap-6 md:grid-cols-2">
|
||||
<div className="space-y-4">
|
||||
<h3 className="text-lg font-semibold">Form Elements</h3>
|
||||
<div className="space-y-3">
|
||||
<div className="space-y-1">
|
||||
<Label htmlFor="email">Email</Label>
|
||||
<Input
|
||||
id="email"
|
||||
placeholder="name@example.com"
|
||||
type="email"
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-1">
|
||||
<Label htmlFor="message">Message</Label>
|
||||
<Textarea
|
||||
id="message"
|
||||
placeholder="Type your message here..."
|
||||
/>
|
||||
</div>
|
||||
<div className="flex items-center space-x-2">
|
||||
<Checkbox id="terms" />
|
||||
<Label htmlFor="terms" className="text-sm">
|
||||
Accept terms and conditions
|
||||
</Label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-4">
|
||||
<h3 className="text-lg font-semibold">Selections</h3>
|
||||
<div className="space-y-3">
|
||||
<Select>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder="Select an option" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="option1">Option 1</SelectItem>
|
||||
<SelectItem value="option2">Option 2</SelectItem>
|
||||
<SelectItem value="option3">Option 3</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<RadioGroup defaultValue="option-one">
|
||||
<div className="flex items-center space-x-2">
|
||||
<RadioGroupItem
|
||||
value="option-one"
|
||||
id="option-one"
|
||||
/>
|
||||
<Label htmlFor="option-one">Option One</Label>
|
||||
</div>
|
||||
<div className="flex items-center space-x-2">
|
||||
<RadioGroupItem
|
||||
value="option-two"
|
||||
id="option-two"
|
||||
/>
|
||||
<Label htmlFor="option-two">Option Two</Label>
|
||||
</div>
|
||||
</RadioGroup>
|
||||
<div className="flex items-center space-x-2">
|
||||
<Switch id="notifications" />
|
||||
<Label htmlFor="notifications">
|
||||
Enable notifications
|
||||
</Label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Cards */}
|
||||
<div className="grid gap-4 md:grid-cols-2">
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Card Title</CardTitle>
|
||||
<CardDescription>
|
||||
Card description goes here
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
This is how cards appear with your custom theme. The
|
||||
borders, shadows, and colors all adapt to your
|
||||
selections.
|
||||
</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Statistics</CardTitle>
|
||||
<CardDescription>
|
||||
Your performance metrics
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-2">
|
||||
<div className="flex justify-between">
|
||||
<span className="text-sm">Progress</span>
|
||||
<span className="text-sm font-medium">75%</span>
|
||||
</div>
|
||||
<Progress value={75} className="h-2" />
|
||||
<div className="flex gap-2 pt-2">
|
||||
<Badge>Active</Badge>
|
||||
<Badge variant="secondary">Updated</Badge>
|
||||
<Badge variant="outline">New</Badge>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
{/* Alert */}
|
||||
<Alert>
|
||||
<AlertTitle>Heads up!</AlertTitle>
|
||||
<AlertDescription>
|
||||
You can customize every aspect of your theme using the
|
||||
controls on the left. Changes are reflected in
|
||||
real-time.
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</TabsContent>
|
||||
|
||||
<TabsContent value="components" className="mt-6">
|
||||
<div
|
||||
className="theme-components-container"
|
||||
data-theme="custom-components"
|
||||
>
|
||||
<style
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: `
|
||||
.theme-components-container[data-theme="custom-components"] {
|
||||
--background: ${hexToHSL(colors.background)};
|
||||
--foreground: ${hexToHSL(colors.foreground)};
|
||||
--card: ${hexToHSL(colors.card)};
|
||||
--card-foreground: ${hexToHSL(colors["card-foreground"])};
|
||||
--popover: ${hexToHSL(colors.popover)};
|
||||
--popover-foreground: ${hexToHSL(colors["popover-foreground"])};
|
||||
--primary: ${hexToHSL(colors.primary)};
|
||||
--primary-foreground: ${hexToHSL(colors["primary-foreground"])};
|
||||
--secondary: ${hexToHSL(colors.secondary)};
|
||||
--secondary-foreground: ${hexToHSL(colors["secondary-foreground"])};
|
||||
--muted: ${hexToHSL(colors.muted)};
|
||||
--muted-foreground: ${hexToHSL(colors["muted-foreground"])};
|
||||
--accent: ${hexToHSL(colors.accent)};
|
||||
--accent-foreground: ${hexToHSL(colors["accent-foreground"])};
|
||||
--destructive: ${hexToHSL(colors.destructive)};
|
||||
--destructive-foreground: ${hexToHSL(colors["destructive-foreground"])};
|
||||
--border: ${hexToHSL(colors.border)};
|
||||
--input: ${hexToHSL(colors.input)};
|
||||
--ring: ${hexToHSL(colors.ring)};
|
||||
--radius: ${radius}rem;
|
||||
}
|
||||
.theme-components-container[data-theme="custom-components"] * {
|
||||
border-radius: calc(var(--radius) * 1);
|
||||
}
|
||||
`,
|
||||
}}
|
||||
/>
|
||||
<div
|
||||
className="rounded-lg border bg-background p-8 space-y-8"
|
||||
style={{
|
||||
backgroundColor: `hsl(var(--background))`,
|
||||
color: `hsl(var(--foreground))`,
|
||||
borderColor: `hsl(var(--border))`,
|
||||
}}
|
||||
>
|
||||
<div className="space-y-6">
|
||||
<div>
|
||||
<h3 className="text-lg font-semibold mb-4">
|
||||
Button Variants
|
||||
</h3>
|
||||
<div className="flex flex-wrap gap-3">
|
||||
<Button>Primary</Button>
|
||||
<Button variant="secondary">Secondary</Button>
|
||||
<Button variant="outline">Outline</Button>
|
||||
<Button variant="ghost">Ghost</Button>
|
||||
<Button variant="destructive">Destructive</Button>
|
||||
<Button variant="link">Link</Button>
|
||||
<Button size="sm">Small</Button>
|
||||
<Button size="lg">Large</Button>
|
||||
<Button disabled>Disabled</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Separator />
|
||||
|
||||
<div>
|
||||
<h3 className="text-lg font-semibold mb-4">
|
||||
Badge Variants
|
||||
</h3>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
<Badge>Default</Badge>
|
||||
<Badge variant="secondary">Secondary</Badge>
|
||||
<Badge variant="outline">Outline</Badge>
|
||||
<Badge variant="destructive">Destructive</Badge>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Separator />
|
||||
|
||||
<div>
|
||||
<h3 className="text-lg font-semibold mb-4">Typography</h3>
|
||||
<div className="space-y-2">
|
||||
<h1 className="text-4xl font-bold">Heading 1</h1>
|
||||
<h2 className="text-3xl font-bold">Heading 2</h2>
|
||||
<h3 className="text-2xl font-bold">Heading 3</h3>
|
||||
<h4 className="text-xl font-bold">Heading 4</h4>
|
||||
<p className="text-lg">Large paragraph text</p>
|
||||
<p>Regular paragraph text</p>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
Muted text
|
||||
</p>
|
||||
<p className="text-xs">Small text</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</TabsContent>
|
||||
|
||||
<TabsContent value="code" className="mt-6">
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>CSS Variables</CardTitle>
|
||||
<CardDescription>
|
||||
Copy this code and paste it into your globals.css file
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="relative">
|
||||
<pre className="overflow-auto rounded-lg bg-muted p-6 text-sm">
|
||||
<code className="language-css">{generateCSS()}</code>
|
||||
</pre>
|
||||
<Button
|
||||
onClick={copyCSS}
|
||||
size="sm"
|
||||
className="absolute right-2 top-2"
|
||||
variant="ghost"
|
||||
>
|
||||
{copied ? (
|
||||
<Check className="h-4 w-4" />
|
||||
) : (
|
||||
<Copy className="h-4 w-4" />
|
||||
)}
|
||||
</Button>
|
||||
</div>
|
||||
<div className="mt-4 rounded-lg bg-muted/50 p-4">
|
||||
<p className="text-sm font-medium mb-2">
|
||||
Usage Instructions:
|
||||
</p>
|
||||
<ol className="text-sm text-muted-foreground space-y-1 list-decimal list-inside">
|
||||
<li>Copy the CSS variables above</li>
|
||||
<li>
|
||||
Open your project's global CSS file (usually globals.css
|
||||
or app.css)
|
||||
</li>
|
||||
<li>
|
||||
Replace the existing :root variables with your new theme
|
||||
</li>
|
||||
<li>Save the file and refresh your application</li>
|
||||
</ol>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</TabsContent>
|
||||
</Tabs>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -1,797 +1,35 @@
|
||||
"use client"
|
||||
|
||||
import * as React from "react"
|
||||
import {
|
||||
Check,
|
||||
Copy,
|
||||
Download,
|
||||
Monitor,
|
||||
Moon,
|
||||
Palette,
|
||||
RotateCcw,
|
||||
Settings2,
|
||||
Shuffle,
|
||||
Sparkles,
|
||||
Sun,
|
||||
} from "lucide-react"
|
||||
import { HexColorInput, HexColorPicker } from "react-colorful"
|
||||
import dynamic from "next/dynamic"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
import {
|
||||
Alert,
|
||||
AlertDescription,
|
||||
AlertTitle,
|
||||
} from "@/registry/default/ui/alert"
|
||||
import { Badge } from "@/registry/default/ui/badge"
|
||||
import { Button } from "@/registry/default/ui/button"
|
||||
import {
|
||||
Card,
|
||||
CardContent,
|
||||
CardDescription,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
} from "@/registry/default/ui/card"
|
||||
import { Checkbox } from "@/registry/default/ui/checkbox"
|
||||
import { Input } from "@/registry/default/ui/input"
|
||||
import { Label } from "@/registry/default/ui/label"
|
||||
import {
|
||||
Popover,
|
||||
PopoverContent,
|
||||
PopoverTrigger,
|
||||
} from "@/registry/default/ui/popover"
|
||||
import { Progress } from "@/registry/default/ui/progress"
|
||||
import { RadioGroup, RadioGroupItem } from "@/registry/default/ui/radio-group"
|
||||
import { ScrollArea } from "@/registry/default/ui/scroll-area"
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from "@/registry/default/ui/select"
|
||||
import { Separator } from "@/registry/default/ui/separator"
|
||||
import { Slider } from "@/registry/default/ui/slider"
|
||||
import { Switch } from "@/registry/default/ui/switch"
|
||||
import {
|
||||
Tabs,
|
||||
TabsContent,
|
||||
TabsList,
|
||||
TabsTrigger,
|
||||
} from "@/registry/default/ui/tabs"
|
||||
import { Textarea } from "@/registry/default/ui/textarea"
|
||||
import { SkeletonCard } from "@/components/skeleton-card"
|
||||
|
||||
const DEFAULT_LIGHT_COLORS = {
|
||||
background: "#ffffff",
|
||||
foreground: "#0a0a0a",
|
||||
card: "#ffffff",
|
||||
"card-foreground": "#0a0a0a",
|
||||
popover: "#ffffff",
|
||||
"popover-foreground": "#0a0a0a",
|
||||
primary: "#0099ff",
|
||||
"primary-foreground": "#ffffff",
|
||||
secondary: "#f4f4f5",
|
||||
"secondary-foreground": "#0a0a0a",
|
||||
muted: "#f4f4f5",
|
||||
"muted-foreground": "#71717a",
|
||||
accent: "#f4f4f5",
|
||||
"accent-foreground": "#0a0a0a",
|
||||
destructive: "#ef4444",
|
||||
"destructive-foreground": "#ffffff",
|
||||
border: "#e4e4e7",
|
||||
input: "#e4e4e7",
|
||||
ring: "#0099ff",
|
||||
}
|
||||
|
||||
const DEFAULT_DARK_COLORS = {
|
||||
background: "#0a0a0a",
|
||||
foreground: "#fafafa",
|
||||
card: "#0a0a0a",
|
||||
"card-foreground": "#fafafa",
|
||||
popover: "#0a0a0a",
|
||||
"popover-foreground": "#fafafa",
|
||||
primary: "#0099ff",
|
||||
"primary-foreground": "#ffffff",
|
||||
secondary: "#262626",
|
||||
"secondary-foreground": "#fafafa",
|
||||
muted: "#262626",
|
||||
"muted-foreground": "#a3a3a3",
|
||||
accent: "#262626",
|
||||
"accent-foreground": "#fafafa",
|
||||
destructive: "#dc2626",
|
||||
"destructive-foreground": "#ffffff",
|
||||
border: "#262626",
|
||||
input: "#262626",
|
||||
ring: "#0099ff",
|
||||
}
|
||||
|
||||
const PRESET_THEMES = {
|
||||
"Electric Blue": {
|
||||
primary: "#0099ff",
|
||||
secondary: "#1e293b",
|
||||
accent: "#06b6d4",
|
||||
},
|
||||
Forest: {
|
||||
primary: "#10b981",
|
||||
secondary: "#065f46",
|
||||
accent: "#34d399",
|
||||
},
|
||||
Sunset: {
|
||||
primary: "#f97316",
|
||||
secondary: "#dc2626",
|
||||
accent: "#fbbf24",
|
||||
},
|
||||
"Purple Haze": {
|
||||
primary: "#8b5cf6",
|
||||
secondary: "#7c3aed",
|
||||
accent: "#a78bfa",
|
||||
},
|
||||
Ocean: {
|
||||
primary: "#0ea5e9",
|
||||
secondary: "#0284c7",
|
||||
accent: "#38bdf8",
|
||||
},
|
||||
}
|
||||
// Dynamically import the color picker library and theme generator
|
||||
const ThemeGeneratorClient = dynamic(
|
||||
() =>
|
||||
import("./theme-generator-client").then((mod) => mod.ThemeGeneratorClient),
|
||||
{
|
||||
loading: () => (
|
||||
<div className="container max-w-[1600px] py-6">
|
||||
<div className="space-y-6">
|
||||
<div>
|
||||
<div className="h-12 w-64 bg-muted animate-pulse rounded" />
|
||||
<div className="h-6 w-96 bg-muted animate-pulse rounded mt-2" />
|
||||
</div>
|
||||
<div className="grid gap-8 lg:grid-cols-[420px_1fr]">
|
||||
<div className="space-y-4">
|
||||
<SkeletonCard />
|
||||
<SkeletonCard />
|
||||
</div>
|
||||
<SkeletonCard className="h-[600px]" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
),
|
||||
ssr: false, // Color picker needs client-side rendering
|
||||
}
|
||||
)
|
||||
|
||||
export default function ThemeGeneratorPage() {
|
||||
const [colors, setColors] = React.useState(DEFAULT_LIGHT_COLORS)
|
||||
const [radius, setRadius] = React.useState(0.5)
|
||||
const [mode, setMode] = React.useState<"light" | "dark">("light")
|
||||
const [copied, setCopied] = React.useState(false)
|
||||
const [activeColorKey, setActiveColorKey] = React.useState<string | null>(
|
||||
null
|
||||
)
|
||||
|
||||
const updateColor = (key: string, value: string) => {
|
||||
setColors((prev) => ({ ...prev, [key]: value }))
|
||||
}
|
||||
|
||||
const toggleMode = () => {
|
||||
const newMode = mode === "light" ? "dark" : "light"
|
||||
setMode(newMode)
|
||||
setColors(newMode === "light" ? DEFAULT_LIGHT_COLORS : DEFAULT_DARK_COLORS)
|
||||
}
|
||||
|
||||
const hexToHSL = (hex: string) => {
|
||||
const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex)
|
||||
if (!result) return "0 0% 0%"
|
||||
|
||||
const r = parseInt(result[1], 16) / 255
|
||||
const g = parseInt(result[2], 16) / 255
|
||||
const b = parseInt(result[3], 16) / 255
|
||||
|
||||
const max = Math.max(r, g, b)
|
||||
const min = Math.min(r, g, b)
|
||||
let h = 0,
|
||||
s = 0,
|
||||
l = (max + min) / 2
|
||||
|
||||
if (max !== min) {
|
||||
const d = max - min
|
||||
s = l > 0.5 ? d / (2 - max - min) : d / (max + min)
|
||||
switch (max) {
|
||||
case r:
|
||||
h = ((g - b) / d + (g < b ? 6 : 0)) / 6
|
||||
break
|
||||
case g:
|
||||
h = ((b - r) / d + 2) / 6
|
||||
break
|
||||
case b:
|
||||
h = ((r - g) / d + 4) / 6
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
return `${Math.round(h * 360)} ${Math.round(s * 100)}% ${Math.round(l * 100)}%`
|
||||
}
|
||||
|
||||
const generateCSS = () => {
|
||||
const css = `@layer base {
|
||||
:root {
|
||||
${Object.entries(colors)
|
||||
.map(([key, value]) => ` --${key}: ${hexToHSL(value)};`)
|
||||
.join("\n")}
|
||||
--radius: ${radius}rem;
|
||||
}
|
||||
|
||||
.dark {
|
||||
${Object.entries(colors)
|
||||
.map(([key, value]) => ` --${key}: ${hexToHSL(value)};`)
|
||||
.join("\n")}
|
||||
}
|
||||
}`
|
||||
return css
|
||||
}
|
||||
|
||||
const copyCSS = async () => {
|
||||
try {
|
||||
await navigator.clipboard.writeText(generateCSS())
|
||||
setCopied(true)
|
||||
setTimeout(() => setCopied(false), 2000)
|
||||
} catch (err) {
|
||||
console.error("Failed to copy CSS:", err)
|
||||
}
|
||||
}
|
||||
|
||||
const applyPresetTheme = (presetName: string) => {
|
||||
const preset = PRESET_THEMES[presetName as keyof typeof PRESET_THEMES]
|
||||
if (preset) {
|
||||
setColors((prev) => ({
|
||||
...prev,
|
||||
primary: preset.primary,
|
||||
"primary-foreground": mode === "dark" ? "#ffffff" : "#ffffff",
|
||||
secondary: preset.secondary,
|
||||
"secondary-foreground": mode === "dark" ? "#fafafa" : "#0a0a0a",
|
||||
accent: preset.accent,
|
||||
"accent-foreground": mode === "dark" ? "#fafafa" : "#0a0a0a",
|
||||
ring: preset.primary,
|
||||
}))
|
||||
}
|
||||
}
|
||||
|
||||
const randomizeTheme = () => {
|
||||
const randomHex = () => {
|
||||
const hue = Math.floor(Math.random() * 360)
|
||||
const saturation = 50 + Math.floor(Math.random() * 50)
|
||||
const lightness = 40 + Math.floor(Math.random() * 30)
|
||||
return hslToHex(hue, saturation, lightness)
|
||||
}
|
||||
|
||||
const primary = randomHex()
|
||||
const secondary = randomHex()
|
||||
const accent = randomHex()
|
||||
|
||||
setColors((prev) => ({
|
||||
...prev,
|
||||
primary,
|
||||
"primary-foreground": "#ffffff",
|
||||
secondary,
|
||||
"secondary-foreground": mode === "dark" ? "#fafafa" : "#0a0a0a",
|
||||
accent,
|
||||
"accent-foreground": mode === "dark" ? "#fafafa" : "#0a0a0a",
|
||||
ring: primary,
|
||||
}))
|
||||
}
|
||||
|
||||
const hslToHex = (h: number, s: number, l: number) => {
|
||||
l /= 100
|
||||
const a = (s * Math.min(l, 1 - l)) / 100
|
||||
const f = (n: number) => {
|
||||
const k = (n + h / 30) % 12
|
||||
const color = l - a * Math.max(Math.min(k - 3, 9 - k, 1), -1)
|
||||
return Math.round(255 * color)
|
||||
.toString(16)
|
||||
.padStart(2, "0")
|
||||
}
|
||||
return `#${f(0)}${f(8)}${f(4)}`
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="container max-w-[1600px] py-6">
|
||||
{/* Header */}
|
||||
<div className="mb-8 flex items-center justify-between">
|
||||
<div>
|
||||
<h1 className="text-4xl font-bold tracking-tight flex items-center gap-3">
|
||||
<Palette className="h-8 w-8 text-primary" />
|
||||
Theme Generator
|
||||
</h1>
|
||||
<p className="mt-2 text-lg text-muted-foreground">
|
||||
Create beautiful themes with live preview and instant export
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex items-center gap-4">
|
||||
<Button
|
||||
variant="outline"
|
||||
size="icon"
|
||||
onClick={toggleMode}
|
||||
className="relative"
|
||||
>
|
||||
{mode === "light" ? (
|
||||
<Moon className="h-5 w-5" />
|
||||
) : (
|
||||
<Sun className="h-5 w-5" />
|
||||
)}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="grid gap-8 lg:grid-cols-[420px_1fr]">
|
||||
{/* Enhanced Controls Sidebar */}
|
||||
<div className="space-y-6">
|
||||
{/* Preset Themes */}
|
||||
<Card className="overflow-hidden">
|
||||
<CardHeader className="bg-gradient-to-r from-primary/10 to-primary/5">
|
||||
<CardTitle className="flex items-center gap-2">
|
||||
<Sparkles className="h-5 w-5" />
|
||||
Preset Themes
|
||||
</CardTitle>
|
||||
<CardDescription>Start with a pre-designed theme</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="pt-6">
|
||||
<div className="grid grid-cols-2 gap-2">
|
||||
{Object.keys(PRESET_THEMES).map((presetName) => (
|
||||
<Button
|
||||
key={presetName}
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() => applyPresetTheme(presetName)}
|
||||
className="justify-start text-xs"
|
||||
>
|
||||
{presetName}
|
||||
</Button>
|
||||
))}
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* Color Customization */}
|
||||
<Card className="overflow-hidden">
|
||||
<CardHeader>
|
||||
<CardTitle className="flex items-center gap-2">
|
||||
<Settings2 className="h-5 w-5" />
|
||||
Color Palette
|
||||
</CardTitle>
|
||||
<CardDescription>Fine-tune your color scheme</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-3">
|
||||
<ScrollArea className="h-[400px] pr-4">
|
||||
<div className="space-y-3">
|
||||
{Object.entries(colors).map(([key, value]) => (
|
||||
<div key={key} className="space-y-1.5">
|
||||
<Label className="text-xs font-medium capitalize">
|
||||
{key.replace(/-/g, " ")}
|
||||
</Label>
|
||||
<Popover
|
||||
open={activeColorKey === key}
|
||||
onOpenChange={(open) =>
|
||||
setActiveColorKey(open ? key : null)
|
||||
}
|
||||
>
|
||||
<PopoverTrigger asChild>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
className="w-full justify-between font-mono text-xs h-9"
|
||||
>
|
||||
<div className="flex items-center gap-2">
|
||||
<div
|
||||
className="h-5 w-5 rounded-md border shadow-sm"
|
||||
style={{ backgroundColor: value }}
|
||||
/>
|
||||
<span>{value.toUpperCase()}</span>
|
||||
</div>
|
||||
<div className="text-[10px] text-muted-foreground">
|
||||
HSL: {hexToHSL(value).split(" ").join(", ")}
|
||||
</div>
|
||||
</Button>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent className="w-auto p-3" align="start">
|
||||
<div className="space-y-3">
|
||||
<HexColorPicker
|
||||
color={value}
|
||||
onChange={(v) => updateColor(key, v)}
|
||||
style={{ width: 200, height: 200 }}
|
||||
/>
|
||||
<HexColorInput
|
||||
color={value}
|
||||
onChange={(v) => updateColor(key, v)}
|
||||
className="w-full rounded-md border px-3 py-1 text-sm font-mono"
|
||||
prefixed
|
||||
/>
|
||||
</div>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</ScrollArea>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* Border Radius Control */}
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="text-sm">Border Radius</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="space-y-3">
|
||||
<Slider
|
||||
value={[radius]}
|
||||
onValueChange={(v) => setRadius(v[0])}
|
||||
min={0}
|
||||
max={1.5}
|
||||
step={0.05}
|
||||
className="w-full"
|
||||
/>
|
||||
<div className="flex justify-between text-xs text-muted-foreground">
|
||||
<span>Sharp</span>
|
||||
<span className="font-mono">{radius}rem</span>
|
||||
<span>Rounded</span>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* Action Buttons */}
|
||||
<div className="grid grid-cols-2 gap-2">
|
||||
<Button
|
||||
onClick={randomizeTheme}
|
||||
variant="outline"
|
||||
size="sm"
|
||||
className="w-full"
|
||||
>
|
||||
<Shuffle className="mr-2 h-4 w-4" />
|
||||
Randomize
|
||||
</Button>
|
||||
<Button
|
||||
onClick={() =>
|
||||
setColors(
|
||||
mode === "light" ? DEFAULT_LIGHT_COLORS : DEFAULT_DARK_COLORS
|
||||
)
|
||||
}
|
||||
variant="outline"
|
||||
size="sm"
|
||||
className="w-full"
|
||||
>
|
||||
<RotateCcw className="mr-2 h-4 w-4" />
|
||||
Reset
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<Button onClick={copyCSS} className="w-full" size="lg">
|
||||
{copied ? (
|
||||
<>
|
||||
<Check className="mr-2 h-4 w-4" />
|
||||
Copied!
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<Copy className="mr-2 h-4 w-4" />
|
||||
Copy CSS Variables
|
||||
</>
|
||||
)}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{/* Enhanced Preview Area */}
|
||||
<div className="space-y-6">
|
||||
<Tabs defaultValue="preview" className="w-full">
|
||||
<TabsList className="grid w-full grid-cols-3">
|
||||
<TabsTrigger value="preview">Live Preview</TabsTrigger>
|
||||
<TabsTrigger value="components">Components</TabsTrigger>
|
||||
<TabsTrigger value="code">Export Code</TabsTrigger>
|
||||
</TabsList>
|
||||
|
||||
<TabsContent value="preview" className="mt-6">
|
||||
<div className="theme-preview-container" data-theme="custom">
|
||||
<style
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: `
|
||||
.theme-preview-container[data-theme="custom"] {
|
||||
--background: ${hexToHSL(colors.background)};
|
||||
--foreground: ${hexToHSL(colors.foreground)};
|
||||
--card: ${hexToHSL(colors.card)};
|
||||
--card-foreground: ${hexToHSL(colors["card-foreground"])};
|
||||
--popover: ${hexToHSL(colors.popover)};
|
||||
--popover-foreground: ${hexToHSL(colors["popover-foreground"])};
|
||||
--primary: ${hexToHSL(colors.primary)};
|
||||
--primary-foreground: ${hexToHSL(colors["primary-foreground"])};
|
||||
--secondary: ${hexToHSL(colors.secondary)};
|
||||
--secondary-foreground: ${hexToHSL(colors["secondary-foreground"])};
|
||||
--muted: ${hexToHSL(colors.muted)};
|
||||
--muted-foreground: ${hexToHSL(colors["muted-foreground"])};
|
||||
--accent: ${hexToHSL(colors.accent)};
|
||||
--accent-foreground: ${hexToHSL(colors["accent-foreground"])};
|
||||
--destructive: ${hexToHSL(colors.destructive)};
|
||||
--destructive-foreground: ${hexToHSL(colors["destructive-foreground"])};
|
||||
--border: ${hexToHSL(colors.border)};
|
||||
--input: ${hexToHSL(colors.input)};
|
||||
--ring: ${hexToHSL(colors.ring)};
|
||||
--radius: ${radius}rem;
|
||||
}
|
||||
.theme-preview-container[data-theme="custom"] * {
|
||||
border-radius: calc(var(--radius) * 1);
|
||||
}
|
||||
`,
|
||||
}}
|
||||
/>
|
||||
<div
|
||||
className="rounded-lg border bg-background p-8 shadow-sm"
|
||||
style={{
|
||||
backgroundColor: `hsl(var(--background))`,
|
||||
color: `hsl(var(--foreground))`,
|
||||
borderColor: `hsl(var(--border))`,
|
||||
}}
|
||||
>
|
||||
<div className="space-y-8">
|
||||
{/* Hero Section Preview */}
|
||||
<div className="space-y-4">
|
||||
<h2 className="text-3xl font-bold">
|
||||
Welcome to Your App
|
||||
</h2>
|
||||
<p className="text-lg text-muted-foreground">
|
||||
This is how your theme looks in a real application
|
||||
context
|
||||
</p>
|
||||
<div className="flex flex-wrap gap-3">
|
||||
<Button size="lg">Get Started</Button>
|
||||
<Button size="lg" variant="secondary">
|
||||
Learn More
|
||||
</Button>
|
||||
<Button size="lg" variant="outline">
|
||||
Documentation
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Separator />
|
||||
|
||||
{/* Form Elements */}
|
||||
<div className="grid gap-6 md:grid-cols-2">
|
||||
<div className="space-y-4">
|
||||
<h3 className="text-lg font-semibold">Form Elements</h3>
|
||||
<div className="space-y-3">
|
||||
<div className="space-y-1">
|
||||
<Label htmlFor="email">Email</Label>
|
||||
<Input
|
||||
id="email"
|
||||
placeholder="name@example.com"
|
||||
type="email"
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-1">
|
||||
<Label htmlFor="message">Message</Label>
|
||||
<Textarea
|
||||
id="message"
|
||||
placeholder="Type your message here..."
|
||||
/>
|
||||
</div>
|
||||
<div className="flex items-center space-x-2">
|
||||
<Checkbox id="terms" />
|
||||
<Label htmlFor="terms" className="text-sm">
|
||||
Accept terms and conditions
|
||||
</Label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-4">
|
||||
<h3 className="text-lg font-semibold">Selections</h3>
|
||||
<div className="space-y-3">
|
||||
<Select>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder="Select an option" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="option1">Option 1</SelectItem>
|
||||
<SelectItem value="option2">Option 2</SelectItem>
|
||||
<SelectItem value="option3">Option 3</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<RadioGroup defaultValue="option-one">
|
||||
<div className="flex items-center space-x-2">
|
||||
<RadioGroupItem
|
||||
value="option-one"
|
||||
id="option-one"
|
||||
/>
|
||||
<Label htmlFor="option-one">Option One</Label>
|
||||
</div>
|
||||
<div className="flex items-center space-x-2">
|
||||
<RadioGroupItem
|
||||
value="option-two"
|
||||
id="option-two"
|
||||
/>
|
||||
<Label htmlFor="option-two">Option Two</Label>
|
||||
</div>
|
||||
</RadioGroup>
|
||||
<div className="flex items-center space-x-2">
|
||||
<Switch id="notifications" />
|
||||
<Label htmlFor="notifications">
|
||||
Enable notifications
|
||||
</Label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Cards */}
|
||||
<div className="grid gap-4 md:grid-cols-2">
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Card Title</CardTitle>
|
||||
<CardDescription>
|
||||
Card description goes here
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
This is how cards appear with your custom theme. The
|
||||
borders, shadows, and colors all adapt to your
|
||||
selections.
|
||||
</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Statistics</CardTitle>
|
||||
<CardDescription>
|
||||
Your performance metrics
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-2">
|
||||
<div className="flex justify-between">
|
||||
<span className="text-sm">Progress</span>
|
||||
<span className="text-sm font-medium">75%</span>
|
||||
</div>
|
||||
<Progress value={75} className="h-2" />
|
||||
<div className="flex gap-2 pt-2">
|
||||
<Badge>Active</Badge>
|
||||
<Badge variant="secondary">Updated</Badge>
|
||||
<Badge variant="outline">New</Badge>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
{/* Alert */}
|
||||
<Alert>
|
||||
<AlertTitle>Heads up!</AlertTitle>
|
||||
<AlertDescription>
|
||||
You can customize every aspect of your theme using the
|
||||
controls on the left. Changes are reflected in
|
||||
real-time.
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</TabsContent>
|
||||
|
||||
<TabsContent value="components" className="mt-6">
|
||||
<div
|
||||
className="theme-components-container"
|
||||
data-theme="custom-components"
|
||||
>
|
||||
<style
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: `
|
||||
.theme-components-container[data-theme="custom-components"] {
|
||||
--background: ${hexToHSL(colors.background)};
|
||||
--foreground: ${hexToHSL(colors.foreground)};
|
||||
--card: ${hexToHSL(colors.card)};
|
||||
--card-foreground: ${hexToHSL(colors["card-foreground"])};
|
||||
--popover: ${hexToHSL(colors.popover)};
|
||||
--popover-foreground: ${hexToHSL(colors["popover-foreground"])};
|
||||
--primary: ${hexToHSL(colors.primary)};
|
||||
--primary-foreground: ${hexToHSL(colors["primary-foreground"])};
|
||||
--secondary: ${hexToHSL(colors.secondary)};
|
||||
--secondary-foreground: ${hexToHSL(colors["secondary-foreground"])};
|
||||
--muted: ${hexToHSL(colors.muted)};
|
||||
--muted-foreground: ${hexToHSL(colors["muted-foreground"])};
|
||||
--accent: ${hexToHSL(colors.accent)};
|
||||
--accent-foreground: ${hexToHSL(colors["accent-foreground"])};
|
||||
--destructive: ${hexToHSL(colors.destructive)};
|
||||
--destructive-foreground: ${hexToHSL(colors["destructive-foreground"])};
|
||||
--border: ${hexToHSL(colors.border)};
|
||||
--input: ${hexToHSL(colors.input)};
|
||||
--ring: ${hexToHSL(colors.ring)};
|
||||
--radius: ${radius}rem;
|
||||
}
|
||||
.theme-components-container[data-theme="custom-components"] * {
|
||||
border-radius: calc(var(--radius) * 1);
|
||||
}
|
||||
`,
|
||||
}}
|
||||
/>
|
||||
<div
|
||||
className="rounded-lg border bg-background p-8 space-y-8"
|
||||
style={{
|
||||
backgroundColor: `hsl(var(--background))`,
|
||||
color: `hsl(var(--foreground))`,
|
||||
borderColor: `hsl(var(--border))`,
|
||||
}}
|
||||
>
|
||||
<div className="space-y-6">
|
||||
<div>
|
||||
<h3 className="text-lg font-semibold mb-4">
|
||||
Button Variants
|
||||
</h3>
|
||||
<div className="flex flex-wrap gap-3">
|
||||
<Button>Primary</Button>
|
||||
<Button variant="secondary">Secondary</Button>
|
||||
<Button variant="outline">Outline</Button>
|
||||
<Button variant="ghost">Ghost</Button>
|
||||
<Button variant="destructive">Destructive</Button>
|
||||
<Button variant="link">Link</Button>
|
||||
<Button size="sm">Small</Button>
|
||||
<Button size="lg">Large</Button>
|
||||
<Button disabled>Disabled</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Separator />
|
||||
|
||||
<div>
|
||||
<h3 className="text-lg font-semibold mb-4">
|
||||
Badge Variants
|
||||
</h3>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
<Badge>Default</Badge>
|
||||
<Badge variant="secondary">Secondary</Badge>
|
||||
<Badge variant="outline">Outline</Badge>
|
||||
<Badge variant="destructive">Destructive</Badge>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Separator />
|
||||
|
||||
<div>
|
||||
<h3 className="text-lg font-semibold mb-4">Typography</h3>
|
||||
<div className="space-y-2">
|
||||
<h1 className="text-4xl font-bold">Heading 1</h1>
|
||||
<h2 className="text-3xl font-bold">Heading 2</h2>
|
||||
<h3 className="text-2xl font-bold">Heading 3</h3>
|
||||
<h4 className="text-xl font-bold">Heading 4</h4>
|
||||
<p className="text-lg">Large paragraph text</p>
|
||||
<p>Regular paragraph text</p>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
Muted text
|
||||
</p>
|
||||
<p className="text-xs">Small text</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</TabsContent>
|
||||
|
||||
<TabsContent value="code" className="mt-6">
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>CSS Variables</CardTitle>
|
||||
<CardDescription>
|
||||
Copy this code and paste it into your globals.css file
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="relative">
|
||||
<pre className="overflow-auto rounded-lg bg-muted p-6 text-sm">
|
||||
<code className="language-css">{generateCSS()}</code>
|
||||
</pre>
|
||||
<Button
|
||||
onClick={copyCSS}
|
||||
size="sm"
|
||||
className="absolute right-2 top-2"
|
||||
variant="ghost"
|
||||
>
|
||||
{copied ? (
|
||||
<Check className="h-4 w-4" />
|
||||
) : (
|
||||
<Copy className="h-4 w-4" />
|
||||
)}
|
||||
</Button>
|
||||
</div>
|
||||
<div className="mt-4 rounded-lg bg-muted/50 p-4">
|
||||
<p className="text-sm font-medium mb-2">
|
||||
Usage Instructions:
|
||||
</p>
|
||||
<ol className="text-sm text-muted-foreground space-y-1 list-decimal list-inside">
|
||||
<li>Copy the CSS variables above</li>
|
||||
<li>
|
||||
Open your project's global CSS file (usually globals.css
|
||||
or app.css)
|
||||
</li>
|
||||
<li>
|
||||
Replace the existing :root variables with your new theme
|
||||
</li>
|
||||
<li>Save the file and refresh your application</li>
|
||||
</ol>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</TabsContent>
|
||||
</Tabs>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
return <ThemeGeneratorClient />
|
||||
}
|
||||
|
||||
@@ -0,0 +1,797 @@
|
||||
"use client"
|
||||
|
||||
import * as React from "react"
|
||||
import {
|
||||
Check,
|
||||
Copy,
|
||||
Download,
|
||||
Monitor,
|
||||
Moon,
|
||||
Palette,
|
||||
RotateCcw,
|
||||
Settings2,
|
||||
Shuffle,
|
||||
Sparkles,
|
||||
Sun,
|
||||
} from "lucide-react"
|
||||
import { HexColorInput, HexColorPicker } from "react-colorful"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
import {
|
||||
Alert,
|
||||
AlertDescription,
|
||||
AlertTitle,
|
||||
} from "@/registry/default/ui/alert"
|
||||
import { Badge } from "@/registry/default/ui/badge"
|
||||
import { Button } from "@/registry/default/ui/button"
|
||||
import {
|
||||
Card,
|
||||
CardContent,
|
||||
CardDescription,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
} from "@/registry/default/ui/card"
|
||||
import { Checkbox } from "@/registry/default/ui/checkbox"
|
||||
import { Input } from "@/registry/default/ui/input"
|
||||
import { Label } from "@/registry/default/ui/label"
|
||||
import {
|
||||
Popover,
|
||||
PopoverContent,
|
||||
PopoverTrigger,
|
||||
} from "@/registry/default/ui/popover"
|
||||
import { Progress } from "@/registry/default/ui/progress"
|
||||
import { RadioGroup, RadioGroupItem } from "@/registry/default/ui/radio-group"
|
||||
import { ScrollArea } from "@/registry/default/ui/scroll-area"
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from "@/registry/default/ui/select"
|
||||
import { Separator } from "@/registry/default/ui/separator"
|
||||
import { Slider } from "@/registry/default/ui/slider"
|
||||
import { Switch } from "@/registry/default/ui/switch"
|
||||
import {
|
||||
Tabs,
|
||||
TabsContent,
|
||||
TabsList,
|
||||
TabsTrigger,
|
||||
} from "@/registry/default/ui/tabs"
|
||||
import { Textarea } from "@/registry/default/ui/textarea"
|
||||
|
||||
const DEFAULT_LIGHT_COLORS = {
|
||||
background: "#ffffff",
|
||||
foreground: "#0a0a0a",
|
||||
card: "#ffffff",
|
||||
"card-foreground": "#0a0a0a",
|
||||
popover: "#ffffff",
|
||||
"popover-foreground": "#0a0a0a",
|
||||
primary: "#0099ff",
|
||||
"primary-foreground": "#ffffff",
|
||||
secondary: "#f4f4f5",
|
||||
"secondary-foreground": "#0a0a0a",
|
||||
muted: "#f4f4f5",
|
||||
"muted-foreground": "#71717a",
|
||||
accent: "#f4f4f5",
|
||||
"accent-foreground": "#0a0a0a",
|
||||
destructive: "#ef4444",
|
||||
"destructive-foreground": "#ffffff",
|
||||
border: "#e4e4e7",
|
||||
input: "#e4e4e7",
|
||||
ring: "#0099ff",
|
||||
}
|
||||
|
||||
const DEFAULT_DARK_COLORS = {
|
||||
background: "#0a0a0a",
|
||||
foreground: "#fafafa",
|
||||
card: "#0a0a0a",
|
||||
"card-foreground": "#fafafa",
|
||||
popover: "#0a0a0a",
|
||||
"popover-foreground": "#fafafa",
|
||||
primary: "#0099ff",
|
||||
"primary-foreground": "#ffffff",
|
||||
secondary: "#262626",
|
||||
"secondary-foreground": "#fafafa",
|
||||
muted: "#262626",
|
||||
"muted-foreground": "#a3a3a3",
|
||||
accent: "#262626",
|
||||
"accent-foreground": "#fafafa",
|
||||
destructive: "#dc2626",
|
||||
"destructive-foreground": "#ffffff",
|
||||
border: "#262626",
|
||||
input: "#262626",
|
||||
ring: "#0099ff",
|
||||
}
|
||||
|
||||
const PRESET_THEMES = {
|
||||
"Electric Blue": {
|
||||
primary: "#0099ff",
|
||||
secondary: "#1e293b",
|
||||
accent: "#06b6d4",
|
||||
},
|
||||
Forest: {
|
||||
primary: "#10b981",
|
||||
secondary: "#065f46",
|
||||
accent: "#34d399",
|
||||
},
|
||||
Sunset: {
|
||||
primary: "#f97316",
|
||||
secondary: "#dc2626",
|
||||
accent: "#fbbf24",
|
||||
},
|
||||
"Purple Haze": {
|
||||
primary: "#8b5cf6",
|
||||
secondary: "#7c3aed",
|
||||
accent: "#a78bfa",
|
||||
},
|
||||
Ocean: {
|
||||
primary: "#0ea5e9",
|
||||
secondary: "#0284c7",
|
||||
accent: "#38bdf8",
|
||||
},
|
||||
}
|
||||
|
||||
export function ThemeGeneratorClient() {
|
||||
const [colors, setColors] = React.useState(DEFAULT_LIGHT_COLORS)
|
||||
const [radius, setRadius] = React.useState(0.5)
|
||||
const [mode, setMode] = React.useState<"light" | "dark">("light")
|
||||
const [copied, setCopied] = React.useState(false)
|
||||
const [activeColorKey, setActiveColorKey] = React.useState<string | null>(
|
||||
null
|
||||
)
|
||||
|
||||
const updateColor = (key: string, value: string) => {
|
||||
setColors((prev) => ({ ...prev, [key]: value }))
|
||||
}
|
||||
|
||||
const toggleMode = () => {
|
||||
const newMode = mode === "light" ? "dark" : "light"
|
||||
setMode(newMode)
|
||||
setColors(newMode === "light" ? DEFAULT_LIGHT_COLORS : DEFAULT_DARK_COLORS)
|
||||
}
|
||||
|
||||
const hexToHSL = (hex: string) => {
|
||||
const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex)
|
||||
if (!result) return "0 0% 0%"
|
||||
|
||||
const r = parseInt(result[1], 16) / 255
|
||||
const g = parseInt(result[2], 16) / 255
|
||||
const b = parseInt(result[3], 16) / 255
|
||||
|
||||
const max = Math.max(r, g, b)
|
||||
const min = Math.min(r, g, b)
|
||||
let h = 0,
|
||||
s = 0,
|
||||
l = (max + min) / 2
|
||||
|
||||
if (max !== min) {
|
||||
const d = max - min
|
||||
s = l > 0.5 ? d / (2 - max - min) : d / (max + min)
|
||||
switch (max) {
|
||||
case r:
|
||||
h = ((g - b) / d + (g < b ? 6 : 0)) / 6
|
||||
break
|
||||
case g:
|
||||
h = ((b - r) / d + 2) / 6
|
||||
break
|
||||
case b:
|
||||
h = ((r - g) / d + 4) / 6
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
return `${Math.round(h * 360)} ${Math.round(s * 100)}% ${Math.round(l * 100)}%`
|
||||
}
|
||||
|
||||
const generateCSS = () => {
|
||||
const css = `@layer base {
|
||||
:root {
|
||||
${Object.entries(colors)
|
||||
.map(([key, value]) => ` --${key}: ${hexToHSL(value)};`)
|
||||
.join("\n")}
|
||||
--radius: ${radius}rem;
|
||||
}
|
||||
|
||||
.dark {
|
||||
${Object.entries(colors)
|
||||
.map(([key, value]) => ` --${key}: ${hexToHSL(value)};`)
|
||||
.join("\n")}
|
||||
}
|
||||
}`
|
||||
return css
|
||||
}
|
||||
|
||||
const copyCSS = async () => {
|
||||
try {
|
||||
await navigator.clipboard.writeText(generateCSS())
|
||||
setCopied(true)
|
||||
setTimeout(() => setCopied(false), 2000)
|
||||
} catch (err) {
|
||||
console.error("Failed to copy CSS:", err)
|
||||
}
|
||||
}
|
||||
|
||||
const applyPresetTheme = (presetName: string) => {
|
||||
const preset = PRESET_THEMES[presetName as keyof typeof PRESET_THEMES]
|
||||
if (preset) {
|
||||
setColors((prev) => ({
|
||||
...prev,
|
||||
primary: preset.primary,
|
||||
"primary-foreground": mode === "dark" ? "#ffffff" : "#ffffff",
|
||||
secondary: preset.secondary,
|
||||
"secondary-foreground": mode === "dark" ? "#fafafa" : "#0a0a0a",
|
||||
accent: preset.accent,
|
||||
"accent-foreground": mode === "dark" ? "#fafafa" : "#0a0a0a",
|
||||
ring: preset.primary,
|
||||
}))
|
||||
}
|
||||
}
|
||||
|
||||
const randomizeTheme = () => {
|
||||
const randomHex = () => {
|
||||
const hue = Math.floor(Math.random() * 360)
|
||||
const saturation = 50 + Math.floor(Math.random() * 50)
|
||||
const lightness = 40 + Math.floor(Math.random() * 30)
|
||||
return hslToHex(hue, saturation, lightness)
|
||||
}
|
||||
|
||||
const primary = randomHex()
|
||||
const secondary = randomHex()
|
||||
const accent = randomHex()
|
||||
|
||||
setColors((prev) => ({
|
||||
...prev,
|
||||
primary,
|
||||
"primary-foreground": "#ffffff",
|
||||
secondary,
|
||||
"secondary-foreground": mode === "dark" ? "#fafafa" : "#0a0a0a",
|
||||
accent,
|
||||
"accent-foreground": mode === "dark" ? "#fafafa" : "#0a0a0a",
|
||||
ring: primary,
|
||||
}))
|
||||
}
|
||||
|
||||
const hslToHex = (h: number, s: number, l: number) => {
|
||||
l /= 100
|
||||
const a = (s * Math.min(l, 1 - l)) / 100
|
||||
const f = (n: number) => {
|
||||
const k = (n + h / 30) % 12
|
||||
const color = l - a * Math.max(Math.min(k - 3, 9 - k, 1), -1)
|
||||
return Math.round(255 * color)
|
||||
.toString(16)
|
||||
.padStart(2, "0")
|
||||
}
|
||||
return `#${f(0)}${f(8)}${f(4)}`
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="container max-w-[1600px] py-6">
|
||||
{/* Header */}
|
||||
<div className="mb-8 flex items-center justify-between">
|
||||
<div>
|
||||
<h1 className="text-4xl font-bold tracking-tight flex items-center gap-3">
|
||||
<Palette className="h-8 w-8 text-primary" />
|
||||
Theme Generator
|
||||
</h1>
|
||||
<p className="mt-2 text-lg text-muted-foreground">
|
||||
Create beautiful themes with live preview and instant export
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex items-center gap-4">
|
||||
<Button
|
||||
variant="outline"
|
||||
size="icon"
|
||||
onClick={toggleMode}
|
||||
className="relative"
|
||||
>
|
||||
{mode === "light" ? (
|
||||
<Moon className="h-5 w-5" />
|
||||
) : (
|
||||
<Sun className="h-5 w-5" />
|
||||
)}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="grid gap-8 lg:grid-cols-[420px_1fr]">
|
||||
{/* Enhanced Controls Sidebar */}
|
||||
<div className="space-y-6">
|
||||
{/* Preset Themes */}
|
||||
<Card className="overflow-hidden">
|
||||
<CardHeader className="bg-gradient-to-r from-primary/10 to-primary/5">
|
||||
<CardTitle className="flex items-center gap-2">
|
||||
<Sparkles className="h-5 w-5" />
|
||||
Preset Themes
|
||||
</CardTitle>
|
||||
<CardDescription>Start with a pre-designed theme</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="pt-6">
|
||||
<div className="grid grid-cols-2 gap-2">
|
||||
{Object.keys(PRESET_THEMES).map((presetName) => (
|
||||
<Button
|
||||
key={presetName}
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() => applyPresetTheme(presetName)}
|
||||
className="justify-start text-xs"
|
||||
>
|
||||
{presetName}
|
||||
</Button>
|
||||
))}
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* Color Customization */}
|
||||
<Card className="overflow-hidden">
|
||||
<CardHeader>
|
||||
<CardTitle className="flex items-center gap-2">
|
||||
<Settings2 className="h-5 w-5" />
|
||||
Color Palette
|
||||
</CardTitle>
|
||||
<CardDescription>Fine-tune your color scheme</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-3">
|
||||
<ScrollArea className="h-[400px] pr-4">
|
||||
<div className="space-y-3">
|
||||
{Object.entries(colors).map(([key, value]) => (
|
||||
<div key={key} className="space-y-1.5">
|
||||
<Label className="text-xs font-medium capitalize">
|
||||
{key.replace(/-/g, " ")}
|
||||
</Label>
|
||||
<Popover
|
||||
open={activeColorKey === key}
|
||||
onOpenChange={(open) =>
|
||||
setActiveColorKey(open ? key : null)
|
||||
}
|
||||
>
|
||||
<PopoverTrigger asChild>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
className="w-full justify-between font-mono text-xs h-9"
|
||||
>
|
||||
<div className="flex items-center gap-2">
|
||||
<div
|
||||
className="h-5 w-5 rounded-md border shadow-sm"
|
||||
style={{ backgroundColor: value }}
|
||||
/>
|
||||
<span>{value.toUpperCase()}</span>
|
||||
</div>
|
||||
<div className="text-[10px] text-muted-foreground">
|
||||
HSL: {hexToHSL(value).split(" ").join(", ")}
|
||||
</div>
|
||||
</Button>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent className="w-auto p-3" align="start">
|
||||
<div className="space-y-3">
|
||||
<HexColorPicker
|
||||
color={value}
|
||||
onChange={(v) => updateColor(key, v)}
|
||||
style={{ width: 200, height: 200 }}
|
||||
/>
|
||||
<HexColorInput
|
||||
color={value}
|
||||
onChange={(v) => updateColor(key, v)}
|
||||
className="w-full rounded-md border px-3 py-1 text-sm font-mono"
|
||||
prefixed
|
||||
/>
|
||||
</div>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</ScrollArea>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* Border Radius Control */}
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="text-sm">Border Radius</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="space-y-3">
|
||||
<Slider
|
||||
value={[radius]}
|
||||
onValueChange={(v) => setRadius(v[0])}
|
||||
min={0}
|
||||
max={1.5}
|
||||
step={0.05}
|
||||
className="w-full"
|
||||
/>
|
||||
<div className="flex justify-between text-xs text-muted-foreground">
|
||||
<span>Sharp</span>
|
||||
<span className="font-mono">{radius}rem</span>
|
||||
<span>Rounded</span>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* Action Buttons */}
|
||||
<div className="grid grid-cols-2 gap-2">
|
||||
<Button
|
||||
onClick={randomizeTheme}
|
||||
variant="outline"
|
||||
size="sm"
|
||||
className="w-full"
|
||||
>
|
||||
<Shuffle className="mr-2 h-4 w-4" />
|
||||
Randomize
|
||||
</Button>
|
||||
<Button
|
||||
onClick={() =>
|
||||
setColors(
|
||||
mode === "light" ? DEFAULT_LIGHT_COLORS : DEFAULT_DARK_COLORS
|
||||
)
|
||||
}
|
||||
variant="outline"
|
||||
size="sm"
|
||||
className="w-full"
|
||||
>
|
||||
<RotateCcw className="mr-2 h-4 w-4" />
|
||||
Reset
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<Button onClick={copyCSS} className="w-full" size="lg">
|
||||
{copied ? (
|
||||
<>
|
||||
<Check className="mr-2 h-4 w-4" />
|
||||
Copied!
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<Copy className="mr-2 h-4 w-4" />
|
||||
Copy CSS Variables
|
||||
</>
|
||||
)}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{/* Enhanced Preview Area */}
|
||||
<div className="space-y-6">
|
||||
<Tabs defaultValue="preview" className="w-full">
|
||||
<TabsList className="grid w-full grid-cols-3">
|
||||
<TabsTrigger value="preview">Live Preview</TabsTrigger>
|
||||
<TabsTrigger value="components">Components</TabsTrigger>
|
||||
<TabsTrigger value="code">Export Code</TabsTrigger>
|
||||
</TabsList>
|
||||
|
||||
<TabsContent value="preview" className="mt-6">
|
||||
<div className="theme-preview-container" data-theme="custom">
|
||||
<style
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: `
|
||||
.theme-preview-container[data-theme="custom"] {
|
||||
--background: ${hexToHSL(colors.background)};
|
||||
--foreground: ${hexToHSL(colors.foreground)};
|
||||
--card: ${hexToHSL(colors.card)};
|
||||
--card-foreground: ${hexToHSL(colors["card-foreground"])};
|
||||
--popover: ${hexToHSL(colors.popover)};
|
||||
--popover-foreground: ${hexToHSL(colors["popover-foreground"])};
|
||||
--primary: ${hexToHSL(colors.primary)};
|
||||
--primary-foreground: ${hexToHSL(colors["primary-foreground"])};
|
||||
--secondary: ${hexToHSL(colors.secondary)};
|
||||
--secondary-foreground: ${hexToHSL(colors["secondary-foreground"])};
|
||||
--muted: ${hexToHSL(colors.muted)};
|
||||
--muted-foreground: ${hexToHSL(colors["muted-foreground"])};
|
||||
--accent: ${hexToHSL(colors.accent)};
|
||||
--accent-foreground: ${hexToHSL(colors["accent-foreground"])};
|
||||
--destructive: ${hexToHSL(colors.destructive)};
|
||||
--destructive-foreground: ${hexToHSL(colors["destructive-foreground"])};
|
||||
--border: ${hexToHSL(colors.border)};
|
||||
--input: ${hexToHSL(colors.input)};
|
||||
--ring: ${hexToHSL(colors.ring)};
|
||||
--radius: ${radius}rem;
|
||||
}
|
||||
.theme-preview-container[data-theme="custom"] * {
|
||||
border-radius: calc(var(--radius) * 1);
|
||||
}
|
||||
`,
|
||||
}}
|
||||
/>
|
||||
<div
|
||||
className="rounded-lg border bg-background p-8 shadow-sm"
|
||||
style={{
|
||||
backgroundColor: `hsl(var(--background))`,
|
||||
color: `hsl(var(--foreground))`,
|
||||
borderColor: `hsl(var(--border))`,
|
||||
}}
|
||||
>
|
||||
<div className="space-y-8">
|
||||
{/* Hero Section Preview */}
|
||||
<div className="space-y-4">
|
||||
<h2 className="text-3xl font-bold">
|
||||
Welcome to Your App
|
||||
</h2>
|
||||
<p className="text-lg text-muted-foreground">
|
||||
This is how your theme looks in a real application
|
||||
context
|
||||
</p>
|
||||
<div className="flex flex-wrap gap-3">
|
||||
<Button size="lg">Get Started</Button>
|
||||
<Button size="lg" variant="secondary">
|
||||
Learn More
|
||||
</Button>
|
||||
<Button size="lg" variant="outline">
|
||||
Documentation
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Separator />
|
||||
|
||||
{/* Form Elements */}
|
||||
<div className="grid gap-6 md:grid-cols-2">
|
||||
<div className="space-y-4">
|
||||
<h3 className="text-lg font-semibold">Form Elements</h3>
|
||||
<div className="space-y-3">
|
||||
<div className="space-y-1">
|
||||
<Label htmlFor="email">Email</Label>
|
||||
<Input
|
||||
id="email"
|
||||
placeholder="name@example.com"
|
||||
type="email"
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-1">
|
||||
<Label htmlFor="message">Message</Label>
|
||||
<Textarea
|
||||
id="message"
|
||||
placeholder="Type your message here..."
|
||||
/>
|
||||
</div>
|
||||
<div className="flex items-center space-x-2">
|
||||
<Checkbox id="terms" />
|
||||
<Label htmlFor="terms" className="text-sm">
|
||||
Accept terms and conditions
|
||||
</Label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-4">
|
||||
<h3 className="text-lg font-semibold">Selections</h3>
|
||||
<div className="space-y-3">
|
||||
<Select>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder="Select an option" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="option1">Option 1</SelectItem>
|
||||
<SelectItem value="option2">Option 2</SelectItem>
|
||||
<SelectItem value="option3">Option 3</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<RadioGroup defaultValue="option-one">
|
||||
<div className="flex items-center space-x-2">
|
||||
<RadioGroupItem
|
||||
value="option-one"
|
||||
id="option-one"
|
||||
/>
|
||||
<Label htmlFor="option-one">Option One</Label>
|
||||
</div>
|
||||
<div className="flex items-center space-x-2">
|
||||
<RadioGroupItem
|
||||
value="option-two"
|
||||
id="option-two"
|
||||
/>
|
||||
<Label htmlFor="option-two">Option Two</Label>
|
||||
</div>
|
||||
</RadioGroup>
|
||||
<div className="flex items-center space-x-2">
|
||||
<Switch id="notifications" />
|
||||
<Label htmlFor="notifications">
|
||||
Enable notifications
|
||||
</Label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Cards */}
|
||||
<div className="grid gap-4 md:grid-cols-2">
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Card Title</CardTitle>
|
||||
<CardDescription>
|
||||
Card description goes here
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
This is how cards appear with your custom theme. The
|
||||
borders, shadows, and colors all adapt to your
|
||||
selections.
|
||||
</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Statistics</CardTitle>
|
||||
<CardDescription>
|
||||
Your performance metrics
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-2">
|
||||
<div className="flex justify-between">
|
||||
<span className="text-sm">Progress</span>
|
||||
<span className="text-sm font-medium">75%</span>
|
||||
</div>
|
||||
<Progress value={75} className="h-2" />
|
||||
<div className="flex gap-2 pt-2">
|
||||
<Badge>Active</Badge>
|
||||
<Badge variant="secondary">Updated</Badge>
|
||||
<Badge variant="outline">New</Badge>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
{/* Alert */}
|
||||
<Alert>
|
||||
<AlertTitle>Heads up!</AlertTitle>
|
||||
<AlertDescription>
|
||||
You can customize every aspect of your theme using the
|
||||
controls on the left. Changes are reflected in
|
||||
real-time.
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</TabsContent>
|
||||
|
||||
<TabsContent value="components" className="mt-6">
|
||||
<div
|
||||
className="theme-components-container"
|
||||
data-theme="custom-components"
|
||||
>
|
||||
<style
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: `
|
||||
.theme-components-container[data-theme="custom-components"] {
|
||||
--background: ${hexToHSL(colors.background)};
|
||||
--foreground: ${hexToHSL(colors.foreground)};
|
||||
--card: ${hexToHSL(colors.card)};
|
||||
--card-foreground: ${hexToHSL(colors["card-foreground"])};
|
||||
--popover: ${hexToHSL(colors.popover)};
|
||||
--popover-foreground: ${hexToHSL(colors["popover-foreground"])};
|
||||
--primary: ${hexToHSL(colors.primary)};
|
||||
--primary-foreground: ${hexToHSL(colors["primary-foreground"])};
|
||||
--secondary: ${hexToHSL(colors.secondary)};
|
||||
--secondary-foreground: ${hexToHSL(colors["secondary-foreground"])};
|
||||
--muted: ${hexToHSL(colors.muted)};
|
||||
--muted-foreground: ${hexToHSL(colors["muted-foreground"])};
|
||||
--accent: ${hexToHSL(colors.accent)};
|
||||
--accent-foreground: ${hexToHSL(colors["accent-foreground"])};
|
||||
--destructive: ${hexToHSL(colors.destructive)};
|
||||
--destructive-foreground: ${hexToHSL(colors["destructive-foreground"])};
|
||||
--border: ${hexToHSL(colors.border)};
|
||||
--input: ${hexToHSL(colors.input)};
|
||||
--ring: ${hexToHSL(colors.ring)};
|
||||
--radius: ${radius}rem;
|
||||
}
|
||||
.theme-components-container[data-theme="custom-components"] * {
|
||||
border-radius: calc(var(--radius) * 1);
|
||||
}
|
||||
`,
|
||||
}}
|
||||
/>
|
||||
<div
|
||||
className="rounded-lg border bg-background p-8 space-y-8"
|
||||
style={{
|
||||
backgroundColor: `hsl(var(--background))`,
|
||||
color: `hsl(var(--foreground))`,
|
||||
borderColor: `hsl(var(--border))`,
|
||||
}}
|
||||
>
|
||||
<div className="space-y-6">
|
||||
<div>
|
||||
<h3 className="text-lg font-semibold mb-4">
|
||||
Button Variants
|
||||
</h3>
|
||||
<div className="flex flex-wrap gap-3">
|
||||
<Button>Primary</Button>
|
||||
<Button variant="secondary">Secondary</Button>
|
||||
<Button variant="outline">Outline</Button>
|
||||
<Button variant="ghost">Ghost</Button>
|
||||
<Button variant="destructive">Destructive</Button>
|
||||
<Button variant="link">Link</Button>
|
||||
<Button size="sm">Small</Button>
|
||||
<Button size="lg">Large</Button>
|
||||
<Button disabled>Disabled</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Separator />
|
||||
|
||||
<div>
|
||||
<h3 className="text-lg font-semibold mb-4">
|
||||
Badge Variants
|
||||
</h3>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
<Badge>Default</Badge>
|
||||
<Badge variant="secondary">Secondary</Badge>
|
||||
<Badge variant="outline">Outline</Badge>
|
||||
<Badge variant="destructive">Destructive</Badge>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Separator />
|
||||
|
||||
<div>
|
||||
<h3 className="text-lg font-semibold mb-4">Typography</h3>
|
||||
<div className="space-y-2">
|
||||
<h1 className="text-4xl font-bold">Heading 1</h1>
|
||||
<h2 className="text-3xl font-bold">Heading 2</h2>
|
||||
<h3 className="text-2xl font-bold">Heading 3</h3>
|
||||
<h4 className="text-xl font-bold">Heading 4</h4>
|
||||
<p className="text-lg">Large paragraph text</p>
|
||||
<p>Regular paragraph text</p>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
Muted text
|
||||
</p>
|
||||
<p className="text-xs">Small text</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</TabsContent>
|
||||
|
||||
<TabsContent value="code" className="mt-6">
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>CSS Variables</CardTitle>
|
||||
<CardDescription>
|
||||
Copy this code and paste it into your globals.css file
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="relative">
|
||||
<pre className="overflow-auto rounded-lg bg-muted p-6 text-sm">
|
||||
<code className="language-css">{generateCSS()}</code>
|
||||
</pre>
|
||||
<Button
|
||||
onClick={copyCSS}
|
||||
size="sm"
|
||||
className="absolute right-2 top-2"
|
||||
variant="ghost"
|
||||
>
|
||||
{copied ? (
|
||||
<Check className="h-4 w-4" />
|
||||
) : (
|
||||
<Copy className="h-4 w-4" />
|
||||
)}
|
||||
</Button>
|
||||
</div>
|
||||
<div className="mt-4 rounded-lg bg-muted/50 p-4">
|
||||
<p className="text-sm font-medium mb-2">
|
||||
Usage Instructions:
|
||||
</p>
|
||||
<ol className="text-sm text-muted-foreground space-y-1 list-decimal list-inside">
|
||||
<li>Copy the CSS variables above</li>
|
||||
<li>
|
||||
Open your project's global CSS file (usually globals.css
|
||||
or app.css)
|
||||
</li>
|
||||
<li>
|
||||
Replace the existing :root variables with your new theme
|
||||
</li>
|
||||
<li>Save the file and refresh your application</li>
|
||||
</ol>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</TabsContent>
|
||||
</Tabs>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
/**
|
||||
* POST /api/chat — AI chat about UI components.
|
||||
*
|
||||
* Uses Hanzo AI (zen-coder-flash) to answer questions about
|
||||
* Hanzo UI components, props, patterns, and composition.
|
||||
* Compatible with Vercel AI SDK useChat hook.
|
||||
*/
|
||||
|
||||
import { NextResponse } from 'next/server'
|
||||
|
||||
export const dynamic = "force-static"
|
||||
|
||||
export async function GET() {
|
||||
return NextResponse.json(
|
||||
{ error: 'AI chat requires a server runtime. Use api.hanzo.ai/v1 directly.' },
|
||||
{ status: 501 },
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
/**
|
||||
* GET /api/registry/components/:name — Get component with full source.
|
||||
*
|
||||
* Returns the component JSON including embedded source code.
|
||||
*/
|
||||
|
||||
import { NextRequest, NextResponse } from "next/server"
|
||||
import { getComponent, getComponentMap } from "../../lib"
|
||||
|
||||
export const dynamic = "force-static"
|
||||
export const dynamicParams = false
|
||||
|
||||
export function generateStaticParams() {
|
||||
const map = getComponentMap()
|
||||
return Array.from(map.keys()).map((name) => ({ name }))
|
||||
}
|
||||
|
||||
export async function GET(
|
||||
_req: NextRequest,
|
||||
{ params }: { params: Promise<{ name: string }> }
|
||||
) {
|
||||
const { name } = await params
|
||||
const component = getComponent(name)
|
||||
|
||||
if (!component) {
|
||||
return NextResponse.json(
|
||||
{ error: "Component not found", name },
|
||||
{ status: 404 }
|
||||
)
|
||||
}
|
||||
|
||||
return NextResponse.json(component, {
|
||||
headers: {
|
||||
"Cache-Control": "public, s-maxage=900, stale-while-revalidate=1800",
|
||||
"Access-Control-Allow-Origin": "*",
|
||||
},
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
/**
|
||||
* GET /api/registry/index — Full registry manifest.
|
||||
*
|
||||
* Returns ALL components with source in a single payload.
|
||||
* MCP clients can hydrate their cache with one HTTP call.
|
||||
*/
|
||||
|
||||
import { NextResponse } from "next/server"
|
||||
import { getFullManifest } from "../lib"
|
||||
|
||||
export const dynamic = "force-static"
|
||||
|
||||
export async function GET() {
|
||||
const manifest = getFullManifest()
|
||||
|
||||
return NextResponse.json(manifest, {
|
||||
headers: {
|
||||
"Cache-Control": "public, s-maxage=900, stale-while-revalidate=1800",
|
||||
"Access-Control-Allow-Origin": "*",
|
||||
},
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,117 @@
|
||||
/**
|
||||
* Shared registry data loader for API routes.
|
||||
*
|
||||
* Reads from the built registry JSON files in public/registry/.
|
||||
* These are generated at build time by scripts/build-registry.mts.
|
||||
*/
|
||||
|
||||
import { readFileSync, readdirSync, existsSync } from "fs"
|
||||
import path from "path"
|
||||
|
||||
const REGISTRY_DIR = path.join(process.cwd(), "public/registry")
|
||||
const STYLES_DIR = path.join(REGISTRY_DIR, "styles/default")
|
||||
|
||||
export interface RegistryItem {
|
||||
name: string
|
||||
type: string
|
||||
dependencies?: string[]
|
||||
devDependencies?: string[]
|
||||
registryDependencies?: string[]
|
||||
files: Array<{ name: string; content: string } | string>
|
||||
description?: string
|
||||
category?: string
|
||||
}
|
||||
|
||||
// In-memory cache (populated on first access, lives for the server lifetime)
|
||||
let _index: RegistryItem[] | null = null
|
||||
let _components: Map<string, RegistryItem> | null = null
|
||||
|
||||
function loadIndex(): RegistryItem[] {
|
||||
if (_index) return _index
|
||||
const indexPath = path.join(REGISTRY_DIR, "index.json")
|
||||
if (!existsSync(indexPath)) return []
|
||||
_index = JSON.parse(readFileSync(indexPath, "utf-8")) as RegistryItem[]
|
||||
return _index
|
||||
}
|
||||
|
||||
function loadComponents(): Map<string, RegistryItem> {
|
||||
if (_components) return _components
|
||||
_components = new Map()
|
||||
|
||||
if (!existsSync(STYLES_DIR)) return _components
|
||||
|
||||
const files = readdirSync(STYLES_DIR).filter((f) => f.endsWith(".json"))
|
||||
for (const file of files) {
|
||||
try {
|
||||
const data = JSON.parse(
|
||||
readFileSync(path.join(STYLES_DIR, file), "utf-8")
|
||||
) as RegistryItem
|
||||
_components.set(data.name, data)
|
||||
} catch {
|
||||
// skip malformed files
|
||||
}
|
||||
}
|
||||
return _components
|
||||
}
|
||||
|
||||
/** Get the full component index (names, types, deps — no source). */
|
||||
export function getIndex(): RegistryItem[] {
|
||||
return loadIndex()
|
||||
}
|
||||
|
||||
/** Get all components with full source code. */
|
||||
export function getComponentMap(): Map<string, RegistryItem> {
|
||||
return loadComponents()
|
||||
}
|
||||
|
||||
/** Get a single component by name (with source). */
|
||||
export function getComponent(name: string): RegistryItem | undefined {
|
||||
const map = loadComponents()
|
||||
// Try exact match first
|
||||
let item = map.get(name)
|
||||
if (item) return item
|
||||
|
||||
// Try with -demo suffix stripped
|
||||
item = map.get(`${name}-demo`)
|
||||
return item
|
||||
}
|
||||
|
||||
/** Search components by name/type. */
|
||||
export function searchComponents(query: string): RegistryItem[] {
|
||||
const q = query.toLowerCase()
|
||||
const index = loadIndex()
|
||||
return index.filter(
|
||||
(item) =>
|
||||
item.name.toLowerCase().includes(q) ||
|
||||
item.type?.toLowerCase().includes(q) ||
|
||||
item.description?.toLowerCase().includes(q) ||
|
||||
item.category?.toLowerCase().includes(q)
|
||||
)
|
||||
}
|
||||
|
||||
/** List components filtered by type. */
|
||||
export function listByType(type?: string): RegistryItem[] {
|
||||
const index = loadIndex()
|
||||
if (!type) return index
|
||||
return index.filter((item) => item.type === type || item.type?.includes(type))
|
||||
}
|
||||
|
||||
/** Get full registry manifest (all components with source — single payload). */
|
||||
export function getFullManifest() {
|
||||
const map = loadComponents()
|
||||
const components: Record<string, any> = {}
|
||||
for (const [name, item] of map) {
|
||||
components[name] = item
|
||||
}
|
||||
return {
|
||||
generated_at: Date.now(),
|
||||
total: map.size,
|
||||
components,
|
||||
}
|
||||
}
|
||||
|
||||
/** Invalidate the in-memory cache (call after registry:build). */
|
||||
export function invalidateCache() {
|
||||
_index = null
|
||||
_components = null
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
/**
|
||||
* GET /api/registry/search?q=button — Search components.
|
||||
*/
|
||||
|
||||
import { NextResponse } from "next/server"
|
||||
import { getIndex } from "../lib"
|
||||
|
||||
export const dynamic = "force-static"
|
||||
|
||||
export async function GET() {
|
||||
// Static export: return full index (client-side filtering)
|
||||
const items = getIndex()
|
||||
|
||||
return NextResponse.json(
|
||||
{
|
||||
total: items.length,
|
||||
results: items.map((item) => ({
|
||||
name: item.name,
|
||||
type: item.type,
|
||||
dependencies: item.dependencies,
|
||||
})),
|
||||
},
|
||||
{
|
||||
headers: {
|
||||
"Cache-Control": "public, s-maxage=300, stale-while-revalidate=600",
|
||||
"Access-Control-Allow-Origin": "*",
|
||||
},
|
||||
}
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
/**
|
||||
* POST /api/search — Search UI components via Hanzo Cloud.
|
||||
*
|
||||
* Proxies to Hanzo Cloud search-docs API with the publishable key.
|
||||
* Client-side code hits this route instead of Cloud directly.
|
||||
*/
|
||||
|
||||
import { NextResponse } from 'next/server'
|
||||
|
||||
export const dynamic = "force-static"
|
||||
|
||||
export async function GET() {
|
||||
return NextResponse.json(
|
||||
{ error: 'Search requires a server runtime. Use client-side search.' },
|
||||
{ status: 501 },
|
||||
)
|
||||
}
|
||||
+21
-1
@@ -8,6 +8,7 @@ import { cn } from "@/lib/utils"
|
||||
import { ActiveThemeProvider } from "@/components/active-theme"
|
||||
import { Analytics } from "@/components/analytics"
|
||||
import { ThemeProvider } from "@/components/providers"
|
||||
import { SkipLinks } from "@/components/skip-links"
|
||||
import { TailwindIndicator } from "@/components/tailwind-indicator"
|
||||
import { ThemeSwitcher } from "@/components/theme-switcher"
|
||||
import { Web3Provider } from "@/components/web3-provider"
|
||||
@@ -17,6 +18,11 @@ import {
|
||||
Toaster as NewYorkToaster,
|
||||
} from "@/registry/default/ui/toaster"
|
||||
|
||||
const META_THEME_COLORS = {
|
||||
light: "white",
|
||||
dark: "black",
|
||||
}
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: {
|
||||
default: siteConfig.name,
|
||||
@@ -87,7 +93,19 @@ export default function RootLayout({ children }: RootLayoutProps) {
|
||||
return (
|
||||
<>
|
||||
<html lang="en" suppressHydrationWarning>
|
||||
<head />
|
||||
<head>
|
||||
<script
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: `
|
||||
try {
|
||||
if (localStorage.theme === 'dark' || ((!('theme' in localStorage) || localStorage.theme === 'system') && window.matchMedia('(prefers-color-scheme: dark)').matches)) {
|
||||
document.querySelector('meta[name="theme-color"]').setAttribute('content', '${META_THEME_COLORS.dark}')
|
||||
}
|
||||
} catch (_) {}
|
||||
`,
|
||||
}}
|
||||
/>
|
||||
</head>
|
||||
<body
|
||||
className={cn(
|
||||
"min-h-svh bg-background font-sans antialiased",
|
||||
@@ -100,9 +118,11 @@ export default function RootLayout({ children }: RootLayoutProps) {
|
||||
defaultTheme="system"
|
||||
enableSystem
|
||||
disableTransitionOnChange
|
||||
enableColorScheme
|
||||
>
|
||||
<Web3Provider>
|
||||
<ActiveThemeProvider initialTheme="neutral">
|
||||
<SkipLinks />
|
||||
<div vaul-drawer-wrapper="">
|
||||
<div className="relative flex min-h-svh flex-col bg-background">
|
||||
{children}
|
||||
|
||||
@@ -174,13 +174,13 @@ function BlockViewerToolbar({ styleName }: { styleName: Style["name"] }) {
|
||||
className="gap-1 *:data-[slot=toggle-group-item]:!size-6 *:data-[slot=toggle-group-item]:!rounded-sm"
|
||||
>
|
||||
<ToggleGroupItem value="100" title="Desktop">
|
||||
<Monitor />
|
||||
<Monitor className="!h-3.5 !w-3.5" />
|
||||
</ToggleGroupItem>
|
||||
<ToggleGroupItem value="60" title="Tablet">
|
||||
<Tablet />
|
||||
<Tablet className="!h-3.5 !w-3.5" />
|
||||
</ToggleGroupItem>
|
||||
<ToggleGroupItem value="30" title="Mobile">
|
||||
<Smartphone />
|
||||
<Smartphone className="!h-3.5 !w-3.5" />
|
||||
</ToggleGroupItem>
|
||||
<Separator orientation="vertical" className="!h-4" />
|
||||
<Button
|
||||
@@ -190,9 +190,9 @@ function BlockViewerToolbar({ styleName }: { styleName: Style["name"] }) {
|
||||
asChild
|
||||
title="Open in New Tab"
|
||||
>
|
||||
<Link href={`/view/${styleName}/${item.name}`} target="_blank">
|
||||
<Link href={`/view/${item.name}`} target="_blank">
|
||||
<span className="sr-only">Open in New Tab</span>
|
||||
<Fullscreen />
|
||||
<Fullscreen className="!h-3.5 !w-3.5" />
|
||||
</Link>
|
||||
</Button>
|
||||
<Separator orientation="vertical" className="!h-4" />
|
||||
@@ -207,22 +207,27 @@ function BlockViewerToolbar({ styleName }: { styleName: Style["name"] }) {
|
||||
}
|
||||
}}
|
||||
>
|
||||
<RotateCw />
|
||||
<RotateCw className="!h-3.5 !w-3.5" />
|
||||
<span className="sr-only">Refresh Preview</span>
|
||||
</Button>
|
||||
</ToggleGroup>
|
||||
</div>
|
||||
<Separator orientation="vertical" className="mx-1 !h-4" />
|
||||
<Button
|
||||
variant="outline"
|
||||
className="w-fit gap-1 px-2 shadow-none"
|
||||
size="sm"
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="size-7"
|
||||
onClick={() => {
|
||||
copyToClipboard(`npx hanzo-ui@latest add ${item.name}`)
|
||||
}}
|
||||
title="Copy install command"
|
||||
>
|
||||
{isCopied ? <Check /> : <Terminal />}
|
||||
<span>npx hanzo-ui add {item.name}</span>
|
||||
{isCopied ? (
|
||||
<Check className="!h-3.5 !w-3.5" />
|
||||
) : (
|
||||
<Clipboard className="!h-3.5 !w-3.5" />
|
||||
)}
|
||||
<span className="sr-only">Copy install command</span>
|
||||
</Button>
|
||||
<Separator orientation="vertical" className="mx-1 !h-4" />
|
||||
<OpenInHButton name={item.name} />
|
||||
@@ -243,7 +248,7 @@ function BlockViewerIframe({
|
||||
return (
|
||||
<iframe
|
||||
key={iframeKey}
|
||||
src={`/view/${styleName}/${item.name}`}
|
||||
src={`/view/${item.name}`}
|
||||
height={item.meta?.iframeHeight ?? 930}
|
||||
loading="lazy"
|
||||
className={cn(
|
||||
|
||||
@@ -1,18 +1,19 @@
|
||||
import { cn } from "@/lib/utils"
|
||||
import {
|
||||
Alert,
|
||||
AlertDescription,
|
||||
AlertTitle,
|
||||
} from "@/registry/default/ui/alert"
|
||||
|
||||
interface CalloutProps {
|
||||
icon?: string
|
||||
title?: string
|
||||
children?: React.ReactNode
|
||||
}
|
||||
|
||||
export function Callout({ title, children, icon, ...props }: CalloutProps) {
|
||||
export function Callout({
|
||||
title,
|
||||
children,
|
||||
icon,
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof Alert> & { icon?: string }) {
|
||||
return (
|
||||
<Alert {...props}>
|
||||
<Alert className={cn("bg-muted/50", className)} {...props}>
|
||||
{icon && <span className="mr-4 text-2xl">{icon}</span>}
|
||||
{title && <AlertTitle>{title}</AlertTitle>}
|
||||
<AlertDescription>{children}</AlertDescription>
|
||||
|
||||
@@ -0,0 +1,633 @@
|
||||
"use client"
|
||||
|
||||
import * as React from "react"
|
||||
import { Check, Search, Tag, X } from "lucide-react"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
import { Badge } from "@/registry/default/ui/badge"
|
||||
import { Button } from "@/registry/default/ui/button"
|
||||
import {
|
||||
Command,
|
||||
CommandEmpty,
|
||||
CommandGroup,
|
||||
CommandInput,
|
||||
CommandItem,
|
||||
CommandList,
|
||||
} from "@/registry/default/ui/command"
|
||||
import {
|
||||
Popover,
|
||||
PopoverContent,
|
||||
PopoverTrigger,
|
||||
} from "@/registry/default/ui/popover"
|
||||
import { ScrollArea } from "@/registry/default/ui/scroll-area"
|
||||
import { Tabs, TabsList, TabsTrigger } from "@/registry/default/ui/tabs"
|
||||
|
||||
// Comprehensive Tailwind CSS classes organized by category
|
||||
const TAILWIND_CLASSES = {
|
||||
layout: [
|
||||
"container",
|
||||
"flex",
|
||||
"inline-flex",
|
||||
"grid",
|
||||
"inline-grid",
|
||||
"block",
|
||||
"inline-block",
|
||||
"inline",
|
||||
"hidden",
|
||||
"flex-row",
|
||||
"flex-col",
|
||||
"flex-wrap",
|
||||
"flex-nowrap",
|
||||
"items-start",
|
||||
"items-center",
|
||||
"items-end",
|
||||
"items-baseline",
|
||||
"items-stretch",
|
||||
"justify-start",
|
||||
"justify-center",
|
||||
"justify-end",
|
||||
"justify-between",
|
||||
"justify-around",
|
||||
"justify-evenly",
|
||||
"gap-0",
|
||||
"gap-1",
|
||||
"gap-2",
|
||||
"gap-3",
|
||||
"gap-4",
|
||||
"gap-6",
|
||||
"gap-8",
|
||||
"gap-12",
|
||||
"grid-cols-1",
|
||||
"grid-cols-2",
|
||||
"grid-cols-3",
|
||||
"grid-cols-4",
|
||||
"grid-cols-6",
|
||||
"grid-cols-12",
|
||||
],
|
||||
spacing: [
|
||||
"p-0",
|
||||
"p-1",
|
||||
"p-2",
|
||||
"p-3",
|
||||
"p-4",
|
||||
"p-6",
|
||||
"p-8",
|
||||
"p-12",
|
||||
"p-16",
|
||||
"px-0",
|
||||
"px-1",
|
||||
"px-2",
|
||||
"px-3",
|
||||
"px-4",
|
||||
"px-6",
|
||||
"px-8",
|
||||
"py-0",
|
||||
"py-1",
|
||||
"py-2",
|
||||
"py-3",
|
||||
"py-4",
|
||||
"py-6",
|
||||
"py-8",
|
||||
"pt-0",
|
||||
"pt-2",
|
||||
"pt-4",
|
||||
"pt-6",
|
||||
"pt-8",
|
||||
"pb-0",
|
||||
"pb-2",
|
||||
"pb-4",
|
||||
"pb-6",
|
||||
"pb-8",
|
||||
"pl-2",
|
||||
"pl-4",
|
||||
"pr-2",
|
||||
"pr-4",
|
||||
"m-0",
|
||||
"m-1",
|
||||
"m-2",
|
||||
"m-4",
|
||||
"m-6",
|
||||
"m-8",
|
||||
"mx-auto",
|
||||
"mx-2",
|
||||
"mx-4",
|
||||
"my-2",
|
||||
"my-4",
|
||||
"my-6",
|
||||
"mt-2",
|
||||
"mt-4",
|
||||
"mb-2",
|
||||
"mb-4",
|
||||
"space-x-2",
|
||||
"space-x-4",
|
||||
"space-y-2",
|
||||
"space-y-4",
|
||||
"space-y-6",
|
||||
"space-y-8",
|
||||
],
|
||||
sizing: [
|
||||
"w-full",
|
||||
"w-auto",
|
||||
"w-1/2",
|
||||
"w-1/3",
|
||||
"w-2/3",
|
||||
"w-1/4",
|
||||
"w-3/4",
|
||||
"w-screen",
|
||||
"w-fit",
|
||||
"w-8",
|
||||
"w-12",
|
||||
"w-16",
|
||||
"w-24",
|
||||
"w-32",
|
||||
"w-64",
|
||||
"h-full",
|
||||
"h-auto",
|
||||
"h-screen",
|
||||
"h-fit",
|
||||
"h-8",
|
||||
"h-12",
|
||||
"h-16",
|
||||
"h-24",
|
||||
"h-32",
|
||||
"h-64",
|
||||
"min-h-screen",
|
||||
"min-h-full",
|
||||
"max-w-xs",
|
||||
"max-w-sm",
|
||||
"max-w-md",
|
||||
"max-w-lg",
|
||||
"max-w-xl",
|
||||
"max-w-2xl",
|
||||
"max-w-4xl",
|
||||
"max-w-7xl",
|
||||
"max-w-full",
|
||||
],
|
||||
colors: [
|
||||
"bg-background",
|
||||
"bg-foreground",
|
||||
"bg-card",
|
||||
"bg-primary",
|
||||
"bg-secondary",
|
||||
"bg-muted",
|
||||
"bg-accent",
|
||||
"bg-destructive",
|
||||
"bg-border",
|
||||
"bg-input",
|
||||
"bg-ring",
|
||||
"bg-white",
|
||||
"bg-black",
|
||||
"bg-transparent",
|
||||
"bg-red-500",
|
||||
"bg-blue-500",
|
||||
"bg-green-500",
|
||||
"bg-yellow-500",
|
||||
"bg-purple-500",
|
||||
"text-foreground",
|
||||
"text-background",
|
||||
"text-primary",
|
||||
"text-secondary",
|
||||
"text-muted-foreground",
|
||||
"text-accent-foreground",
|
||||
"text-destructive",
|
||||
"text-white",
|
||||
"text-black",
|
||||
"text-red-500",
|
||||
"text-blue-500",
|
||||
"text-green-500",
|
||||
"border-border",
|
||||
"border-input",
|
||||
"border-primary",
|
||||
"border-secondary",
|
||||
"border-destructive",
|
||||
"border-white",
|
||||
"border-black",
|
||||
"border-transparent",
|
||||
],
|
||||
typography: [
|
||||
"text-xs",
|
||||
"text-sm",
|
||||
"text-base",
|
||||
"text-lg",
|
||||
"text-xl",
|
||||
"text-2xl",
|
||||
"text-3xl",
|
||||
"text-4xl",
|
||||
"font-thin",
|
||||
"font-light",
|
||||
"font-normal",
|
||||
"font-medium",
|
||||
"font-semibold",
|
||||
"font-bold",
|
||||
"font-extrabold",
|
||||
"italic",
|
||||
"not-italic",
|
||||
"uppercase",
|
||||
"lowercase",
|
||||
"capitalize",
|
||||
"normal-case",
|
||||
"underline",
|
||||
"line-through",
|
||||
"no-underline",
|
||||
"text-left",
|
||||
"text-center",
|
||||
"text-right",
|
||||
"text-justify",
|
||||
"leading-none",
|
||||
"leading-tight",
|
||||
"leading-normal",
|
||||
"leading-relaxed",
|
||||
"tracking-tighter",
|
||||
"tracking-tight",
|
||||
"tracking-normal",
|
||||
"tracking-wide",
|
||||
],
|
||||
borders: [
|
||||
"border",
|
||||
"border-0",
|
||||
"border-2",
|
||||
"border-4",
|
||||
"border-t",
|
||||
"border-b",
|
||||
"border-l",
|
||||
"border-r",
|
||||
"border-x",
|
||||
"border-y",
|
||||
"border-solid",
|
||||
"border-dashed",
|
||||
"border-dotted",
|
||||
"border-none",
|
||||
"rounded",
|
||||
"rounded-none",
|
||||
"rounded-sm",
|
||||
"rounded-md",
|
||||
"rounded-lg",
|
||||
"rounded-xl",
|
||||
"rounded-2xl",
|
||||
"rounded-3xl",
|
||||
"rounded-full",
|
||||
"rounded-t",
|
||||
"rounded-b",
|
||||
"rounded-l",
|
||||
"rounded-r",
|
||||
],
|
||||
effects: [
|
||||
"shadow",
|
||||
"shadow-sm",
|
||||
"shadow-md",
|
||||
"shadow-lg",
|
||||
"shadow-xl",
|
||||
"shadow-2xl",
|
||||
"shadow-none",
|
||||
"opacity-0",
|
||||
"opacity-25",
|
||||
"opacity-50",
|
||||
"opacity-75",
|
||||
"opacity-100",
|
||||
"blur",
|
||||
"blur-sm",
|
||||
"blur-md",
|
||||
"blur-lg",
|
||||
"blur-none",
|
||||
"backdrop-blur",
|
||||
"backdrop-blur-sm",
|
||||
"backdrop-blur-md",
|
||||
"grayscale",
|
||||
"invert",
|
||||
"sepia",
|
||||
],
|
||||
transitions: [
|
||||
"transition",
|
||||
"transition-all",
|
||||
"transition-colors",
|
||||
"transition-opacity",
|
||||
"transition-shadow",
|
||||
"transition-transform",
|
||||
"duration-75",
|
||||
"duration-100",
|
||||
"duration-150",
|
||||
"duration-200",
|
||||
"duration-300",
|
||||
"duration-500",
|
||||
"duration-700",
|
||||
"ease-linear",
|
||||
"ease-in",
|
||||
"ease-out",
|
||||
"ease-in-out",
|
||||
"animate-spin",
|
||||
"animate-ping",
|
||||
"animate-pulse",
|
||||
"animate-bounce",
|
||||
],
|
||||
interactivity: [
|
||||
"cursor-auto",
|
||||
"cursor-pointer",
|
||||
"cursor-wait",
|
||||
"cursor-text",
|
||||
"cursor-move",
|
||||
"cursor-not-allowed",
|
||||
"select-none",
|
||||
"select-text",
|
||||
"select-all",
|
||||
"pointer-events-none",
|
||||
"pointer-events-auto",
|
||||
"resize-none",
|
||||
"resize",
|
||||
"hover:opacity-75",
|
||||
"hover:scale-105",
|
||||
"hover:bg-accent",
|
||||
"focus:outline-none",
|
||||
"focus:ring",
|
||||
"focus:ring-primary",
|
||||
"active:scale-95",
|
||||
],
|
||||
positioning: [
|
||||
"static",
|
||||
"fixed",
|
||||
"absolute",
|
||||
"relative",
|
||||
"sticky",
|
||||
"top-0",
|
||||
"right-0",
|
||||
"bottom-0",
|
||||
"left-0",
|
||||
"inset-0",
|
||||
"z-0",
|
||||
"z-10",
|
||||
"z-20",
|
||||
"z-30",
|
||||
"z-40",
|
||||
"z-50",
|
||||
"overflow-auto",
|
||||
"overflow-hidden",
|
||||
"overflow-visible",
|
||||
"overflow-scroll",
|
||||
"overflow-x-auto",
|
||||
"overflow-y-auto",
|
||||
],
|
||||
}
|
||||
|
||||
const CATEGORY_LABELS: Record<keyof typeof TAILWIND_CLASSES, string> = {
|
||||
layout: "Layout",
|
||||
spacing: "Spacing",
|
||||
sizing: "Sizing",
|
||||
colors: "Colors",
|
||||
typography: "Typography",
|
||||
borders: "Borders",
|
||||
effects: "Effects",
|
||||
transitions: "Transitions",
|
||||
interactivity: "Interactivity",
|
||||
positioning: "Positioning",
|
||||
}
|
||||
|
||||
interface ClassAutocompleteProps {
|
||||
value: string
|
||||
onChange: (value: string) => void
|
||||
}
|
||||
|
||||
export function ClassAutocomplete({ value, onChange }: ClassAutocompleteProps) {
|
||||
const [open, setOpen] = React.useState(false)
|
||||
const [search, setSearch] = React.useState("")
|
||||
const [selectedCategory, setSelectedCategory] = React.useState<
|
||||
keyof typeof TAILWIND_CLASSES | "all" | "recent"
|
||||
>("all")
|
||||
const [recentClasses, setRecentClasses] = React.useState<string[]>([])
|
||||
|
||||
// Parse existing classes
|
||||
const existingClasses = React.useMemo(
|
||||
() => value.split(" ").filter(Boolean),
|
||||
[value]
|
||||
)
|
||||
|
||||
// Get all classes
|
||||
const allClasses = React.useMemo(
|
||||
() => Object.values(TAILWIND_CLASSES).flat().sort(),
|
||||
[]
|
||||
)
|
||||
|
||||
// Filter classes based on category and search
|
||||
const filteredClasses = React.useMemo(() => {
|
||||
let classes: string[] = []
|
||||
|
||||
if (selectedCategory === "all") {
|
||||
classes = allClasses
|
||||
} else if (selectedCategory === "recent") {
|
||||
classes = recentClasses
|
||||
} else {
|
||||
classes = TAILWIND_CLASSES[selectedCategory] || []
|
||||
}
|
||||
|
||||
if (search) {
|
||||
classes = classes.filter((cls) =>
|
||||
cls.toLowerCase().includes(search.toLowerCase())
|
||||
)
|
||||
}
|
||||
|
||||
return classes
|
||||
}, [selectedCategory, search, allClasses, recentClasses])
|
||||
|
||||
// Add a class
|
||||
const addClass = (className: string) => {
|
||||
if (!existingClasses.includes(className)) {
|
||||
const newClasses = [...existingClasses, className].join(" ")
|
||||
onChange(newClasses)
|
||||
|
||||
// Add to recent (max 20)
|
||||
setRecentClasses((prev) => {
|
||||
const updated = [className, ...prev.filter((c) => c !== className)]
|
||||
return updated.slice(0, 20)
|
||||
})
|
||||
}
|
||||
setSearch("")
|
||||
setOpen(false)
|
||||
}
|
||||
|
||||
// Remove a class
|
||||
const removeClass = (className: string) => {
|
||||
const newClasses = existingClasses
|
||||
.filter((cls) => cls !== className)
|
||||
.join(" ")
|
||||
onChange(newClasses)
|
||||
}
|
||||
|
||||
// Validate class (simple check if it exists in our list)
|
||||
const isValidClass = (className: string) => {
|
||||
return allClasses.includes(className)
|
||||
}
|
||||
|
||||
// Get category for a class
|
||||
const getClassCategory = (className: string): string => {
|
||||
for (const [category, classes] of Object.entries(TAILWIND_CLASSES)) {
|
||||
if (classes.includes(className)) {
|
||||
return CATEGORY_LABELS[category as keyof typeof TAILWIND_CLASSES]
|
||||
}
|
||||
}
|
||||
return "Custom"
|
||||
}
|
||||
|
||||
// Get preview description
|
||||
const getClassPreview = (className: string): string => {
|
||||
if (className.startsWith("bg-")) return "Background color"
|
||||
if (className.startsWith("text-") && !className.includes("size"))
|
||||
return "Text color/size"
|
||||
if (className.startsWith("p-") || className.startsWith("m-"))
|
||||
return "Spacing"
|
||||
if (className.startsWith("w-") || className.startsWith("h-"))
|
||||
return "Sizing"
|
||||
if (className.startsWith("border")) return "Border style"
|
||||
if (className.startsWith("rounded")) return "Border radius"
|
||||
if (className.startsWith("shadow")) return "Shadow effect"
|
||||
if (className.startsWith("flex") || className.startsWith("grid"))
|
||||
return "Layout"
|
||||
if (className.startsWith("hover:")) return "Hover state"
|
||||
if (className.startsWith("focus:")) return "Focus state"
|
||||
return "Utility class"
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="space-y-2">
|
||||
{/* Applied Classes */}
|
||||
{existingClasses.length > 0 && (
|
||||
<div className="flex flex-wrap gap-1.5 rounded-md border bg-muted/30 p-2">
|
||||
{existingClasses.map((cls) => (
|
||||
<Badge
|
||||
key={cls}
|
||||
variant={isValidClass(cls) ? "secondary" : "destructive"}
|
||||
className="group relative pr-1 text-[10px]"
|
||||
>
|
||||
<span className="max-w-[120px] truncate">{cls}</span>
|
||||
<button
|
||||
onClick={() => removeClass(cls)}
|
||||
className="ml-1 rounded-sm opacity-70 ring-offset-background transition-opacity hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2"
|
||||
>
|
||||
<X className="h-2.5 w-2.5" />
|
||||
</button>
|
||||
</Badge>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Add Class Input */}
|
||||
<Popover open={open} onOpenChange={setOpen}>
|
||||
<PopoverTrigger asChild>
|
||||
<Button
|
||||
variant="outline"
|
||||
role="combobox"
|
||||
aria-expanded={open}
|
||||
className="w-full justify-start text-xs font-normal"
|
||||
>
|
||||
<Search className="mr-2 h-3.5 w-3.5 shrink-0 opacity-50" />
|
||||
{existingClasses.length === 0
|
||||
? "Add Tailwind classes..."
|
||||
: `${existingClasses.length} ${existingClasses.length === 1 ? "class" : "classes"} applied`}
|
||||
</Button>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent className="w-[400px] p-0" align="start">
|
||||
<div className="space-y-2 p-2">
|
||||
{/* Category Tabs */}
|
||||
<Tabs
|
||||
value={selectedCategory}
|
||||
onValueChange={(v) =>
|
||||
setSelectedCategory(v as keyof typeof TAILWIND_CLASSES)
|
||||
}
|
||||
>
|
||||
<TabsList className="grid h-8 w-full grid-cols-5 gap-1 bg-transparent p-0">
|
||||
<TabsTrigger value="all" className="h-7 text-[10px] px-1">
|
||||
All
|
||||
</TabsTrigger>
|
||||
<TabsTrigger value="recent" className="h-7 text-[10px] px-1">
|
||||
Recent
|
||||
</TabsTrigger>
|
||||
<TabsTrigger value="layout" className="h-7 text-[10px] px-1">
|
||||
Layout
|
||||
</TabsTrigger>
|
||||
<TabsTrigger value="spacing" className="h-7 text-[10px] px-1">
|
||||
Space
|
||||
</TabsTrigger>
|
||||
<TabsTrigger value="colors" className="h-7 text-[10px] px-1">
|
||||
Color
|
||||
</TabsTrigger>
|
||||
</TabsList>
|
||||
</Tabs>
|
||||
|
||||
{/* Search */}
|
||||
<Command className="border">
|
||||
<CommandInput
|
||||
placeholder={`Search ${selectedCategory === "all" ? "all" : selectedCategory} classes...`}
|
||||
value={search}
|
||||
onValueChange={setSearch}
|
||||
className="h-8 text-xs"
|
||||
/>
|
||||
<CommandList>
|
||||
<CommandEmpty className="py-6 text-xs text-center text-muted-foreground">
|
||||
No classes found.
|
||||
</CommandEmpty>
|
||||
<CommandGroup>
|
||||
<ScrollArea className="h-[250px]">
|
||||
{filteredClasses.map((cls) => {
|
||||
const isApplied = existingClasses.includes(cls)
|
||||
return (
|
||||
<CommandItem
|
||||
key={cls}
|
||||
value={cls}
|
||||
onSelect={() => addClass(cls)}
|
||||
disabled={isApplied}
|
||||
className="text-xs"
|
||||
>
|
||||
<div className="flex flex-1 items-center justify-between">
|
||||
<div className="flex items-center gap-2">
|
||||
<Check
|
||||
className={cn(
|
||||
"h-3 w-3",
|
||||
isApplied ? "opacity-100" : "opacity-0"
|
||||
)}
|
||||
/>
|
||||
<code className="font-mono">{cls}</code>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<Badge
|
||||
variant="outline"
|
||||
className="text-[9px] font-normal"
|
||||
>
|
||||
{getClassCategory(cls)}
|
||||
</Badge>
|
||||
</div>
|
||||
</div>
|
||||
</CommandItem>
|
||||
)
|
||||
})}
|
||||
</ScrollArea>
|
||||
</CommandGroup>
|
||||
</CommandList>
|
||||
</Command>
|
||||
|
||||
{/* Category Quick Links */}
|
||||
<div className="flex flex-wrap gap-1 border-t pt-2">
|
||||
{Object.entries(CATEGORY_LABELS).map(([key, label]) => (
|
||||
<Button
|
||||
key={key}
|
||||
variant={selectedCategory === key ? "default" : "ghost"}
|
||||
size="sm"
|
||||
onClick={() =>
|
||||
setSelectedCategory(key as keyof typeof TAILWIND_CLASSES)
|
||||
}
|
||||
className="h-6 text-[10px] px-2"
|
||||
>
|
||||
<Tag className="mr-1 h-2.5 w-2.5" />
|
||||
{label}
|
||||
</Button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
|
||||
{/* Helper Text */}
|
||||
<p className="text-[10px] text-muted-foreground">
|
||||
{existingClasses.length === 0
|
||||
? "Click to browse Tailwind classes by category"
|
||||
: `${allClasses.length - existingClasses.length} more classes available`}
|
||||
</p>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user