Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
756541fae8 | ||
|
|
a2b0a9d133 | ||
|
|
17d239d11b | ||
|
|
8e28d44e8e | ||
|
|
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 |
@@ -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"]
|
||||
}
|
||||
@@ -4,3 +4,43 @@ updates:
|
||||
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
|
||||
@@ -18,11 +18,11 @@ jobs:
|
||||
with:
|
||||
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
|
||||
@@ -54,11 +54,11 @@ jobs:
|
||||
with:
|
||||
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
|
||||
@@ -92,11 +95,11 @@ jobs:
|
||||
with:
|
||||
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
|
||||
|
||||
@@ -11,7 +11,7 @@ permissions:
|
||||
|
||||
jobs:
|
||||
deprecated:
|
||||
runs-on: ubuntu-latest
|
||||
runs-on: hanzo-build-linux-amd64
|
||||
steps:
|
||||
- name: Checkout PR
|
||||
uses: actions/checkout@v4
|
||||
|
||||
@@ -8,7 +8,7 @@ on:
|
||||
|
||||
jobs:
|
||||
stale:
|
||||
runs-on: ubuntu-latest
|
||||
runs-on: hanzo-build-linux-amd64
|
||||
if: github.repository_owner == 'shadcn-ui'
|
||||
steps:
|
||||
- uses: actions/stale@v9
|
||||
|
||||
@@ -1,5 +1,9 @@
|
||||
name: NPM Publish
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
id-token: write
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
@@ -27,7 +31,7 @@ on:
|
||||
|
||||
jobs:
|
||||
publish:
|
||||
runs-on: ubuntu-latest
|
||||
runs-on: hanzo-build-linux-amd64
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
@@ -39,7 +43,6 @@ jobs:
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: '20'
|
||||
cache: 'pnpm'
|
||||
registry-url: 'https://registry.npmjs.org'
|
||||
|
||||
- name: Install dependencies
|
||||
@@ -48,37 +51,37 @@ jobs:
|
||||
- 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 pkg/ui-mcp && pnpm build
|
||||
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 pkg/checkout && pnpm build
|
||||
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
|
||||
@@ -138,34 +141,34 @@ jobs:
|
||||
|
||||
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 "pkg/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/checkout"
|
||||
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
|
||||
|
||||
@@ -4,11 +4,12 @@ on:
|
||||
push:
|
||||
branches: [main]
|
||||
paths:
|
||||
- 'pkg/*/package.json'
|
||||
- 'pkgs/*/package.json'
|
||||
- 'pkgs/*/package.json'
|
||||
|
||||
jobs:
|
||||
detect-changes:
|
||||
runs-on: ubuntu-latest
|
||||
runs-on: hanzo-build-linux-amd64
|
||||
outputs:
|
||||
packages: ${{ steps.changed.outputs.packages }}
|
||||
steps:
|
||||
@@ -19,37 +20,47 @@ jobs:
|
||||
- name: Detect changed packages
|
||||
id: changed
|
||||
run: |
|
||||
CHANGED_PACKAGES=()
|
||||
for pkg_json in pkg/*/package.json; do
|
||||
CHANGED=()
|
||||
for pkg_json in pkgs/*/package.json pkgs/*/package.json; do
|
||||
[ ! -f "$pkg_json" ] && continue
|
||||
pkg_dir=$(dirname "$pkg_json")
|
||||
pkg_name=$(basename "$pkg_dir")
|
||||
|
||||
# Check if package.json version changed
|
||||
# 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
|
||||
|
||||
# Get old and new versions
|
||||
OLD_VERSION=$(git show HEAD~1:"$pkg_json" 2>/dev/null | jq -r '.version' || echo "0.0.0")
|
||||
NEW_VERSION=$(jq -r '.version' "$pkg_json")
|
||||
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_VERSION" != "$NEW_VERSION" ]; then
|
||||
echo "Version change detected: $pkg_name ($OLD_VERSION -> $NEW_VERSION)"
|
||||
CHANGED_PACKAGES+=("$pkg_name")
|
||||
if [ "$OLD" != "$NEW" ]; then
|
||||
echo "Version change: $PKG_NAME ($OLD -> $NEW)"
|
||||
CHANGED+=("$(basename "$pkg_dir")")
|
||||
fi
|
||||
done
|
||||
|
||||
if [ ${#CHANGED_PACKAGES[@]} -eq 0 ]; then
|
||||
if [ ${#CHANGED[@]} -eq 0 ]; then
|
||||
echo "packages=[]" >> $GITHUB_OUTPUT
|
||||
else
|
||||
JSON=$(printf '%s\n' "${CHANGED_PACKAGES[@]}" | jq -R . | jq -s -c .)
|
||||
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: ubuntu-latest
|
||||
runs-on: hanzo-build-linux-amd64
|
||||
strategy:
|
||||
matrix:
|
||||
package: ${{ fromJson(needs.detect-changes.outputs.packages) }}
|
||||
@@ -57,14 +68,11 @@ jobs:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- uses: pnpm/action-setup@v4
|
||||
with:
|
||||
version: 9
|
||||
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 20
|
||||
registry-url: 'https://registry.npmjs.org'
|
||||
cache: 'pnpm'
|
||||
|
||||
- name: Install dependencies
|
||||
run: pnpm install
|
||||
|
||||
@@ -14,9 +14,9 @@ permissions:
|
||||
|
||||
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
|
||||
@@ -33,7 +33,6 @@ jobs:
|
||||
with:
|
||||
node-version: 20
|
||||
registry-url: "https://registry.npmjs.org"
|
||||
cache: "pnpm"
|
||||
|
||||
- name: Update npm for OIDC support
|
||||
run: npm install -g npm@latest
|
||||
|
||||
@@ -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
-60
@@ -1,75 +1,45 @@
|
||||
name: E2E and Visual Tests
|
||||
name: Test
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [main, develop]
|
||||
pull_request:
|
||||
branches: [main, develop]
|
||||
schedule:
|
||||
# Run tests weekly on Sunday at 2 AM UTC to catch breaking updates
|
||||
- cron: '0 2 * * 0'
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
env:
|
||||
NODE_OPTIONS: --max-old-space-size=8192
|
||||
branches: ["*"]
|
||||
|
||||
jobs:
|
||||
test:
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 60
|
||||
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
|
||||
env:
|
||||
NODE_OPTIONS: --max-old-space-size=8192
|
||||
|
||||
- name: Run component health check
|
||||
run: pnpm health-check
|
||||
continue-on-error: true
|
||||
|
||||
- name: Run E2E tests
|
||||
run: pnpm test:e2e
|
||||
timeout-minutes: 30
|
||||
env:
|
||||
CI: true
|
||||
|
||||
- name: Run visual regression tests
|
||||
run: pnpm test:visual
|
||||
timeout-minutes: 20
|
||||
continue-on-error: true
|
||||
env:
|
||||
CI: true
|
||||
|
||||
- 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
|
||||
|
||||
@@ -13,8 +13,46 @@ on:
|
||||
- "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: ubuntu-latest
|
||||
runs-on: hanzo-build-linux-amd64
|
||||
name: pnpm validate:registries
|
||||
permissions:
|
||||
contents: read
|
||||
@@ -28,6 +66,44 @@ jobs:
|
||||
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
|
||||
|
||||
@@ -5,6 +5,7 @@ node_modules
|
||||
**/node_modules
|
||||
.pnp
|
||||
.pnp.js
|
||||
/pnpm-lock.yaml
|
||||
|
||||
# testing
|
||||
coverage
|
||||
@@ -78,3 +79,6 @@ playwright-report/
|
||||
.fleet
|
||||
|
||||
.notes
|
||||
.claude/settings.local.json
|
||||
pkg/ui/dist-test/
|
||||
app/public/api/
|
||||
|
||||
@@ -1,2 +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
+3
-3
@@ -4,8 +4,8 @@
|
||||
"typescript.preferences.quoteStyle": "single",
|
||||
"eslint.workingDirectories": [
|
||||
{ "pattern": "apps/*/" },
|
||||
{ "pattern": "packages/*/" },
|
||||
{ "pattern": "pkg/*/" }
|
||||
{ "pattern": "pkgs/*/" },
|
||||
{ "pattern": "pkgs/*/" }
|
||||
],
|
||||
"tailwindCSS.classFunctions": ["cva", "cn"],
|
||||
"vitest.debugExclude": [
|
||||
@@ -19,6 +19,6 @@
|
||||
"search.exclude": {
|
||||
"apps/v4/registry/radix-*": true,
|
||||
"apps/v4/public/r/*": true,
|
||||
"packages/shadcn/test/fixtures/*": true
|
||||
"pkgs/shadcn/test/fixtures/*": true
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,190 @@
|
||||
# Hanzo UI Consolidation — one library, all polish, no duplication
|
||||
|
||||
**Goal.** RIP the fragmentation (`@hanzo/data` vs `@hanzo/ui` vs in-console duplicates) into ONE canonical, cross-platform, presentational, clean-room library: **`@hanzo/ui`, built on `@hanzo/gui`**. Preserve every polished component + token; zero loss; no fork.
|
||||
|
||||
**Status.**
|
||||
- **Step 1 — Audit:** complete (this document; four source trees read file-by-file).
|
||||
- **Step 2 — Establish `@hanzo/ui` + migrate the stable foundation:** **DONE & GREEN** (`pkg/ui` is now a real package; `tsc --noEmit` = 0 errors, `vitest` = 12/12).
|
||||
- **Step 3 — Console re-point:** **STAGED, not merged.** Gate is split (backend live, app lanes in-flight) — see [Step 3](#step-3--console-re-point-staged).
|
||||
|
||||
**One decision needs CTO sign-off before publish** — see [Naming / version](#naming--version--the-one-decision).
|
||||
|
||||
---
|
||||
|
||||
## Architecture — three layers, decomplected
|
||||
|
||||
```
|
||||
@hanzo/gui (Tamagui/One) cross-platform primitives: Text, XStack/YStack, Button, Input,
|
||||
│ Card, Popover, Select, Switch, Slider, Spinner, ScrollView …
|
||||
▼
|
||||
@hanzo/data (pkg/data) the metadata-driven RECORD layer: fields → records → views
|
||||
│ (RecordsView, DataTable, BoardView, RecordDetail, field editors)
|
||||
▼
|
||||
@hanzo/ui (pkg/ui) ◄──── THE ONE LIBRARY. Two orthogonal concerns, one package:
|
||||
• product/app layer → import { … } from '@hanzo/ui'
|
||||
• record layer → import { … } from '@hanzo/ui/data' (re-exports @hanzo/data)
|
||||
```
|
||||
|
||||
`@hanzo/data` stays the **source of truth** for the record layer; `@hanzo/ui` **composes** it (`export * from '@hanzo/data'` on the `./data` subpath) rather than copying — so the CMS/ERP/Help app lanes that consume `@hanzo/data` today keep working untouched, and there is exactly one home. This mirrors how the gui base wraps `hanzogui`.
|
||||
|
||||
Both layers own a `DataTable` (product = generic typed `<T>` list; data = field-driven record grid). Keeping the record layer on the `./data` subpath keeps each name unambiguous — no rename, no collision.
|
||||
|
||||
---
|
||||
|
||||
## The preserve-and-merge manifest
|
||||
|
||||
Legend: **✅ migrated** (in `pkg/ui`, GREEN) · **↪ re-exported** (composed from `@hanzo/data`) · **⏳ staged** (Step 3, after app lanes land) · **🔧 pending prop-injection** (app-coupled; decouple before it can live in a presentational lib) · **🏠 stays in console** (app glue, not a UI component).
|
||||
|
||||
### A. Record layer — `@hanzo/data` → `@hanzo/ui/data` ↪
|
||||
|
||||
Source: `pkg/data/src/*` (v1.2.0, clean-room, already published). Surfaced through `@hanzo/ui/data` and `@hanzo/ui/primitives/bases/data`. Nothing re-implemented.
|
||||
|
||||
| Component / module | Source | New home |
|
||||
|---|---|---|
|
||||
| `RecordsView` (flagship: toolbar over table⇆board, filter/sort/search/group, saved views) | `view/RecordsView.tsx`, `view/Toolbar.tsx` | `@hanzo/ui/data` |
|
||||
| `DataTable` (field-driven record grid: sort headers, drag resize/reorder, row select, inline edit, pagination) | `table/DataTable.tsx` | `@hanzo/ui/data` |
|
||||
| `BoardView` (kanban, optimistic drag-move) · `RecordCard` | `board/BoardView.tsx`, `table/RecordCard.tsx` | `@hanzo/ui/data` |
|
||||
| `RecordDetail` / `RecordForm` | `record/RecordDetail.tsx` | `@hanzo/ui/data` |
|
||||
| Field model + registry (`FieldType` ×24, `FieldDefinition`, `registerField`, routers) | `field/{types,registry,registerDefaults,FieldDisplay,FieldInput}` | `@hanzo/ui/data` |
|
||||
| Field displays + **editors** (select/currency/date/relation/file/JSON/boolean/fullName/address…) | `field/{displays,inputs}.tsx` | `@hanzo/ui/data` |
|
||||
| Composable primitives: `Menu`, `CheckBox`, `Toggle`, `Calendar` | `primitives/*` | `@hanzo/ui/data` |
|
||||
| Pure logic: `applyView`, `sortRecords`, `filterRecords`, `searchRecords`, `paginate`, `boardColumns`, … | `{table,board,view}/logic.ts` | `@hanzo/ui/data` (+ `@hanzo/data/*/logic` subpaths for Node hosts) |
|
||||
| Tokens: `tokens`, `TAG_TONES`, `tagTone` | `theme.ts` | **also top-level `@hanzo/ui`** (token surface) |
|
||||
|
||||
### B. Console `components/ui/*` product primitives → `@hanzo/ui/product` ✅ / 🔧
|
||||
|
||||
Source: `console2/src/components/ui/*`. The pure subset was ported **byte-identical** (verified by `diff`) — 100% of the polish preserved (Charts' hover tooltips/axis-ticks/honest-null, DataTable width/flex/hover-gating, Field's 6 variants, SlideOver's focus-trap + scroll-lock, ComboBox's ReDoS-safe filter).
|
||||
|
||||
| Component | Source (`console2/src/components/ui/`) | New home | Status |
|
||||
|---|---|---|---|
|
||||
| `DataTable<T>` (generic typed list) + `Column` | `DataTable.tsx` | `@hanzo/ui` (`product/DataTable`) | ✅ |
|
||||
| `PageHeader` | `PageHeader.tsx` | `@hanzo/ui` | ✅ |
|
||||
| `Field*` (`FieldRow/Text/TextArea/Switch/Select/Slider`) | `Field.tsx` | `@hanzo/ui` | ✅ |
|
||||
| **Charts** (`Sparkline/LineChart/BarChart/Donut/BarRows`, `CHART_PALETTE`) | `Charts.tsx` | `@hanzo/ui` | ✅ |
|
||||
| `Donut` (standalone dependency-free ring) | `Donut.tsx` | `@hanzo/ui` as **`DonutRing`** (alias — Charts.Donut is canonical) | ✅ |
|
||||
| `StatusTag` (health verdict → pill) | `StatusTag.tsx` | `@hanzo/ui` | ✅ |
|
||||
| `EmptyState` (DO/Vercel-class first-run) | `EmptyState.tsx` | `@hanzo/ui` | ✅ (**decoupled** — see §I) |
|
||||
| `Metric*` (`MetricCard/MiniBars/UtilBar/LegendDot/Panel/HintButton`, `SERIES`) | `Metric.tsx` | `@hanzo/ui` (its `Sparkline` preserved as **`MetricSparkline`**) | ✅ |
|
||||
| `ComboBox` + pure `filterOptions`/`isKnownOption` | `ComboBox.tsx`, `combobox/filter.ts` | `@hanzo/ui` | ✅ (+ `filter.test.ts`) |
|
||||
| `PrimaryButton` · `HanzoMark` · `ProductIcon` · `ProviderLogo` | resp. files | `@hanzo/ui` | ✅ |
|
||||
| `SlideOver` (a11y drawer) · `SelectMenu` · `Toast` (`useToast`) | resp. files | `@hanzo/ui` | ✅ (SlideOver import decoupled `~/…/color`→`./color`) |
|
||||
| `Reorder<T>` (pointer DnD) · `FadeIn` · `ThemeToggle` · `color` (`asColor`/`IconLike`) | resp. files | `@hanzo/ui` | ✅ (+ `Reorder.test.ts`) |
|
||||
| `DetailPane` (right-side pane over SlideOver) | `components/DetailPane.tsx` | `@hanzo/ui` | ⏳ (liftable; lands with Step 3 — depends only on SlideOver) |
|
||||
| `ChunkGuard` (stale-deploy chunk-error reload) | `components/ChunkGuard.tsx` | `@hanzo/ui` | ⏳ (fully portable; lands with Step 3) |
|
||||
|
||||
### C. Design tokens + motion → `@hanzo/ui` ✅
|
||||
|
||||
The brief's premise (`globals.css` `t_dark`/`t_light` token blocks) is **corrected**: `t_dark`/`t_light` are `@hanzo/gui` (Tamagui) theme **class names**; the calm values live in three real places, all preserved:
|
||||
|
||||
| Polish | Source | New home |
|
||||
|---|---|---|
|
||||
| Calm dark-first tokens (surface, text, accent `#60a5fa`, tag tones) — raw hex, theme-config independent | `pkg/data/src/theme.ts` | `@hanzo/ui` (top-level `tokens`/`TAG_TONES`/`tagTone`) |
|
||||
| OKLCH shadcn-compatible palette (soft-charcoal `oklch(0.145 0 0)`, `--radius: 0.5rem`, indigo sidebar accent) | `pkgs/ui/style/hanzo-default-colors.css`, `@hanzo/tokens` | unchanged (legacy shadcn line); the gui line uses the hex tokens above |
|
||||
| **Motion vocabulary** — `hz-fade-up` (FadeIn), `hz-collapse`/`hz-slide`/`hz-fade` (shell/drawer), `hz-drag-item`, `hz-skeleton`/`hz-pulse`/`hz-row-in`; all `prefers-reduced-motion`-guarded | `console2/app/globals.css` (motion section) | **`@hanzo/ui/styles/motion.css`** (verbatim) — `import '@hanzo/ui/styles/motion.css'` once at app root |
|
||||
|
||||
### D. Generic DocType renderer → `@hanzo/ui` ⏳ (Step 3, FINAL — active lane)
|
||||
|
||||
Source: `console2/src/components/doctype/*`. **Actively edited by the CMS/ERP/Help lanes (untracked).** Dependency-**injected** (`client: FrameworkClient` prop) — not `~/lib/api`-coupled — so the real move-blockers are `~/lib/framework/{types,fields}` + the `ui/*` primitives (now in `@hanzo/ui`) + `@hanzo/data` (now `@hanzo/ui/data`).
|
||||
|
||||
| File | Class | Step-3 disposition |
|
||||
|---|---|---|
|
||||
| `MediaGrid.tsx` | presentational | → `@hanzo/ui` (cleanest lift; props-only DAM gallery) |
|
||||
| `DocTypeRecords.tsx` / `DocTypeDetail.tsx` | mixed (data-bound shell over `@hanzo/data` views) | shell **stays in console**; renders `@hanzo/ui/data` views (already true) |
|
||||
| `CollectionsBrowser.tsx` | mixed (card grid + install/create orchestration) | presentational card-grid → `@hanzo/ui`; orchestration stays |
|
||||
| `data.ts` | glue (relation loading) | stays in console |
|
||||
|
||||
### E. LivingOverview → `@hanzo/ui` ⏳ (Step 3 — active lane)
|
||||
|
||||
Source: `console2/src/components/products/overview/living/*` (untracked/modified). Pure/presentational parts portable; app-glue concentrated in `registry.ts` (live API clients).
|
||||
|
||||
| Part | Class | Step-3 disposition |
|
||||
|---|---|---|
|
||||
| `motion.ts`, `hooks.ts`, `logic.ts`, `config.ts` (types) | pure | → `@hanzo/ui` (unit-tested; count-up, poll clock, unit formatting) |
|
||||
| `tiles.tsx`, `LivingOverview.tsx` | presentational (reuse `ui/Charts` verbatim) | → `@hanzo/ui` (once `config.ts`'s one `~/lib/products/registry` `ProductIcon` type is swapped for `@hanzo/ui`'s `IconLike`) |
|
||||
| `adapters.ts`, `registry.ts` | glue (map REAL `/v1` sources → `OverviewData`) | **stay in console** |
|
||||
|
||||
### F. Framework / Base-data libs → stay in console 🏠
|
||||
|
||||
`src/lib/framework/*` (DocType wire contract + `FrameworkApi` client + pure `fields.ts` mapper to `@hanzo/data`) and `src/lib/base-data/*` (Base REST client + schema→field mapper) are **app data-access glue**, not UI. They stay in the console. Their pure mappers (`docTypeToFields`, `baseCollectionToFields`) could later publish as a small `@hanzo/base-data` adapter, but they are not UI components and are out of scope for `@hanzo/ui`.
|
||||
|
||||
### G. App-coupled console components → 🔧 pending prop-injection (NOT yet in the lib)
|
||||
|
||||
These import app `~/lib`/session/router/branding and must be made presentational (inject props) before they belong in a host-agnostic lib. Kept in console for now; the injection contract is specified so the eventual lift is mechanical.
|
||||
|
||||
| Component | Coupling | Inject to lift |
|
||||
|---|---|---|
|
||||
| `BackendStateCard` (`BackendState.tsx`) | `~/lib/api` `ApiError` | accept a numeric `status` (or shared error shape) instead of `ApiError` |
|
||||
| `BrandLogo` | session + live IAM `organization()` + `~/config` + branding | `orgName`, `logoUrl` (or a `loadOrgLogo` loader), brand mark/name as props |
|
||||
| `Breadcrumbs` | `next/navigation` + catalog lookups | `crumbs[]` (or `pathname` + resolver) + `onNavigate` |
|
||||
| `Loader` / `BrandMark` | `~/config` + brand logo pkgs | `brand`/`brandName` + the animated-SVG getter |
|
||||
| `States` (`ErrorState`, `OperatorAccessRequired`) | `~/lib/api` + session + branding | the error, the account identity, brand strings |
|
||||
|
||||
---
|
||||
|
||||
## What this pass built (Step 2)
|
||||
|
||||
New in **`pkg/ui`** (was a bare `src/` staging dir — no package):
|
||||
|
||||
- `package.json` — `@hanzo/ui` `8.0.0`, source-published (`files: ["src"]`, exports map to `.ts`), peers `@hanzo/gui >=7.2.2`, `@hanzo/data >=1.2.0`, `react >=19`; BSD-3-Clause. Exports: `.`, `./product`, `./data`, `./primitives/bases/data`, `./styles/motion.css`.
|
||||
- `src/index.ts` — top-level barrel (product layer + tokens).
|
||||
- `src/product/index.ts` — product barrel; **collision-free** (Charts `Sparkline`/`Donut` canonical; Metric's → `MetricSparkline`; standalone ring → `DonutRing`; `ComboOption` from the one filter module) — **zero loss**.
|
||||
- `src/data.ts` — `export * from '@hanzo/data'` (the record layer, one home).
|
||||
- `src/styles/hanzo-motion.css` — the motion vocabulary, verbatim.
|
||||
- `tsconfig.json`, `vitest.config.ts`, `gui.config.ts`, `gui.d.ts`, `.npmignore`, `README.md` — mirror the proven `pkg/data` setup.
|
||||
- **One decouple fix:** `product/EmptyState.tsx` no longer imports `~/lib/products/registry`; it uses the local `IconLike` type — the last host coupling in the product tree is gone.
|
||||
|
||||
**Green:** `tsc --noEmit` = 0 errors (strict); `vitest run` = 12/12 (`combobox/filter`, `Reorder`). Every product component is host-agnostic (imports only `react`, `@hanzo/gui`, `@hanzogui/*`, `@hanzo/data`, relative).
|
||||
|
||||
## Clean-room guarantee (preserved)
|
||||
|
||||
`pkg/data` audited for GPL/Twenty contamination: **none.** No GPL, no copied license headers/SPDX, no Twenty entity/decorator architecture. The field/record model is an independent `FieldType` union + `FieldDefinition` + runtime `Map` registry (records are plain `Record<string, unknown>`). "Twenty" appears **only as benchmark prose** ("Airtable/Twenty-class polish, clean-room"). License: BSD-3-Clause. `@hanzo/ui` inherits the same posture.
|
||||
|
||||
---
|
||||
|
||||
## Naming / version — DECIDED: `@hanzo/ui@8`, shadcn retired to `@hanzo/ui-shadcn`
|
||||
|
||||
**Decision:** the gui-based unified library takes the `@hanzo/ui` name **forward at `8.0.0`** (aligning with the "Hanzo Cloud 8.x" umbrella; major = breaking re-platform). The legacy shadcn/Radix line (`pkgs/ui`, v5.7.0) is **retired by renaming** to `@hanzo/ui-shadcn` (never hard-deleted) — it stays fully alive under the new name, freeing `@hanzo/ui` for v8. Precedent: `pkg/data@1.2.0` already superseded `pkgs/data@1.1.0` under the same name.
|
||||
|
||||
This is **DONE (repo-local):** `pkg/ui/package.json` is `@hanzo/ui@8.0.0`, GREEN. The rest is a coordinated, **sequenced** retire — because publishing `@hanzo/ui@8` (a different, gui-based API with no shadcn `Button/Card/Dialog`) under the name ~20 repos consume for shadcn primitives will BREAK any consumer that resolves to `@8`. So order matters:
|
||||
|
||||
**Blast radius (measured across `~/work/hanzo`).** Declared deps on `@hanzo/ui` in ~20 repos. Most pin `^5.x` (semver-safe from an `@8` bump): paas, chat, platform, app, hanzo.ai, o11y, docs, mdx, hanzobot, ui-repo `pkgs/checkout` `^5.3`, `pkgs/agent-ui` `^5.0`. **Would break on `@8`:** `hanzoai/identity/app` (`"latest"`), ui-repo `pkgs/commerce` (`>=5.0.0`); `app/` uses `workspace:^`. The `ui.hanzo.ai` docs app + `pkgs/{commerce,checkout,agent-ui}` import the shadcn line internally.
|
||||
|
||||
**Publish reality.** `.github/workflows/publish-on-tag.yml` publishes **`pkgs/ui`** (the shadcn line) as `@hanzo/ui` on a `v*` tag — it does not reference `pkg/ui`. So a tag push today publishes shadcn, not v8. Publishing v8 needs the workflow rewired to build/publish `pkg/ui`. `npm` is not authed locally (`npm whoami` empty) — per house rules, publish goes through **CI (self-hosted runners, canonical org `NPM_TOKEN`)**, not a local `npm publish`.
|
||||
|
||||
**Safe sequence to fully land v8 (each step reversible until the tag push):**
|
||||
1. ✅ `pkg/ui` = `@hanzo/ui@8.0.0`, GREEN (done, committed to a branch).
|
||||
2. Rename `pkgs/ui` name → `@hanzo/ui-shadcn`; update the internal ui-repo consumers (`app/`, `pkgs/{commerce,checkout,agent-ui}`) + `check-no-hanzogui`/registry scripts that reference it.
|
||||
3. Migrate external consumers off `@hanzo/ui`→`@hanzo/ui-shadcn` (start with the break-risk ones: `identity` `latest`, `commerce` `>=5`; the `^5` pins are safe to migrate at leisure). ~20 repos — do as a tracked sweep or flag per-repo.
|
||||
4. Rewire `publish-on-tag.yml` (and `release.yml`) to build + publish `pkg/ui` as `@hanzo/ui@8`, and `pkgs/ui-shadcn` as `@hanzo/ui-shadcn`.
|
||||
5. `npm deprecate '@hanzo/ui@<8' 'moved to @hanzo/ui-shadcn; @hanzo/ui@8+ is the @hanzo/gui-based unified lib'`.
|
||||
6. Tag `v8.0.0` → CI publishes `@hanzo/ui@8.0.0`. Verify `npm view @hanzo/ui@8.0.0`.
|
||||
|
||||
**Gated on the user's direct go-ahead** (irreversible / globally-visible / ecosystem-wide): steps 2–6 — the `pkgs/ui` rename, the ~20-repo consumer sweep, the CI rewire, `npm deprecate`, and the `v8.0.0` tag push that triggers publish. These were not executed autonomously.
|
||||
|
||||
---
|
||||
|
||||
## Step 3 — console re-point (STAGED)
|
||||
|
||||
**Gate (per the brief):** cloud `/v1/framework/modules` live **AND** console CMS/ERP/Help native.
|
||||
- ✅ Backend: `/v1/framework/modules[/:module[/install]]` is **implemented + wired** (`cloud/clients/framework/framework.go:71-73`, HIP-0106 order 129, bound in `subsystems.go:130`, real tests).
|
||||
- ⏳ Console: `CmsModule.tsx`, `ErpModule.tsx`, `HelpModule.tsx`, `components/doctype/` are **untracked/in-flight** on `feat/console-native-cms`.
|
||||
|
||||
→ Gate is **split**, so the re-point is **staged, not applied.** I deliberately did **not** touch `console2`'s working tree (it holds the lanes' uncommitted work — editing it would collide and risk loss, the opposite of the goal).
|
||||
|
||||
**Ready-to-apply re-point (mechanical, once the lanes land + merge):**
|
||||
1. `console2` deps: keep `@hanzo/gui`, `@hanzo/data`; add `@hanzo/ui` (`^6.0.0`). (`@hanzo/data` may stay as a direct dep or be dropped in favor of `@hanzo/ui/data` — both resolve to the same source.)
|
||||
2. Re-point imports (delete the now-duplicated in-console copies — extract, don't copy):
|
||||
- `~/components/ui/{DataTable,PageHeader,Field,Charts,Donut,StatusTag,EmptyState,Metric,ComboBox,combobox/filter,PrimaryButton,HanzoMark,ProductIcon,ProviderLogo,SlideOver,SelectMenu,Toast,Reorder,FadeIn,ThemeToggle,color}` → **`@hanzo/ui`** (or `@hanzo/ui/product`). Two spot-renames: Metric's `Sparkline`→`MetricSparkline`, standalone `Donut`→`DonutRing`.
|
||||
- `@hanzo/data` (in `doctype/*`, `base-data/*`, `products/*Module`) → **`@hanzo/ui/data`** (identical surface).
|
||||
- `~/components/{DetailPane,ChunkGuard}` → **`@hanzo/ui`**.
|
||||
- `app/globals.css` motion block → `import '@hanzo/ui/styles/motion.css'` (keep base resets local).
|
||||
- Move the 5 app-coupled components (§G) into the lib only after applying their inject-props contract; until then they stay local.
|
||||
3. Verify **identical render**: `tsc --noEmit` + `vitest` + `next build` green, then headless-Playwright the live pages pixel-same (the polish is byte-identical, so parity is expected).
|
||||
|
||||
**Report:** `@hanzo/ui` is the ONE unified library — product layer + record layer (`@hanzo/data`) + charts + calm tokens + motion, all on `@hanzo/gui`, presentational, cross-platform, clean-room, GREEN. Console re-point is **ready to apply once the CMS/ERP/Help lanes land**; it was not merged to avoid colliding with that in-flight work.
|
||||
|
||||
## Follow-ups (semver-minor, with visual e2e)
|
||||
|
||||
- Collapse the preserved Sparkline/Donut variants to one each (`MetricSparkline`→`Sparkline`, `DonutRing`→`Donut`) once call-sites are proven identical.
|
||||
- Lift §G's five components after applying their prop-injection contracts.
|
||||
- Consider publishing the pure `framework`/`base-data` mappers as a small `@hanzo/base-data` adapter (not UI).
|
||||
+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
|
||||
|
||||
|
||||
@@ -10,7 +10,7 @@ React component library (shadcn/ui fork). 161 components, 24+ blocks, two themes
|
||||
|
||||
```
|
||||
ui/
|
||||
app/ Documentation site (Next.js 15.3.1, React 19)
|
||||
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
|
||||
@@ -18,13 +18,25 @@ ui/
|
||||
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)
|
||||
auth/ Auth components (Firebase optional since v2.6.0)
|
||||
auth-firebase/ Firebase auth (opt-in package)
|
||||
commerce/ E-commerce components
|
||||
react/ React primitives
|
||||
brand/ Branding system
|
||||
brands/ White-label configs (Zoo, Lux)
|
||||
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
|
||||
```
|
||||
|
||||
## Critical: Build Order
|
||||
@@ -78,7 +90,19 @@ import { cn } from '@hanzo/ui/lib/utils'
|
||||
|
||||
## Tech Stack
|
||||
|
||||
React 18.3.1 (19 experimental), Next.js 15.3.1, Tailwind CSS (OKLCH colors), Radix UI, Turborepo + pnpm, Fumadocs (MDX), class-variance-authority.
|
||||
React 19, Next.js 15.3+, Tailwind CSS 4 (OKLCH colors), Radix UI, Turborepo + pnpm, Fumadocs (MDX), class-variance-authority.
|
||||
|
||||
## Upstream Sync
|
||||
|
||||
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.
|
||||
|
||||
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
|
||||
|
||||
@@ -95,11 +119,12 @@ React 18.3.1 (19 experimental), Next.js 15.3.1, Tailwind CSS (OKLCH colors), Rad
|
||||
- Firebase split to optional `@hanzo/auth-firebase` package (Jan 2025)
|
||||
- `@hanzo/auth` v2.6.0 uses pluggable provider registry: `registerAuthProvider('firebase', FirebaseAuthService)`
|
||||
|
||||
## Component Stats (2025-10-18)
|
||||
## Component Stats
|
||||
|
||||
- 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
|
||||
|
||||
## Rules
|
||||
|
||||
|
||||
@@ -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.**
|
||||
|
||||
+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.
|
||||
|
||||
+430
-8
File diff suppressed because one or more lines are too long
+12
-5
@@ -1,8 +1,15 @@
|
||||
// @ts-nocheck
|
||||
import { dynamic } from '@hanzo/docs-mdx/runtime/dynamic';
|
||||
import * as Config from '../source.config';
|
||||
import { dynamic } from "@hanzo/docs-mdx/runtime/dynamic"
|
||||
|
||||
const create = await dynamic<typeof Config, import("@hanzo/docs-mdx/runtime/types").InternalTypeConfig & {
|
||||
DocData: {
|
||||
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"]}});
|
||||
>(
|
||||
Config,
|
||||
{ configPath: "source.config.ts", environment: "next", outDir: ".docs" },
|
||||
{ doc: { passthroughs: ["extractedReferences"] } }
|
||||
)
|
||||
|
||||
+388
-192
File diff suppressed because one or more lines are too long
@@ -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"
|
||||
}
|
||||
}
|
||||
@@ -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",
|
||||
|
||||
@@ -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,8 +1,9 @@
|
||||
"use client"
|
||||
|
||||
import * as React from "react"
|
||||
import { AlertCircle, RefreshCw } from "lucide-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"
|
||||
@@ -64,24 +65,21 @@ function BlockPreview({ blockName }: { blockName: string }) {
|
||||
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)
|
||||
}
|
||||
},
|
||||
[]
|
||||
)
|
||||
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 () => {
|
||||
@@ -92,7 +90,10 @@ function BlockPreview({ blockName }: { blockName: string }) {
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<div ref={containerRef} className="relative aspect-[4/3] min-h-[400px] bg-muted/50">
|
||||
<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">
|
||||
@@ -273,15 +274,14 @@ export default function BuilderPage() {
|
||||
<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>
|
||||
)}
|
||||
{block?.category && block?.category !== "undefined" && (
|
||||
<Badge
|
||||
variant="outline"
|
||||
className="shrink-0 text-xs font-medium"
|
||||
>
|
||||
{block.category}
|
||||
</Badge>
|
||||
)}
|
||||
</div>
|
||||
{block?.subcategory &&
|
||||
block?.subcategory !== "undefined" && (
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -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>
|
||||
)
|
||||
}
|
||||
@@ -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 "@hanzo/docs-core/page-tree"
|
||||
|
||||
import { source } from "@/lib/source"
|
||||
import { absoluteUrl } from "@/lib/utils"
|
||||
|
||||
@@ -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 },
|
||||
)
|
||||
}
|
||||
+5
-5
@@ -2,11 +2,6 @@ import "@/styles/globals.css"
|
||||
|
||||
import { Metadata, Viewport } from "next"
|
||||
|
||||
const META_THEME_COLORS = {
|
||||
light: "white",
|
||||
dark: "black",
|
||||
}
|
||||
|
||||
import { siteConfig } from "@/config/site"
|
||||
import { fontMono, fontSans } from "@/lib/fonts"
|
||||
import { cn } from "@/lib/utils"
|
||||
@@ -23,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,
|
||||
|
||||
@@ -222,7 +222,11 @@ function BlockViewerToolbar({ styleName }: { styleName: Style["name"] }) {
|
||||
}}
|
||||
title="Copy install command"
|
||||
>
|
||||
{isCopied ? <Check className="!h-3.5 !w-3.5" /> : <Clipboard className="!h-3.5 !w-3.5" />}
|
||||
{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" />
|
||||
|
||||
@@ -62,7 +62,15 @@ export function CommandMenu({ ...props }: DialogProps) {
|
||||
const { recentSearches, addRecentSearch, clearRecentSearches } =
|
||||
useRecentSearches()
|
||||
|
||||
const modKey = React.useMemo(() => (isMacOS() ? "⌘" : "Ctrl"), [])
|
||||
// SSR renders without `window`, so the first client (hydration) render MUST
|
||||
// also produce "Ctrl" — computing the platform-specific glyph during render
|
||||
// makes the hydrated text ("⌘" on macOS) diverge from the server text
|
||||
// ("Ctrl") → React #418 (hydration text mismatch). Start from the SSR-stable
|
||||
// value and upgrade to the platform glyph after mount (client-only effect).
|
||||
const [modKey, setModKey] = React.useState("Ctrl")
|
||||
React.useEffect(() => {
|
||||
if (isMacOS()) setModKey("⌘")
|
||||
}, [])
|
||||
|
||||
React.useEffect(() => {
|
||||
const down = (e: KeyboardEvent) => {
|
||||
|
||||
@@ -97,6 +97,82 @@ export const docsConfig: DocsConfig = {
|
||||
href: "/docs/changelog",
|
||||
items: [],
|
||||
},
|
||||
{
|
||||
title: "MCP Server",
|
||||
href: "/docs/mcp",
|
||||
items: [],
|
||||
label: "New",
|
||||
},
|
||||
{
|
||||
title: "About",
|
||||
href: "/docs/about",
|
||||
items: [],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
title: "Installation",
|
||||
items: [
|
||||
{
|
||||
title: "Next.js",
|
||||
href: "/docs/installation/next",
|
||||
items: [],
|
||||
},
|
||||
{
|
||||
title: "Vite",
|
||||
href: "/docs/installation/vite",
|
||||
items: [],
|
||||
},
|
||||
{
|
||||
title: "Remix",
|
||||
href: "/docs/installation/remix",
|
||||
items: [],
|
||||
},
|
||||
{
|
||||
title: "Astro",
|
||||
href: "/docs/installation/astro",
|
||||
items: [],
|
||||
},
|
||||
{
|
||||
title: "Gatsby",
|
||||
href: "/docs/installation/gatsby",
|
||||
items: [],
|
||||
},
|
||||
{
|
||||
title: "Laravel",
|
||||
href: "/docs/installation/laravel",
|
||||
items: [],
|
||||
},
|
||||
{
|
||||
title: "Manual",
|
||||
href: "/docs/installation/manual",
|
||||
items: [],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
title: "Dark Mode",
|
||||
items: [
|
||||
{
|
||||
title: "Next.js",
|
||||
href: "/docs/dark-mode/next",
|
||||
items: [],
|
||||
},
|
||||
{
|
||||
title: "Vite",
|
||||
href: "/docs/dark-mode/vite",
|
||||
items: [],
|
||||
},
|
||||
{
|
||||
title: "Remix",
|
||||
href: "/docs/dark-mode/remix",
|
||||
items: [],
|
||||
},
|
||||
{
|
||||
title: "Astro",
|
||||
href: "/docs/dark-mode/astro",
|
||||
items: [],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
@@ -143,6 +219,12 @@ export const docsConfig: DocsConfig = {
|
||||
href: "/docs/components/button",
|
||||
items: [],
|
||||
},
|
||||
{
|
||||
title: "Button Group",
|
||||
href: "/docs/components/button-group",
|
||||
items: [],
|
||||
label: "New",
|
||||
},
|
||||
{
|
||||
title: "Calendar",
|
||||
href: "/docs/components/calendar",
|
||||
@@ -188,6 +270,30 @@ export const docsConfig: DocsConfig = {
|
||||
href: "/docs/components/data-table",
|
||||
items: [],
|
||||
},
|
||||
{
|
||||
title: "Desktop",
|
||||
href: "/docs/components/desktop",
|
||||
items: [],
|
||||
label: "New",
|
||||
},
|
||||
{
|
||||
title: "Desktop Hooks",
|
||||
href: "/docs/components/desktop-hooks",
|
||||
items: [],
|
||||
label: "New",
|
||||
},
|
||||
{
|
||||
title: "Desktop Spotlight",
|
||||
href: "/docs/components/desktop-spotlight",
|
||||
items: [],
|
||||
label: "New",
|
||||
},
|
||||
{
|
||||
title: "Desktop Window",
|
||||
href: "/docs/components/desktop-window",
|
||||
items: [],
|
||||
label: "New",
|
||||
},
|
||||
{
|
||||
title: "Date Picker",
|
||||
href: "/docs/components/date-picker",
|
||||
@@ -198,6 +304,12 @@ export const docsConfig: DocsConfig = {
|
||||
href: "/docs/components/dialog",
|
||||
items: [],
|
||||
},
|
||||
{
|
||||
title: "Direction",
|
||||
href: "/docs/components/direction",
|
||||
items: [],
|
||||
label: "New",
|
||||
},
|
||||
{
|
||||
title: "Drawer",
|
||||
href: "/docs/components/drawer",
|
||||
@@ -208,6 +320,18 @@ export const docsConfig: DocsConfig = {
|
||||
href: "/docs/components/dropdown-menu",
|
||||
items: [],
|
||||
},
|
||||
{
|
||||
title: "Empty State",
|
||||
href: "/docs/components/empty",
|
||||
items: [],
|
||||
label: "New",
|
||||
},
|
||||
{
|
||||
title: "Field",
|
||||
href: "/docs/components/field",
|
||||
items: [],
|
||||
label: "New",
|
||||
},
|
||||
{
|
||||
title: "Form",
|
||||
href: "/docs/components/form",
|
||||
@@ -223,12 +347,24 @@ export const docsConfig: DocsConfig = {
|
||||
href: "/docs/components/input",
|
||||
items: [],
|
||||
},
|
||||
{
|
||||
title: "Input Group",
|
||||
href: "/docs/components/input-group",
|
||||
items: [],
|
||||
label: "New",
|
||||
},
|
||||
{
|
||||
title: "Input OTP",
|
||||
href: "/docs/components/input-otp",
|
||||
items: [],
|
||||
label: "New",
|
||||
},
|
||||
{
|
||||
title: "Item",
|
||||
href: "/docs/components/item",
|
||||
items: [],
|
||||
label: "New",
|
||||
},
|
||||
{
|
||||
title: "Label",
|
||||
href: "/docs/components/label",
|
||||
@@ -613,6 +749,24 @@ export const docsConfig: DocsConfig = {
|
||||
items: [],
|
||||
label: "New",
|
||||
},
|
||||
{
|
||||
title: "Particles Background",
|
||||
href: "/docs/components/particles-background",
|
||||
items: [],
|
||||
label: "New",
|
||||
},
|
||||
{
|
||||
title: "Grid Pattern",
|
||||
href: "/docs/components/grid-pattern",
|
||||
items: [],
|
||||
label: "New",
|
||||
},
|
||||
{
|
||||
title: "Timeline",
|
||||
href: "/docs/components/timeline",
|
||||
items: [],
|
||||
label: "New",
|
||||
},
|
||||
{
|
||||
title: "Pin List",
|
||||
href: "/docs/components/pin-list",
|
||||
@@ -1028,6 +1182,18 @@ export const docsConfig: DocsConfig = {
|
||||
items: [],
|
||||
label: "New",
|
||||
},
|
||||
{
|
||||
title: "AI Code",
|
||||
href: "/docs/ai/code",
|
||||
items: [],
|
||||
label: "New",
|
||||
},
|
||||
{
|
||||
title: "AI Voice",
|
||||
href: "/docs/ai/voice",
|
||||
items: [],
|
||||
label: "New",
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
@@ -1174,6 +1340,40 @@ export const docsConfig: DocsConfig = {
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
title: "Guides",
|
||||
items: [
|
||||
{
|
||||
title: "Page Builder",
|
||||
href: "/docs/guides/page-builder",
|
||||
items: [],
|
||||
label: "New",
|
||||
},
|
||||
{
|
||||
title: "Visual Workflows",
|
||||
href: "/docs/guides/visual-workflows",
|
||||
items: [],
|
||||
label: "New",
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
title: "Hanzo React",
|
||||
items: [
|
||||
{
|
||||
title: "Getting Started",
|
||||
href: "/docs/hanzo-react/getting-started",
|
||||
items: [],
|
||||
label: "New",
|
||||
},
|
||||
{
|
||||
title: "Hooks",
|
||||
href: "/docs/hanzo-react/hooks",
|
||||
items: [],
|
||||
label: "New",
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
title: "Packages",
|
||||
items: [
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
---
|
||||
title: Contact Forms
|
||||
description: Contact form sections with validation, multiple fields, and submission handling.
|
||||
---
|
||||
|
||||
## Overview
|
||||
|
||||
Contact form blocks provide complete form layouts for collecting user inquiries. They include input validation, loading states, and success confirmation.
|
||||
|
||||
## Features
|
||||
|
||||
- **Form Validation**: Built-in HTML5 and custom validation
|
||||
- **Multiple Fields**: Name, email, subject, and message inputs
|
||||
- **Loading States**: Visual feedback during form submission
|
||||
- **Success Message**: Confirmation after successful submission
|
||||
- **Responsive Design**: Adapts to all screen sizes
|
||||
- **Accessible**: Semantic form elements with proper labels
|
||||
|
||||
## Usage
|
||||
|
||||
```tsx
|
||||
import ContactBlock from "@/registry/default/block/contact"
|
||||
|
||||
export default function Page() {
|
||||
return <ContactBlock />
|
||||
}
|
||||
```
|
||||
@@ -0,0 +1,26 @@
|
||||
---
|
||||
title: CTA Sections
|
||||
description: Call-to-action sections for driving user conversions and signups.
|
||||
---
|
||||
|
||||
## Overview
|
||||
|
||||
CTA (Call-to-Action) blocks are designed to drive user engagement and conversions. They feature prominent buttons, compelling copy, and focused layouts that guide users toward a specific action.
|
||||
|
||||
## Features
|
||||
|
||||
- **Conversion-Focused**: Designed to maximize click-through rates
|
||||
- **Responsive Design**: Adapts to all screen sizes
|
||||
- **Multiple Layouts**: Centered, split, and banner variations
|
||||
- **Button Variants**: Primary, secondary, and ghost CTA styles
|
||||
- **Background Options**: Solid colors, gradients, and image backgrounds
|
||||
|
||||
## Usage
|
||||
|
||||
```tsx
|
||||
import CTABlock from "@/registry/default/block/cta"
|
||||
|
||||
export default function Page() {
|
||||
return <CTABlock />
|
||||
}
|
||||
```
|
||||
@@ -0,0 +1,27 @@
|
||||
---
|
||||
title: FAQ Sections
|
||||
description: Frequently asked questions sections with expandable accordions and search.
|
||||
---
|
||||
|
||||
## Overview
|
||||
|
||||
FAQ blocks present commonly asked questions in organized, expandable layouts. They use accordion patterns for progressive disclosure and optional search filtering.
|
||||
|
||||
## Features
|
||||
|
||||
- **Accordion Layout**: Expandable question/answer pairs
|
||||
- **Search Filtering**: Optional search to find specific questions
|
||||
- **Categories**: Group questions by topic
|
||||
- **Responsive**: Full-width layout that works on all devices
|
||||
- **Accessible**: Keyboard navigation and screen reader support
|
||||
- **Animated**: Smooth expand/collapse transitions
|
||||
|
||||
## Usage
|
||||
|
||||
```tsx
|
||||
import FAQBlock from "@/registry/default/block/faq"
|
||||
|
||||
export default function Page() {
|
||||
return <FAQBlock />
|
||||
}
|
||||
```
|
||||
@@ -0,0 +1,26 @@
|
||||
---
|
||||
title: Feature Sections
|
||||
description: Feature grid and list sections for highlighting product capabilities.
|
||||
---
|
||||
|
||||
## Overview
|
||||
|
||||
Feature blocks display product capabilities in organized grid or list layouts. They combine icons, headings, and descriptions to communicate value propositions clearly.
|
||||
|
||||
## Features
|
||||
|
||||
- **Grid Layouts**: 2, 3, or 4 column responsive grids
|
||||
- **Icon Support**: Lucide icons or custom SVGs
|
||||
- **Responsive**: Stacks to single column on mobile
|
||||
- **Customizable**: Flexible content and styling options
|
||||
- **Accessible**: Semantic HTML with proper heading hierarchy
|
||||
|
||||
## Usage
|
||||
|
||||
```tsx
|
||||
import FeaturesBlock from "@/registry/default/block/features"
|
||||
|
||||
export default function Page() {
|
||||
return <FeaturesBlock />
|
||||
}
|
||||
```
|
||||
@@ -0,0 +1,36 @@
|
||||
---
|
||||
title: Hero Sections
|
||||
description: Full-width hero sections for landing pages with headlines, CTAs, and imagery.
|
||||
---
|
||||
|
||||
## Overview
|
||||
|
||||
Hero blocks are viewport-sized sections designed for the top of landing pages. They feature bold typography, call-to-action buttons, and visual elements to capture user attention.
|
||||
|
||||
## Variations
|
||||
|
||||
### App UI Showcase
|
||||
|
||||
<ComponentPreview name="showcase-app-ui-01" />
|
||||
|
||||
A hero section showcasing your app's UI with a centered layout and product screenshot.
|
||||
|
||||
### E-commerce Showcase
|
||||
|
||||
<ComponentPreview name="showcase-ecommerce-01" />
|
||||
|
||||
A hero section designed for e-commerce with product imagery and shopping CTAs.
|
||||
|
||||
### Marketing Showcase
|
||||
|
||||
<ComponentPreview name="showcase-marketing-01" />
|
||||
|
||||
A bold marketing hero section with large typography and conversion-focused design.
|
||||
|
||||
## Features
|
||||
|
||||
- **Responsive Design**: Adapts to all screen sizes
|
||||
- **CTA Buttons**: Primary and secondary call-to-action buttons
|
||||
- **Visual Elements**: Support for images, gradients, and animations
|
||||
- **Typography**: Large, bold headlines with supporting text
|
||||
- **Customizable**: Easy to modify colors, text, and layout
|
||||
@@ -0,0 +1,27 @@
|
||||
---
|
||||
title: Stats Sections
|
||||
description: Statistics and metrics sections with counters, charts, and key figures.
|
||||
---
|
||||
|
||||
## Overview
|
||||
|
||||
Stats blocks display key metrics and achievements in visually impactful layouts. They feature animated counters, comparison figures, and trend indicators.
|
||||
|
||||
## Features
|
||||
|
||||
- **Animated Counters**: Number animations on scroll into view
|
||||
- **Multiple Layouts**: Grid, inline, and featured stat variations
|
||||
- **Trend Indicators**: Up/down arrows with percentage changes
|
||||
- **Responsive**: Adapts from multi-column to single column on mobile
|
||||
- **Icons**: Support for metric-specific icons
|
||||
- **Customizable**: Easy to update values and labels
|
||||
|
||||
## Usage
|
||||
|
||||
```tsx
|
||||
import StatsBlock from "@/registry/default/block/stats"
|
||||
|
||||
export default function Page() {
|
||||
return <StatsBlock />
|
||||
}
|
||||
```
|
||||
@@ -0,0 +1,27 @@
|
||||
---
|
||||
title: Team Sections
|
||||
description: Team member grid and list sections with photos, roles, and social links.
|
||||
---
|
||||
|
||||
## Overview
|
||||
|
||||
Team blocks showcase team members with profile photos, names, roles, and social links. They support multiple layout variations for different page contexts.
|
||||
|
||||
## Features
|
||||
|
||||
- **Grid Layout**: Responsive grid with team member cards
|
||||
- **Profile Photos**: Avatar images with fallback initials
|
||||
- **Role Display**: Job titles and department labels
|
||||
- **Social Links**: GitHub, Twitter, LinkedIn integration
|
||||
- **Responsive**: Adapts columns based on screen size
|
||||
- **Hover Effects**: Interactive card hover states
|
||||
|
||||
## Usage
|
||||
|
||||
```tsx
|
||||
import TeamBlock from "@/registry/default/block/team"
|
||||
|
||||
export default function Page() {
|
||||
return <TeamBlock />
|
||||
}
|
||||
```
|
||||
@@ -0,0 +1,27 @@
|
||||
---
|
||||
title: Testimonials
|
||||
description: Testimonial sections with user quotes, ratings, and social proof.
|
||||
---
|
||||
|
||||
## Overview
|
||||
|
||||
Testimonial blocks display customer reviews and social proof in visually appealing layouts. They support avatars, star ratings, company logos, and quote formatting.
|
||||
|
||||
## Features
|
||||
|
||||
- **Multiple Layouts**: Grid, carousel, and featured testimonial styles
|
||||
- **Avatar Support**: User photos with name and title
|
||||
- **Star Ratings**: Visual rating display
|
||||
- **Company Logos**: Brand association for B2B testimonials
|
||||
- **Responsive**: Adapts from multi-column to single column on mobile
|
||||
- **Animated**: Optional entrance animations and carousel transitions
|
||||
|
||||
## Usage
|
||||
|
||||
```tsx
|
||||
import TestimonialsBlock from "@/registry/default/block/testimonials"
|
||||
|
||||
export default function Page() {
|
||||
return <TestimonialsBlock />
|
||||
}
|
||||
```
|
||||
@@ -0,0 +1,64 @@
|
||||
---
|
||||
title: Area Chart
|
||||
description: Area charts for visualizing trends and cumulative values over time.
|
||||
---
|
||||
|
||||
<ComponentPreview name="chart-demo" description="Area chart example" />
|
||||
|
||||
## Overview
|
||||
|
||||
Area charts are used to show trends over time or across categories. They combine line charts with filled areas beneath the line to emphasize volume.
|
||||
|
||||
## Usage
|
||||
|
||||
```tsx
|
||||
import { ChartContainer, ChartTooltip, ChartTooltipContent } from "@hanzo/ui"
|
||||
import { Area, AreaChart, CartesianGrid, XAxis } from "recharts"
|
||||
|
||||
const data = [
|
||||
{ month: "Jan", desktop: 186, mobile: 80 },
|
||||
{ month: "Feb", desktop: 305, mobile: 200 },
|
||||
{ month: "Mar", desktop: 237, mobile: 120 },
|
||||
]
|
||||
|
||||
const config = {
|
||||
desktop: { label: "Desktop", color: "hsl(var(--chart-1))" },
|
||||
mobile: { label: "Mobile", color: "hsl(var(--chart-2))" },
|
||||
}
|
||||
|
||||
export function AreaChartDemo() {
|
||||
return (
|
||||
<ChartContainer config={config}>
|
||||
<AreaChart data={data}>
|
||||
<CartesianGrid vertical={false} />
|
||||
<XAxis dataKey="month" />
|
||||
<ChartTooltip content={<ChartTooltipContent />} />
|
||||
<Area
|
||||
dataKey="desktop"
|
||||
fill="var(--color-desktop)"
|
||||
stroke="var(--color-desktop)"
|
||||
/>
|
||||
<Area
|
||||
dataKey="mobile"
|
||||
fill="var(--color-mobile)"
|
||||
stroke="var(--color-mobile)"
|
||||
/>
|
||||
</AreaChart>
|
||||
</ChartContainer>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
## Variants
|
||||
|
||||
### Stacked Area
|
||||
|
||||
Stack multiple areas to show cumulative totals.
|
||||
|
||||
### Gradient Fill
|
||||
|
||||
Apply gradient fills for visual depth.
|
||||
|
||||
### Step Area
|
||||
|
||||
Use `type="step"` for discrete step transitions.
|
||||
@@ -0,0 +1,58 @@
|
||||
---
|
||||
title: Bar Chart
|
||||
description: Bar charts for comparing categorical data and distributions.
|
||||
---
|
||||
|
||||
<ComponentPreview name="chart-demo" description="Bar chart example" />
|
||||
|
||||
## Overview
|
||||
|
||||
Bar charts display data with rectangular bars proportional to their values. They are ideal for comparing quantities across categories.
|
||||
|
||||
## Usage
|
||||
|
||||
```tsx
|
||||
import { ChartContainer, ChartTooltip, ChartTooltipContent } from "@hanzo/ui"
|
||||
import { Bar, BarChart, CartesianGrid, XAxis } from "recharts"
|
||||
|
||||
const data = [
|
||||
{ month: "Jan", desktop: 186 },
|
||||
{ month: "Feb", desktop: 305 },
|
||||
{ month: "Mar", desktop: 237 },
|
||||
]
|
||||
|
||||
const config = {
|
||||
desktop: { label: "Desktop", color: "hsl(var(--chart-1))" },
|
||||
}
|
||||
|
||||
export function BarChartDemo() {
|
||||
return (
|
||||
<ChartContainer config={config}>
|
||||
<BarChart data={data}>
|
||||
<CartesianGrid vertical={false} />
|
||||
<XAxis dataKey="month" />
|
||||
<ChartTooltip content={<ChartTooltipContent />} />
|
||||
<Bar dataKey="desktop" fill="var(--color-desktop)" radius={4} />
|
||||
</BarChart>
|
||||
</ChartContainer>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
## Variants
|
||||
|
||||
### Horizontal Bar
|
||||
|
||||
Use `layout="vertical"` for horizontal orientation.
|
||||
|
||||
### Stacked Bar
|
||||
|
||||
Stack multiple bars to show composition within categories.
|
||||
|
||||
### Mixed Bar
|
||||
|
||||
Combine different bar styles and data series.
|
||||
|
||||
### With Labels
|
||||
|
||||
Add data labels directly on bars using custom label components.
|
||||
@@ -0,0 +1,108 @@
|
||||
---
|
||||
title: Charts
|
||||
description: Beautiful, composable chart components built on Recharts.
|
||||
---
|
||||
|
||||
## Overview
|
||||
|
||||
@hanzo/ui provides a set of chart components built on top of [Recharts](https://recharts.org/). The chart components are designed to be composable and work with Tailwind CSS theming.
|
||||
|
||||
## Installation
|
||||
|
||||
<Tabs defaultValue="cli">
|
||||
|
||||
<TabsList>
|
||||
<TabsTrigger value="cli">CLI</TabsTrigger>
|
||||
<TabsTrigger value="manual">Manual</TabsTrigger>
|
||||
</TabsList>
|
||||
|
||||
<TabsContent value="cli">
|
||||
|
||||
```bash
|
||||
npx @hanzo/ui@latest add chart
|
||||
```
|
||||
|
||||
</TabsContent>
|
||||
|
||||
<TabsContent value="manual">
|
||||
|
||||
<Steps>
|
||||
|
||||
<Step>Install recharts:</Step>
|
||||
|
||||
```bash
|
||||
pnpm add recharts
|
||||
```
|
||||
|
||||
<Step>Copy the chart components into your project.</Step>
|
||||
|
||||
<ComponentSource name="chart" />
|
||||
|
||||
</Steps>
|
||||
|
||||
</TabsContent>
|
||||
|
||||
</Tabs>
|
||||
|
||||
## Components
|
||||
|
||||
The chart package includes the following components:
|
||||
|
||||
- **ChartContainer** - Responsive container that handles theming and sizing
|
||||
- **ChartTooltip** - Styled tooltip component
|
||||
- **ChartTooltipContent** - Content renderer for tooltips
|
||||
- **ChartLegend** - Chart legend component
|
||||
- **ChartLegendContent** - Content renderer for legends
|
||||
|
||||
## Chart Types
|
||||
|
||||
- [Area Chart](/docs/charts/area) - For showing trends over time
|
||||
- [Bar Chart](/docs/charts/bar) - For comparing categorical data
|
||||
- [Line Chart](/docs/charts/line) - For showing continuous data
|
||||
- [Pie Chart](/docs/charts/pie) - For showing proportional data
|
||||
- [Radar Chart](/docs/charts/radar) - For multivariate data comparison
|
||||
- [Radial Chart](/docs/charts/radial) - For circular data visualization
|
||||
- [Tooltip](/docs/charts/tooltip) - Shared tooltip component
|
||||
|
||||
## Usage
|
||||
|
||||
```tsx
|
||||
import { ChartContainer, ChartTooltip, ChartTooltipContent } from "@hanzo/ui"
|
||||
import { Bar, BarChart, XAxis } from "recharts"
|
||||
|
||||
const data = [
|
||||
{ month: "Jan", value: 186 },
|
||||
{ month: "Feb", value: 305 },
|
||||
{ month: "Mar", value: 237 },
|
||||
]
|
||||
|
||||
const config = {
|
||||
value: { label: "Value", color: "hsl(var(--chart-1))" },
|
||||
}
|
||||
|
||||
export function MyChart() {
|
||||
return (
|
||||
<ChartContainer config={config}>
|
||||
<BarChart data={data}>
|
||||
<XAxis dataKey="month" />
|
||||
<ChartTooltip content={<ChartTooltipContent />} />
|
||||
<Bar dataKey="value" fill="var(--color-value)" />
|
||||
</BarChart>
|
||||
</ChartContainer>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
## Theming
|
||||
|
||||
Charts use CSS variables for theming. Define chart colors in your CSS:
|
||||
|
||||
```css
|
||||
:root {
|
||||
--chart-1: 12 76% 61%;
|
||||
--chart-2: 173 58% 39%;
|
||||
--chart-3: 197 37% 24%;
|
||||
--chart-4: 43 74% 66%;
|
||||
--chart-5: 27 87% 67%;
|
||||
}
|
||||
```
|
||||
@@ -0,0 +1,60 @@
|
||||
---
|
||||
title: Line Chart
|
||||
description: Line charts for showing continuous data trends and comparisons.
|
||||
---
|
||||
|
||||
<ComponentPreview name="chart-demo" description="Line chart example" />
|
||||
|
||||
## Overview
|
||||
|
||||
Line charts connect data points with straight or curved lines, ideal for showing trends and changes over continuous intervals.
|
||||
|
||||
## Usage
|
||||
|
||||
```tsx
|
||||
import { ChartContainer, ChartTooltip, ChartTooltipContent } from "@hanzo/ui"
|
||||
import { CartesianGrid, Line, LineChart, XAxis } from "recharts"
|
||||
|
||||
const data = [
|
||||
{ month: "Jan", desktop: 186, mobile: 80 },
|
||||
{ month: "Feb", desktop: 305, mobile: 200 },
|
||||
{ month: "Mar", desktop: 237, mobile: 120 },
|
||||
]
|
||||
|
||||
const config = {
|
||||
desktop: { label: "Desktop", color: "hsl(var(--chart-1))" },
|
||||
mobile: { label: "Mobile", color: "hsl(var(--chart-2))" },
|
||||
}
|
||||
|
||||
export function LineChartDemo() {
|
||||
return (
|
||||
<ChartContainer config={config}>
|
||||
<LineChart data={data}>
|
||||
<CartesianGrid vertical={false} />
|
||||
<XAxis dataKey="month" />
|
||||
<ChartTooltip content={<ChartTooltipContent />} />
|
||||
<Line dataKey="desktop" stroke="var(--color-desktop)" strokeWidth={2} />
|
||||
<Line dataKey="mobile" stroke="var(--color-mobile)" strokeWidth={2} />
|
||||
</LineChart>
|
||||
</ChartContainer>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
## Variants
|
||||
|
||||
### With Dots
|
||||
|
||||
Show data point markers on the line.
|
||||
|
||||
### Custom Dots
|
||||
|
||||
Use custom shapes for data point markers.
|
||||
|
||||
### Linear vs Curved
|
||||
|
||||
Toggle between `type="linear"` and `type="monotone"` interpolation.
|
||||
|
||||
### Interactive
|
||||
|
||||
Add hover interactions and clickable data points.
|
||||
@@ -0,0 +1,52 @@
|
||||
---
|
||||
title: Pie Chart
|
||||
description: Pie and donut charts for showing proportional data and distributions.
|
||||
---
|
||||
|
||||
## Overview
|
||||
|
||||
Pie charts display data as proportional slices of a circle, ideal for showing parts of a whole.
|
||||
|
||||
## Usage
|
||||
|
||||
```tsx
|
||||
import { ChartContainer, ChartTooltip, ChartTooltipContent } from "@hanzo/ui"
|
||||
import { Pie, PieChart } from "recharts"
|
||||
|
||||
const data = [
|
||||
{ browser: "Chrome", visitors: 275, fill: "var(--color-chrome)" },
|
||||
{ browser: "Safari", visitors: 200, fill: "var(--color-safari)" },
|
||||
{ browser: "Firefox", visitors: 187, fill: "var(--color-firefox)" },
|
||||
]
|
||||
|
||||
const config = {
|
||||
chrome: { label: "Chrome", color: "hsl(var(--chart-1))" },
|
||||
safari: { label: "Safari", color: "hsl(var(--chart-2))" },
|
||||
firefox: { label: "Firefox", color: "hsl(var(--chart-3))" },
|
||||
}
|
||||
|
||||
export function PieChartDemo() {
|
||||
return (
|
||||
<ChartContainer config={config}>
|
||||
<PieChart>
|
||||
<ChartTooltip content={<ChartTooltipContent />} />
|
||||
<Pie data={data} dataKey="visitors" nameKey="browser" />
|
||||
</PieChart>
|
||||
</ChartContainer>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
## Variants
|
||||
|
||||
### Donut Chart
|
||||
|
||||
Use `innerRadius` and `outerRadius` to create donut charts.
|
||||
|
||||
### With Labels
|
||||
|
||||
Add labels inside or outside slices.
|
||||
|
||||
### Interactive
|
||||
|
||||
Highlight slices on hover with `activeIndex`.
|
||||
@@ -0,0 +1,53 @@
|
||||
---
|
||||
title: Radar Chart
|
||||
description: Radar charts for multivariate data comparison and analysis.
|
||||
---
|
||||
|
||||
## Overview
|
||||
|
||||
Radar charts display multivariate data on axes starting from the same point, useful for comparing multiple quantitative variables.
|
||||
|
||||
## Usage
|
||||
|
||||
```tsx
|
||||
import { ChartContainer, ChartTooltip, ChartTooltipContent } from "@hanzo/ui"
|
||||
import { PolarAngleAxis, PolarGrid, Radar, RadarChart } from "recharts"
|
||||
|
||||
const data = [
|
||||
{ subject: "Math", A: 120, B: 110 },
|
||||
{ subject: "Chinese", A: 98, B: 130 },
|
||||
{ subject: "English", A: 86, B: 130 },
|
||||
{ subject: "Geography", A: 99, B: 100 },
|
||||
{ subject: "Physics", A: 85, B: 90 },
|
||||
{ subject: "History", A: 65, B: 85 },
|
||||
]
|
||||
|
||||
const config = {
|
||||
A: { label: "Student A", color: "hsl(var(--chart-1))" },
|
||||
B: { label: "Student B", color: "hsl(var(--chart-2))" },
|
||||
}
|
||||
|
||||
export function RadarChartDemo() {
|
||||
return (
|
||||
<ChartContainer config={config}>
|
||||
<RadarChart data={data}>
|
||||
<PolarGrid />
|
||||
<PolarAngleAxis dataKey="subject" />
|
||||
<ChartTooltip content={<ChartTooltipContent />} />
|
||||
<Radar dataKey="A" fill="var(--color-A)" fillOpacity={0.6} />
|
||||
<Radar dataKey="B" fill="var(--color-B)" fillOpacity={0.6} />
|
||||
</RadarChart>
|
||||
</ChartContainer>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
## Variants
|
||||
|
||||
### Filled Radar
|
||||
|
||||
Use `fillOpacity` to create filled radar areas.
|
||||
|
||||
### Multiple Series
|
||||
|
||||
Compare multiple datasets on the same radar.
|
||||
@@ -0,0 +1,52 @@
|
||||
---
|
||||
title: Radial Chart
|
||||
description: Radial bar charts for circular data visualization and progress indicators.
|
||||
---
|
||||
|
||||
## Overview
|
||||
|
||||
Radial charts display data in a circular layout, useful for progress indicators, gauges, and circular comparisons.
|
||||
|
||||
## Usage
|
||||
|
||||
```tsx
|
||||
import { ChartContainer, ChartTooltip, ChartTooltipContent } from "@hanzo/ui"
|
||||
import { RadialBar, RadialBarChart } from "recharts"
|
||||
|
||||
const data = [
|
||||
{ name: "Chrome", visitors: 275, fill: "var(--color-chrome)" },
|
||||
{ name: "Safari", visitors: 200, fill: "var(--color-safari)" },
|
||||
{ name: "Firefox", visitors: 187, fill: "var(--color-firefox)" },
|
||||
]
|
||||
|
||||
const config = {
|
||||
chrome: { label: "Chrome", color: "hsl(var(--chart-1))" },
|
||||
safari: { label: "Safari", color: "hsl(var(--chart-2))" },
|
||||
firefox: { label: "Firefox", color: "hsl(var(--chart-3))" },
|
||||
}
|
||||
|
||||
export function RadialChartDemo() {
|
||||
return (
|
||||
<ChartContainer config={config}>
|
||||
<RadialBarChart data={data} innerRadius={30} outerRadius={110}>
|
||||
<ChartTooltip content={<ChartTooltipContent />} />
|
||||
<RadialBar dataKey="visitors" />
|
||||
</RadialBarChart>
|
||||
</ChartContainer>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
## Variants
|
||||
|
||||
### Stacked Radial
|
||||
|
||||
Stack multiple radial bars concentrically.
|
||||
|
||||
### With Labels
|
||||
|
||||
Add labels to radial segments for clarity.
|
||||
|
||||
### Progress Gauge
|
||||
|
||||
Use a single radial bar as a progress or gauge indicator.
|
||||
@@ -0,0 +1,61 @@
|
||||
---
|
||||
title: Chart Tooltip
|
||||
description: Customizable tooltip component for all chart types.
|
||||
---
|
||||
|
||||
## Overview
|
||||
|
||||
The chart tooltip component provides a consistent, themed tooltip across all chart types. It integrates with the chart config system for automatic label and color resolution.
|
||||
|
||||
## Usage
|
||||
|
||||
```tsx
|
||||
import { ChartTooltip, ChartTooltipContent } from "@hanzo/ui"
|
||||
|
||||
// Inside any chart component:
|
||||
;<ChartTooltip content={<ChartTooltipContent />} />
|
||||
```
|
||||
|
||||
## Props
|
||||
|
||||
### ChartTooltipContent
|
||||
|
||||
| Prop | Type | Default | Description |
|
||||
| ------------- | --------------------------- | ------- | ------------------------- |
|
||||
| hideLabel | boolean | false | Hide the label in tooltip |
|
||||
| hideIndicator | boolean | false | Hide the color indicator |
|
||||
| indicator | "line" \| "dot" \| "dashed" | "dot" | Indicator style |
|
||||
| nameKey | string | - | Key for item names |
|
||||
| labelKey | string | - | Key for labels |
|
||||
|
||||
## Customization
|
||||
|
||||
### Custom Formatter
|
||||
|
||||
```tsx
|
||||
<ChartTooltip
|
||||
content={
|
||||
<ChartTooltipContent
|
||||
formatter={(value, name) => (
|
||||
<span>
|
||||
{name}: ${value.toLocaleString()}
|
||||
</span>
|
||||
)}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
```
|
||||
|
||||
### Custom Label
|
||||
|
||||
```tsx
|
||||
<ChartTooltip
|
||||
content={<ChartTooltipContent labelFormatter={(label) => `Date: ${label}`} />}
|
||||
/>
|
||||
```
|
||||
|
||||
## Indicator Styles
|
||||
|
||||
- **dot** - Small colored circle (default)
|
||||
- **line** - Colored line indicator
|
||||
- **dashed** - Dashed line indicator
|
||||
@@ -63,18 +63,28 @@ function Desktop() {
|
||||
|
||||
return (
|
||||
<div>
|
||||
<button onClick={() => windows.openWindow('Settings')}>Open Settings</button>
|
||||
<button onClick={() => windows.toggleWindow('Terminal')}>Toggle Terminal</button>
|
||||
<button onClick={() => windows.openWindow("Settings")}>
|
||||
Open Settings
|
||||
</button>
|
||||
<button onClick={() => windows.toggleWindow("Terminal")}>
|
||||
Toggle Terminal
|
||||
</button>
|
||||
<button onClick={() => windows.closeAllWindows()}>Close All</button>
|
||||
|
||||
{windows.isOpen('Settings') && (
|
||||
<Window title="Settings" onClose={() => windows.closeWindow('Settings')}>
|
||||
{windows.isOpen("Settings") && (
|
||||
<Window
|
||||
title="Settings"
|
||||
onClose={() => windows.closeWindow("Settings")}
|
||||
>
|
||||
...
|
||||
</Window>
|
||||
)}
|
||||
|
||||
{windows.isOpen('Terminal') && (
|
||||
<Window title="Terminal" onClose={() => windows.closeWindow('Terminal')}>
|
||||
{windows.isOpen("Terminal") && (
|
||||
<Window
|
||||
title="Terminal"
|
||||
onClose={() => windows.closeWindow("Terminal")}
|
||||
>
|
||||
...
|
||||
</Window>
|
||||
)}
|
||||
@@ -85,17 +95,17 @@ function Desktop() {
|
||||
|
||||
#### API
|
||||
|
||||
| Method | Type | Description |
|
||||
|--------|------|-------------|
|
||||
| `isOpen` | `(id: string) => boolean` | Check if a window is open |
|
||||
| `openWindow` | `(id: string) => void` | Open a window |
|
||||
| `closeWindow` | `(id: string) => void` | Close a window |
|
||||
| `toggleWindow` | `(id: string) => void` | Toggle a window's open state |
|
||||
| `closeAllWindows` | `() => void` | Close all windows |
|
||||
| `focusWindow` | `(id: string) => void` | Focus/activate a window |
|
||||
| `activeWindow` | `string \| null` | Currently active window |
|
||||
| `openWindows` | `string[]` | List of all open window IDs |
|
||||
| `windows` | `Record<string, boolean>` | Window state map |
|
||||
| Method | Type | Description |
|
||||
| ----------------- | ------------------------- | ---------------------------- |
|
||||
| `isOpen` | `(id: string) => boolean` | Check if a window is open |
|
||||
| `openWindow` | `(id: string) => void` | Open a window |
|
||||
| `closeWindow` | `(id: string) => void` | Close a window |
|
||||
| `toggleWindow` | `(id: string) => void` | Toggle a window's open state |
|
||||
| `closeAllWindows` | `() => void` | Close all windows |
|
||||
| `focusWindow` | `(id: string) => void` | Focus/activate a window |
|
||||
| `activeWindow` | `string \| null` | Currently active window |
|
||||
| `openWindows` | `string[]` | List of all open window IDs |
|
||||
| `windows` | `Record<string, boolean>` | Window state map |
|
||||
|
||||
---
|
||||
|
||||
@@ -150,23 +160,23 @@ function SettingsPanel() {
|
||||
|
||||
#### API
|
||||
|
||||
| Property | Type | Description |
|
||||
|----------|------|-------------|
|
||||
| `theme` | `'light' \| 'dark' \| 'system'` | Current theme |
|
||||
| `setTheme` | `(theme) => void` | Set theme |
|
||||
| `colorScheme` | `string` | Color scheme name |
|
||||
| `setColorScheme` | `(scheme) => void` | Set color scheme |
|
||||
| `showDock` | `boolean` | Whether dock is visible |
|
||||
| `setShowDock` | `(show) => void` | Toggle dock visibility |
|
||||
| `dockPosition` | `'bottom' \| 'left' \| 'right'` | Dock position |
|
||||
| `setDockPosition` | `(position) => void` | Set dock position |
|
||||
| `dockMagnification` | `boolean` | Dock magnification enabled |
|
||||
| `setDockMagnification` | `(enabled) => void` | Toggle magnification |
|
||||
| `fontSize` | `number` | Base font size |
|
||||
| `setFontSize` | `(size) => void` | Set font size |
|
||||
| `windowTransparency` | `number` | Window transparency (0-1) |
|
||||
| `setWindowTransparency` | `(opacity) => void` | Set transparency |
|
||||
| `resetToDefaults` | `() => void` | Reset all settings |
|
||||
| Property | Type | Description |
|
||||
| ----------------------- | ------------------------------- | -------------------------- |
|
||||
| `theme` | `'light' \| 'dark' \| 'system'` | Current theme |
|
||||
| `setTheme` | `(theme) => void` | Set theme |
|
||||
| `colorScheme` | `string` | Color scheme name |
|
||||
| `setColorScheme` | `(scheme) => void` | Set color scheme |
|
||||
| `showDock` | `boolean` | Whether dock is visible |
|
||||
| `setShowDock` | `(show) => void` | Toggle dock visibility |
|
||||
| `dockPosition` | `'bottom' \| 'left' \| 'right'` | Dock position |
|
||||
| `setDockPosition` | `(position) => void` | Set dock position |
|
||||
| `dockMagnification` | `boolean` | Dock magnification enabled |
|
||||
| `setDockMagnification` | `(enabled) => void` | Toggle magnification |
|
||||
| `fontSize` | `number` | Base font size |
|
||||
| `setFontSize` | `(size) => void` | Set font size |
|
||||
| `windowTransparency` | `number` | Window transparency (0-1) |
|
||||
| `setWindowTransparency` | `(opacity) => void` | Set transparency |
|
||||
| `resetToDefaults` | `() => void` | Reset all settings |
|
||||
|
||||
---
|
||||
|
||||
@@ -182,20 +192,18 @@ function Desktop() {
|
||||
|
||||
return (
|
||||
<div>
|
||||
<button onClick={() => overlays.open('spotlight')}>
|
||||
<button onClick={() => overlays.open("spotlight")}>
|
||||
Open Spotlight (⌘+Space)
|
||||
</button>
|
||||
|
||||
<button onClick={() => overlays.open('about')}>
|
||||
About This Mac
|
||||
</button>
|
||||
<button onClick={() => overlays.open("about")}>About This Mac</button>
|
||||
|
||||
{overlays.isOpen('spotlight') && (
|
||||
<Spotlight onClose={() => overlays.close('spotlight')} />
|
||||
{overlays.isOpen("spotlight") && (
|
||||
<Spotlight onClose={() => overlays.close("spotlight")} />
|
||||
)}
|
||||
|
||||
{overlays.isOpen('about') && (
|
||||
<AboutDialog onClose={() => overlays.close('about')} />
|
||||
{overlays.isOpen("about") && (
|
||||
<AboutDialog onClose={() => overlays.close("about")} />
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
@@ -204,14 +212,14 @@ function Desktop() {
|
||||
|
||||
#### API
|
||||
|
||||
| Property | Type | Description |
|
||||
|----------|------|-------------|
|
||||
| `overlays` | `OverlayState` | Current overlay states |
|
||||
| `isOpen` | `(id: string) => boolean` | Check if an overlay is open |
|
||||
| `open` | `(id: string) => void` | Open an overlay by id |
|
||||
| `close` | `(id: string) => void` | Close an overlay by id |
|
||||
| `toggle` | `(id: string) => void` | Toggle an overlay |
|
||||
| `closeAll` | `() => void` | Close all overlays |
|
||||
| Property | Type | Description |
|
||||
| ---------- | ------------------------- | --------------------------- |
|
||||
| `overlays` | `OverlayState` | Current overlay states |
|
||||
| `isOpen` | `(id: string) => boolean` | Check if an overlay is open |
|
||||
| `open` | `(id: string) => void` | Open an overlay by id |
|
||||
| `close` | `(id: string) => void` | Close an overlay by id |
|
||||
| `toggle` | `(id: string) => void` | Toggle an overlay |
|
||||
| `closeAll` | `() => void` | Close all overlays |
|
||||
|
||||
Built-in overlay IDs: `spotlight`, `contextMenu`, `modal`, `drawer`
|
||||
|
||||
@@ -259,29 +267,29 @@ function Desktop() {
|
||||
|
||||
#### Shortcut Definition
|
||||
|
||||
| Property | Type | Description |
|
||||
|----------|------|-------------|
|
||||
| `key` | `string` | Key to listen for |
|
||||
| `meta` | `boolean` | Require ⌘ (Mac) / Ctrl (Windows) |
|
||||
| `alt` | `boolean` | Require Alt/Option |
|
||||
| `shift` | `boolean` | Require Shift |
|
||||
| `ctrl` | `boolean` | Require Control |
|
||||
| `action` | `() => void` | Callback when triggered |
|
||||
| Property | Type | Description |
|
||||
| -------- | ------------ | -------------------------------- |
|
||||
| `key` | `string` | Key to listen for |
|
||||
| `meta` | `boolean` | Require ⌘ (Mac) / Ctrl (Windows) |
|
||||
| `alt` | `boolean` | Require Alt/Option |
|
||||
| `shift` | `boolean` | Require Shift |
|
||||
| `ctrl` | `boolean` | Require Control |
|
||||
| `action` | `() => void` | Callback when triggered |
|
||||
|
||||
## Complete Example
|
||||
|
||||
```tsx
|
||||
import {
|
||||
Window,
|
||||
Spotlight,
|
||||
useWindowManager,
|
||||
useDesktopSettings,
|
||||
useKeyboardShortcuts,
|
||||
useOverlayManager,
|
||||
useKeyboardShortcuts
|
||||
useWindowManager,
|
||||
Window,
|
||||
} from "@hanzo/ui/desktop"
|
||||
import { Dock, DockItem } from "@hanzo/ui/dock"
|
||||
|
||||
const apps = ['Finder', 'Terminal', 'Safari', 'Mail', 'Calendar', 'Settings']
|
||||
const apps = ["Finder", "Terminal", "Safari", "Mail", "Calendar", "Settings"]
|
||||
|
||||
export function Desktop() {
|
||||
const windows = useWindowManager()
|
||||
@@ -290,9 +298,9 @@ export function Desktop() {
|
||||
|
||||
// Register keyboard shortcuts
|
||||
useKeyboardShortcuts([
|
||||
{ key: ' ', meta: true, action: overlays.openSpotlight },
|
||||
{ key: 'q', meta: true, action: () => windows.close(windows.activeWindow) },
|
||||
{ key: ',', meta: true, action: () => windows.open('Settings') },
|
||||
{ key: " ", meta: true, action: overlays.openSpotlight },
|
||||
{ key: "q", meta: true, action: () => windows.close(windows.activeWindow) },
|
||||
{ key: ",", meta: true, action: () => windows.open("Settings") },
|
||||
])
|
||||
|
||||
return (
|
||||
@@ -300,33 +308,36 @@ export function Desktop() {
|
||||
className="h-screen w-screen relative"
|
||||
style={{
|
||||
opacity: settings.windowTransparency,
|
||||
fontSize: settings.fontSize
|
||||
fontSize: settings.fontSize,
|
||||
}}
|
||||
>
|
||||
{/* Desktop Background */}
|
||||
<div className="absolute inset-0 bg-gradient-to-br from-blue-500 to-purple-600" />
|
||||
|
||||
{/* Windows */}
|
||||
{apps.map(app => windows.isOpen(app) && (
|
||||
<Window
|
||||
key={app}
|
||||
title={app}
|
||||
onClose={() => windows.close(app)}
|
||||
windowType={settings.theme === 'dark' ? 'dark' : 'default'}
|
||||
>
|
||||
<div className="p-4">{app} content</div>
|
||||
</Window>
|
||||
))}
|
||||
{apps.map(
|
||||
(app) =>
|
||||
windows.isOpen(app) && (
|
||||
<Window
|
||||
key={app}
|
||||
title={app}
|
||||
onClose={() => windows.close(app)}
|
||||
windowType={settings.theme === "dark" ? "dark" : "default"}
|
||||
>
|
||||
<div className="p-4">{app} content</div>
|
||||
</Window>
|
||||
)
|
||||
)}
|
||||
|
||||
{/* Spotlight */}
|
||||
{overlays.spotlight && (
|
||||
<Spotlight
|
||||
isOpen={true}
|
||||
onClose={overlays.closeSpotlight}
|
||||
items={apps.map(app => ({
|
||||
items={apps.map((app) => ({
|
||||
id: app.toLowerCase(),
|
||||
label: app,
|
||||
category: 'Applications'
|
||||
category: "Applications",
|
||||
}))}
|
||||
onSelect={(item) => {
|
||||
windows.open(item.label)
|
||||
@@ -341,7 +352,7 @@ export function Desktop() {
|
||||
position={settings.dockPosition}
|
||||
magnification={settings.dockMagnification ? 60 : 0}
|
||||
>
|
||||
{apps.map(app => (
|
||||
{apps.map((app) => (
|
||||
<DockItem
|
||||
key={app}
|
||||
tooltip={app}
|
||||
|
||||
@@ -57,9 +57,19 @@ npm install framer-motion lucide-react
|
||||
import { Spotlight, SpotlightItem } from "@hanzo/ui/desktop"
|
||||
|
||||
const items: SpotlightItem[] = [
|
||||
{ id: 'settings', title: 'Settings', category: 'Applications', icon: <Settings /> },
|
||||
{ id: 'documents', title: 'Documents', category: 'Folders', icon: <Folder /> },
|
||||
{ id: 'music', title: 'Music', category: 'Applications', icon: <Music /> },
|
||||
{
|
||||
id: "settings",
|
||||
title: "Settings",
|
||||
category: "Applications",
|
||||
icon: <Settings />,
|
||||
},
|
||||
{
|
||||
id: "documents",
|
||||
title: "Documents",
|
||||
category: "Folders",
|
||||
icon: <Folder />,
|
||||
},
|
||||
{ id: "music", title: "Music", category: "Applications", icon: <Music /> },
|
||||
]
|
||||
|
||||
export function SpotlightDemo() {
|
||||
@@ -74,7 +84,7 @@ export function SpotlightDemo() {
|
||||
onClose={() => setIsOpen(false)}
|
||||
items={items}
|
||||
onSelect={(item) => {
|
||||
console.log('Selected:', item)
|
||||
console.log("Selected:", item)
|
||||
setIsOpen(false)
|
||||
}}
|
||||
/>
|
||||
@@ -87,25 +97,25 @@ export function SpotlightDemo() {
|
||||
|
||||
### Spotlight
|
||||
|
||||
| Prop | Type | Default | Description |
|
||||
|------|------|---------|-------------|
|
||||
| `isOpen` | `boolean` | - | Controls visibility of the spotlight dialog |
|
||||
| `onClose` | `() => void` | - | Callback when spotlight is closed |
|
||||
| `items` | `SpotlightItem[]` | - | Array of searchable items |
|
||||
| `onSelect` | `(item: SpotlightItem) => void` | - | Callback when an item is selected |
|
||||
| `placeholder` | `string` | `'Search...'` | Search input placeholder text |
|
||||
| Prop | Type | Default | Description |
|
||||
| ------------- | ------------------------------- | ------------- | ------------------------------------------- |
|
||||
| `isOpen` | `boolean` | - | Controls visibility of the spotlight dialog |
|
||||
| `onClose` | `() => void` | - | Callback when spotlight is closed |
|
||||
| `items` | `SpotlightItem[]` | - | Array of searchable items |
|
||||
| `onSelect` | `(item: SpotlightItem) => void` | - | Callback when an item is selected |
|
||||
| `placeholder` | `string` | `'Search...'` | Search input placeholder text |
|
||||
|
||||
### SpotlightItem
|
||||
|
||||
| Property | Type | Description |
|
||||
|----------|------|-------------|
|
||||
| `id` | `string` | Unique identifier for the item |
|
||||
| `title` | `string` | Display title |
|
||||
| `subtitle` | `string` | Optional subtitle/description text |
|
||||
| `icon` | `ReactNode` | Optional icon to display |
|
||||
| `category` | `string` | Category for grouping |
|
||||
| `keywords` | `string[]` | Optional search keywords |
|
||||
| `action` | `() => void` | Optional action callback |
|
||||
| Property | Type | Description |
|
||||
| ---------- | ------------ | ---------------------------------- |
|
||||
| `id` | `string` | Unique identifier for the item |
|
||||
| `title` | `string` | Display title |
|
||||
| `subtitle` | `string` | Optional subtitle/description text |
|
||||
| `icon` | `ReactNode` | Optional icon to display |
|
||||
| `category` | `string` | Category for grouping |
|
||||
| `keywords` | `string[]` | Optional search keywords |
|
||||
| `action` | `() => void` | Optional action callback |
|
||||
|
||||
## Features
|
||||
|
||||
@@ -120,26 +130,70 @@ export function SpotlightDemo() {
|
||||
### Application Launcher
|
||||
|
||||
```tsx
|
||||
import { Spotlight, SpotlightItem, useKeyboardShortcuts } from "@hanzo/ui/desktop"
|
||||
import {
|
||||
Terminal,
|
||||
Chrome,
|
||||
Mail,
|
||||
Spotlight,
|
||||
SpotlightItem,
|
||||
useKeyboardShortcuts,
|
||||
} from "@hanzo/ui/desktop"
|
||||
import {
|
||||
Calendar,
|
||||
Chrome,
|
||||
Folder,
|
||||
Mail,
|
||||
Music,
|
||||
Settings,
|
||||
Folder
|
||||
Terminal,
|
||||
} from "lucide-react"
|
||||
|
||||
const apps: SpotlightItem[] = [
|
||||
{ id: 'terminal', label: 'Terminal', category: 'Applications', icon: <Terminal className="h-5 w-5" /> },
|
||||
{ id: 'safari', label: 'Safari', category: 'Applications', icon: <Chrome className="h-5 w-5" /> },
|
||||
{ id: 'mail', label: 'Mail', category: 'Applications', icon: <Mail className="h-5 w-5" /> },
|
||||
{ id: 'calendar', label: 'Calendar', category: 'Applications', icon: <Calendar className="h-5 w-5" /> },
|
||||
{ id: 'music', label: 'Music', category: 'Applications', icon: <Music className="h-5 w-5" /> },
|
||||
{ id: 'settings', label: 'System Preferences', category: 'Applications', icon: <Settings className="h-5 w-5" /> },
|
||||
{ id: 'documents', label: 'Documents', category: 'Folders', icon: <Folder className="h-5 w-5" /> },
|
||||
{ id: 'downloads', label: 'Downloads', category: 'Folders', icon: <Folder className="h-5 w-5" /> },
|
||||
{
|
||||
id: "terminal",
|
||||
label: "Terminal",
|
||||
category: "Applications",
|
||||
icon: <Terminal className="h-5 w-5" />,
|
||||
},
|
||||
{
|
||||
id: "safari",
|
||||
label: "Safari",
|
||||
category: "Applications",
|
||||
icon: <Chrome className="h-5 w-5" />,
|
||||
},
|
||||
{
|
||||
id: "mail",
|
||||
label: "Mail",
|
||||
category: "Applications",
|
||||
icon: <Mail className="h-5 w-5" />,
|
||||
},
|
||||
{
|
||||
id: "calendar",
|
||||
label: "Calendar",
|
||||
category: "Applications",
|
||||
icon: <Calendar className="h-5 w-5" />,
|
||||
},
|
||||
{
|
||||
id: "music",
|
||||
label: "Music",
|
||||
category: "Applications",
|
||||
icon: <Music className="h-5 w-5" />,
|
||||
},
|
||||
{
|
||||
id: "settings",
|
||||
label: "System Preferences",
|
||||
category: "Applications",
|
||||
icon: <Settings className="h-5 w-5" />,
|
||||
},
|
||||
{
|
||||
id: "documents",
|
||||
label: "Documents",
|
||||
category: "Folders",
|
||||
icon: <Folder className="h-5 w-5" />,
|
||||
},
|
||||
{
|
||||
id: "downloads",
|
||||
label: "Downloads",
|
||||
category: "Folders",
|
||||
icon: <Folder className="h-5 w-5" />,
|
||||
},
|
||||
]
|
||||
|
||||
export function AppLauncher() {
|
||||
@@ -147,7 +201,7 @@ export function AppLauncher() {
|
||||
|
||||
// ⌘+Space to open (like macOS)
|
||||
useKeyboardShortcuts([
|
||||
{ key: ' ', meta: true, action: () => setIsOpen(true) }
|
||||
{ key: " ", meta: true, action: () => setIsOpen(true) },
|
||||
])
|
||||
|
||||
return (
|
||||
@@ -157,7 +211,7 @@ export function AppLauncher() {
|
||||
items={apps}
|
||||
placeholder="Search applications..."
|
||||
onSelect={(item) => {
|
||||
console.log('Launch:', item.label)
|
||||
console.log("Launch:", item.label)
|
||||
setIsOpen(false)
|
||||
}}
|
||||
/>
|
||||
@@ -169,22 +223,51 @@ export function AppLauncher() {
|
||||
|
||||
```tsx
|
||||
import { Spotlight, SpotlightItem } from "@hanzo/ui/desktop"
|
||||
import {
|
||||
Save,
|
||||
Copy,
|
||||
Scissors,
|
||||
Clipboard,
|
||||
Undo,
|
||||
Redo
|
||||
} from "lucide-react"
|
||||
import { Clipboard, Copy, Redo, Save, Scissors, Undo } from "lucide-react"
|
||||
|
||||
const commands: SpotlightItem[] = [
|
||||
{ id: 'save', label: 'Save', category: 'File', icon: <Save />, shortcut: '⌘S' },
|
||||
{ id: 'copy', label: 'Copy', category: 'Edit', icon: <Copy />, shortcut: '⌘C' },
|
||||
{ id: 'cut', label: 'Cut', category: 'Edit', icon: <Scissors />, shortcut: '⌘X' },
|
||||
{ id: 'paste', label: 'Paste', category: 'Edit', icon: <Clipboard />, shortcut: '⌘V' },
|
||||
{ id: 'undo', label: 'Undo', category: 'Edit', icon: <Undo />, shortcut: '⌘Z' },
|
||||
{ id: 'redo', label: 'Redo', category: 'Edit', icon: <Redo />, shortcut: '⌘⇧Z' },
|
||||
{
|
||||
id: "save",
|
||||
label: "Save",
|
||||
category: "File",
|
||||
icon: <Save />,
|
||||
shortcut: "⌘S",
|
||||
},
|
||||
{
|
||||
id: "copy",
|
||||
label: "Copy",
|
||||
category: "Edit",
|
||||
icon: <Copy />,
|
||||
shortcut: "⌘C",
|
||||
},
|
||||
{
|
||||
id: "cut",
|
||||
label: "Cut",
|
||||
category: "Edit",
|
||||
icon: <Scissors />,
|
||||
shortcut: "⌘X",
|
||||
},
|
||||
{
|
||||
id: "paste",
|
||||
label: "Paste",
|
||||
category: "Edit",
|
||||
icon: <Clipboard />,
|
||||
shortcut: "⌘V",
|
||||
},
|
||||
{
|
||||
id: "undo",
|
||||
label: "Undo",
|
||||
category: "Edit",
|
||||
icon: <Undo />,
|
||||
shortcut: "⌘Z",
|
||||
},
|
||||
{
|
||||
id: "redo",
|
||||
label: "Redo",
|
||||
category: "Edit",
|
||||
icon: <Redo />,
|
||||
shortcut: "⌘⇧Z",
|
||||
},
|
||||
]
|
||||
|
||||
export function CommandPalette() {
|
||||
@@ -197,7 +280,7 @@ export function CommandPalette() {
|
||||
items={commands}
|
||||
placeholder="Type a command..."
|
||||
onSelect={(item) => {
|
||||
console.log('Execute:', item.id)
|
||||
console.log("Execute:", item.id)
|
||||
setIsOpen(false)
|
||||
}}
|
||||
/>
|
||||
@@ -207,9 +290,9 @@ export function CommandPalette() {
|
||||
|
||||
## Keyboard Shortcuts
|
||||
|
||||
| Shortcut | Action |
|
||||
|----------|--------|
|
||||
| `↑` / `↓` | Navigate between items |
|
||||
| `Enter` | Select highlighted item |
|
||||
| `Escape` | Close spotlight |
|
||||
| Shortcut | Action |
|
||||
| --------------- | -------------------------------- |
|
||||
| `↑` / `↓` | Navigate between items |
|
||||
| `Enter` | Select highlighted item |
|
||||
| `Escape` | Close spotlight |
|
||||
| `⌘K` / `⌘Space` | Open spotlight (when configured) |
|
||||
|
||||
@@ -75,17 +75,17 @@ export function WindowDemo() {
|
||||
|
||||
## Props
|
||||
|
||||
| Prop | Type | Default | Description |
|
||||
|------|------|---------|-------------|
|
||||
| `title` | `string` | - | Window title displayed in the title bar |
|
||||
| `onClose` | `() => void` | - | Callback when the close button is clicked |
|
||||
| `initialPosition` | `{ x: number, y: number }` | `{ x: 100, y: 100 }` | Initial window position |
|
||||
| `initialSize` | `{ width: number, height: number }` | `{ width: 800, height: 600 }` | Initial window dimensions |
|
||||
| `minWidth` | `number` | `200` | Minimum window width |
|
||||
| `minHeight` | `number` | `150` | Minimum window height |
|
||||
| `windowType` | `'default' \| 'dark' \| 'light' \| 'transparent'` | `'default'` | Window style variant |
|
||||
| `className` | `string` | - | Additional CSS classes for the window |
|
||||
| `children` | `ReactNode` | - | Window content |
|
||||
| Prop | Type | Default | Description |
|
||||
| ----------------- | ------------------------------------------------- | ----------------------------- | ----------------------------------------- |
|
||||
| `title` | `string` | - | Window title displayed in the title bar |
|
||||
| `onClose` | `() => void` | - | Callback when the close button is clicked |
|
||||
| `initialPosition` | `{ x: number, y: number }` | `{ x: 100, y: 100 }` | Initial window position |
|
||||
| `initialSize` | `{ width: number, height: number }` | `{ width: 800, height: 600 }` | Initial window dimensions |
|
||||
| `minWidth` | `number` | `200` | Minimum window width |
|
||||
| `minHeight` | `number` | `150` | Minimum window height |
|
||||
| `windowType` | `'default' \| 'dark' \| 'light' \| 'transparent'` | `'default'` | Window style variant |
|
||||
| `className` | `string` | - | Additional CSS classes for the window |
|
||||
| `children` | `ReactNode` | - | Window content |
|
||||
|
||||
## Variants
|
||||
|
||||
@@ -128,7 +128,7 @@ export function WindowDemo() {
|
||||
|
||||
```tsx
|
||||
import { Window } from "@hanzo/ui/desktop"
|
||||
import { Settings, User, Bell } from "lucide-react"
|
||||
import { Bell, Settings, User } from "lucide-react"
|
||||
|
||||
export function SettingsWindow() {
|
||||
const [isOpen, setIsOpen] = useState(true)
|
||||
@@ -164,35 +164,35 @@ export function SettingsWindow() {
|
||||
### Multiple Windows
|
||||
|
||||
```tsx
|
||||
import { Window, useWindowManager } from "@hanzo/ui/desktop"
|
||||
import { useWindowManager, Window } from "@hanzo/ui/desktop"
|
||||
|
||||
export function MultiWindowDemo() {
|
||||
const windows = useWindowManager()
|
||||
|
||||
return (
|
||||
<div className="relative h-screen">
|
||||
{windows.isOpen('Settings') && (
|
||||
{windows.isOpen("Settings") && (
|
||||
<Window
|
||||
title="Settings"
|
||||
onClose={() => windows.close('Settings')}
|
||||
onClose={() => windows.close("Settings")}
|
||||
initialPosition={{ x: 100, y: 100 }}
|
||||
>
|
||||
<div className="p-4">Settings content</div>
|
||||
</Window>
|
||||
)}
|
||||
|
||||
{windows.isOpen('Editor') && (
|
||||
{windows.isOpen("Editor") && (
|
||||
<Window
|
||||
title="Editor"
|
||||
onClose={() => windows.close('Editor')}
|
||||
onClose={() => windows.close("Editor")}
|
||||
initialPosition={{ x: 200, y: 150 }}
|
||||
>
|
||||
<div className="p-4">Editor content</div>
|
||||
</Window>
|
||||
)}
|
||||
|
||||
<button onClick={() => windows.open('Settings')}>Open Settings</button>
|
||||
<button onClick={() => windows.open('Editor')}>Open Editor</button>
|
||||
<button onClick={() => windows.open("Settings")}>Open Settings</button>
|
||||
<button onClick={() => windows.open("Editor")}>Open Editor</button>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -8,19 +8,19 @@ The Desktop namespace provides components and hooks for building macOS-style des
|
||||
|
||||
## Components
|
||||
|
||||
| Component | Description |
|
||||
|-----------|-------------|
|
||||
| [Window](/docs/components/desktop-window) | Draggable, resizable window with minimize, maximize, and close controls |
|
||||
| [Spotlight](/docs/components/desktop-spotlight) | macOS Spotlight-style search and command palette |
|
||||
| Component | Description |
|
||||
| ----------------------------------------------- | ----------------------------------------------------------------------- |
|
||||
| [Window](/docs/components/desktop-window) | Draggable, resizable window with minimize, maximize, and close controls |
|
||||
| [Spotlight](/docs/components/desktop-spotlight) | macOS Spotlight-style search and command palette |
|
||||
|
||||
## Hooks
|
||||
|
||||
| Hook | Description |
|
||||
|------|-------------|
|
||||
| `useWindowManager` | Manage multiple windows with open/close/toggle state |
|
||||
| `useDesktopSettings` | Persist desktop settings like theme, dock position |
|
||||
| `useOverlayManager` | Manage overlay states (spotlight, about dialog, etc.) |
|
||||
| `useKeyboardShortcuts` | Register keyboard shortcuts (⌘+Space, ⌘+Q, etc.) |
|
||||
| Hook | Description |
|
||||
| ---------------------- | ----------------------------------------------------- |
|
||||
| `useWindowManager` | Manage multiple windows with open/close/toggle state |
|
||||
| `useDesktopSettings` | Persist desktop settings like theme, dock position |
|
||||
| `useOverlayManager` | Manage overlay states (spotlight, about dialog, etc.) |
|
||||
| `useKeyboardShortcuts` | Register keyboard shortcuts (⌘+Space, ⌘+Q, etc.) |
|
||||
|
||||
See [Desktop Hooks](/docs/components/desktop-hooks) for detailed hook documentation.
|
||||
|
||||
@@ -51,12 +51,12 @@ Then import from the desktop namespace:
|
||||
|
||||
```tsx
|
||||
import {
|
||||
Window,
|
||||
Spotlight,
|
||||
useWindowManager,
|
||||
useDesktopSettings,
|
||||
useKeyboardShortcuts,
|
||||
useOverlayManager,
|
||||
useKeyboardShortcuts
|
||||
useWindowManager,
|
||||
Window,
|
||||
} from "@hanzo/ui/desktop"
|
||||
```
|
||||
|
||||
@@ -68,10 +68,10 @@ import {
|
||||
|
||||
```tsx
|
||||
import {
|
||||
Window,
|
||||
Spotlight,
|
||||
useKeyboardShortcuts,
|
||||
useWindowManager,
|
||||
useKeyboardShortcuts
|
||||
Window,
|
||||
} from "@hanzo/ui/desktop"
|
||||
import { Dock, DockItem } from "@hanzo/ui/dock"
|
||||
|
||||
@@ -80,16 +80,16 @@ export function MyDesktop() {
|
||||
const [showSpotlight, setShowSpotlight] = useState(false)
|
||||
|
||||
useKeyboardShortcuts([
|
||||
{ key: ' ', meta: true, action: () => setShowSpotlight(true) },
|
||||
{ key: " ", meta: true, action: () => setShowSpotlight(true) },
|
||||
])
|
||||
|
||||
return (
|
||||
<div className="h-screen relative">
|
||||
{/* Windows */}
|
||||
{windows.isOpen('Settings') && (
|
||||
{windows.isOpen("Settings") && (
|
||||
<Window
|
||||
title="Settings"
|
||||
onClose={() => windows.closeWindow('Settings')}
|
||||
onClose={() => windows.closeWindow("Settings")}
|
||||
>
|
||||
<div className="p-4">Settings content</div>
|
||||
</Window>
|
||||
@@ -99,9 +99,7 @@ export function MyDesktop() {
|
||||
<Spotlight
|
||||
isOpen={showSpotlight}
|
||||
onClose={() => setShowSpotlight(false)}
|
||||
items={[
|
||||
{ id: 'settings', title: 'Settings', category: 'Apps' },
|
||||
]}
|
||||
items={[{ id: "settings", title: "Settings", category: "Apps" }]}
|
||||
onSelect={(item) => {
|
||||
windows.openWindow(item.title)
|
||||
setShowSpotlight(false)
|
||||
@@ -112,7 +110,7 @@ export function MyDesktop() {
|
||||
<Dock position="bottom">
|
||||
<DockItem
|
||||
tooltip="Settings"
|
||||
onClick={() => windows.toggleWindow('Settings')}
|
||||
onClick={() => windows.toggleWindow("Settings")}
|
||||
/>
|
||||
</Dock>
|
||||
</div>
|
||||
|
||||
@@ -0,0 +1,45 @@
|
||||
---
|
||||
title: Direction
|
||||
description: Provider and hook for RTL/LTR text direction support.
|
||||
---
|
||||
|
||||
## Overview
|
||||
|
||||
The Direction component provides RTL (right-to-left) and LTR (left-to-right) text direction support through a React context provider and hook. Useful for internationalization and bidirectional text layouts.
|
||||
|
||||
## Usage
|
||||
|
||||
```tsx
|
||||
import { DirectionProvider, useDirection } from "@hanzo/ui"
|
||||
|
||||
export function App() {
|
||||
return (
|
||||
<DirectionProvider direction="rtl">
|
||||
<MyComponent />
|
||||
</DirectionProvider>
|
||||
)
|
||||
}
|
||||
|
||||
function MyComponent() {
|
||||
const direction = useDirection()
|
||||
return <div>Current direction: {direction}</div>
|
||||
}
|
||||
```
|
||||
|
||||
## Props
|
||||
|
||||
### DirectionProvider
|
||||
|
||||
| Prop | Type | Default | Description |
|
||||
| --------- | -------------- | ------- | --------------- |
|
||||
| dir | "ltr" \| "rtl" | - | Text direction |
|
||||
| direction | "ltr" \| "rtl" | - | Alias for `dir` |
|
||||
| children | ReactNode | - | Child elements |
|
||||
|
||||
### useDirection
|
||||
|
||||
Returns the current direction from the nearest `DirectionProvider`.
|
||||
|
||||
```tsx
|
||||
const direction = useDirection() // "ltr" | "rtl"
|
||||
```
|
||||
@@ -1,71 +0,0 @@
|
||||
---
|
||||
title: Stock Market
|
||||
description: Display real-time stock market overview with indices and major stocks.
|
||||
component: true
|
||||
---
|
||||
|
||||
<ComponentPreview
|
||||
name="market-overview"
|
||||
description="Stock market overview widget"
|
||||
/>
|
||||
|
||||
## Installation
|
||||
|
||||
<Tabs defaultValue="cli">
|
||||
|
||||
<TabsList>
|
||||
<TabsTrigger value="cli">CLI</TabsTrigger>
|
||||
<TabsTrigger value="manual">Manual</TabsTrigger>
|
||||
</TabsList>
|
||||
|
||||
<TabsContent value="cli">
|
||||
|
||||
```bash
|
||||
npx @hanzo/ui@latest add market-overview
|
||||
```
|
||||
|
||||
</TabsContent>
|
||||
|
||||
<TabsContent value="manual">
|
||||
|
||||
<Steps>
|
||||
|
||||
<Step>Install the following dependencies:</Step>
|
||||
|
||||
```bash
|
||||
npm install @hanzo/ui
|
||||
```
|
||||
|
||||
<Step>Copy and paste the following code into your project.</Step>
|
||||
|
||||
<ComponentSource name="market-overview" />
|
||||
|
||||
<Step>Update the import paths to match your project setup.</Step>
|
||||
|
||||
</Steps>
|
||||
|
||||
</TabsContent>
|
||||
|
||||
</Tabs>
|
||||
|
||||
## Usage
|
||||
|
||||
```tsx
|
||||
import { MarketOverview } from "@hanzo/ui/finance"
|
||||
|
||||
export default function Demo() {
|
||||
return <MarketOverview colorTheme="dark" showFloatingTooltip />
|
||||
}
|
||||
```
|
||||
|
||||
## Props
|
||||
|
||||
| Prop | Type | Default | Description |
|
||||
| ------------------- | ----------------- | ---------------------------------------- | ---------------------- |
|
||||
| colorTheme | "light" \| "dark" | "dark" | Color theme |
|
||||
| width | string \| number | "100%" | Widget width |
|
||||
| height | string \| number | 400 | Widget height |
|
||||
| showFloatingTooltip | boolean | false | Show tooltip on hover |
|
||||
| locale | string | "en" | Language locale |
|
||||
| isTransparent | boolean | false | Transparent background |
|
||||
| tabs | string[] | ["indices", "futures", "bonds", "forex"] | Visible tabs |
|
||||
@@ -0,0 +1,56 @@
|
||||
---
|
||||
title: Stock Market
|
||||
description: Display stock market overview with major indices and equities.
|
||||
component: true
|
||||
---
|
||||
|
||||
<ComponentPreview name="market-overview" description="Stock market overview" />
|
||||
|
||||
## Installation
|
||||
|
||||
<Tabs defaultValue="cli">
|
||||
|
||||
<TabsList>
|
||||
<TabsTrigger value="cli">CLI</TabsTrigger>
|
||||
<TabsTrigger value="manual">Manual</TabsTrigger>
|
||||
</TabsList>
|
||||
|
||||
<TabsContent value="cli">
|
||||
|
||||
```bash
|
||||
npx @hanzo/ui@latest add market-overview
|
||||
```
|
||||
|
||||
</TabsContent>
|
||||
|
||||
<TabsContent value="manual">
|
||||
|
||||
<Steps>
|
||||
|
||||
<Step>Install the following dependencies:</Step>
|
||||
|
||||
```bash
|
||||
npm install @hanzo/ui
|
||||
```
|
||||
|
||||
<Step>Copy and paste the following code into your project.</Step>
|
||||
|
||||
<ComponentSource name="market-overview" />
|
||||
|
||||
<Step>Update the import paths to match your project setup.</Step>
|
||||
|
||||
</Steps>
|
||||
|
||||
</TabsContent>
|
||||
|
||||
</Tabs>
|
||||
|
||||
## Usage
|
||||
|
||||
```tsx
|
||||
import { MarketOverview } from "@hanzo/ui/finance"
|
||||
|
||||
export default function Demo() {
|
||||
return <MarketOverview tabs={["indices"]} colorTheme="dark" />
|
||||
}
|
||||
```
|
||||
@@ -0,0 +1,119 @@
|
||||
---
|
||||
title: React Native
|
||||
description: Using @hanzo/ui components in React Native and Expo applications.
|
||||
---
|
||||
|
||||
# React Native Guide (Beta)
|
||||
|
||||
@hanzo/ui provides React Native components built on top of React Native's core primitives and NativeWind for styling. This integration is currently in beta.
|
||||
|
||||
## Installation
|
||||
|
||||
<Steps>
|
||||
|
||||
### Install the package
|
||||
|
||||
```bash
|
||||
npm install @hanzo/ui
|
||||
# or
|
||||
pnpm add @hanzo/ui
|
||||
```
|
||||
|
||||
### Install NativeWind
|
||||
|
||||
```bash
|
||||
npm install nativewind
|
||||
npm install -D tailwindcss
|
||||
```
|
||||
|
||||
### Configure Tailwind
|
||||
|
||||
```js title="tailwind.config.js"
|
||||
/** @type {import('tailwindcss').Config} */
|
||||
module.exports = {
|
||||
content: [
|
||||
"./App.{js,jsx,ts,tsx}",
|
||||
"./src/**/*.{js,jsx,ts,tsx}",
|
||||
"./node_modules/@hanzo/ui/**/*.{js,ts,jsx,tsx}",
|
||||
],
|
||||
presets: [require("nativewind/preset")],
|
||||
theme: {
|
||||
extend: {},
|
||||
},
|
||||
plugins: [],
|
||||
}
|
||||
```
|
||||
|
||||
</Steps>
|
||||
|
||||
## Usage
|
||||
|
||||
```tsx
|
||||
import { useState } from "react"
|
||||
import { Button, Card, Input } from "@hanzo/ui/native"
|
||||
|
||||
export default function App() {
|
||||
const [count, setCount] = useState(0)
|
||||
|
||||
return (
|
||||
<Card>
|
||||
<Button onPress={() => setCount((c) => c + 1)}>
|
||||
Clicked {count} times
|
||||
</Button>
|
||||
<Input value={String(count)} />
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
## Expo Integration
|
||||
|
||||
```tsx
|
||||
// app/(tabs)/index.tsx
|
||||
import { Button, Card } from "@hanzo/ui/native"
|
||||
|
||||
export default function HomeScreen() {
|
||||
return (
|
||||
<Card>
|
||||
<Button>Click me</Button>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
## Component Coverage
|
||||
|
||||
**Available:**
|
||||
|
||||
- Button, Card, Input, Label, Badge
|
||||
- Checkbox, Switch, Separator
|
||||
- Tabs, Avatar
|
||||
|
||||
**In Progress:**
|
||||
|
||||
- Dialog and Sheet (modal-based)
|
||||
- Select and Popover
|
||||
- Navigation components
|
||||
|
||||
**Not Available:**
|
||||
|
||||
- 3D components
|
||||
- Browser-specific components (Safari mockup, etc.)
|
||||
- Chart components
|
||||
|
||||
## Platform Differences
|
||||
|
||||
Some components behave differently on native platforms:
|
||||
|
||||
- **Dialog** uses React Native's `Modal` component
|
||||
- **Sheet** uses bottom sheet gestures via `react-native-gesture-handler`
|
||||
- **Animations** use `react-native-reanimated` instead of Framer Motion
|
||||
|
||||
## Status
|
||||
|
||||
This integration is in **beta**. Component coverage is limited compared to the web version. Please report issues on [GitHub](https://github.com/hanzoai/ui/issues).
|
||||
|
||||
## Next Steps
|
||||
|
||||
- [View all components](/docs/components)
|
||||
- [React Guide](/docs/frameworks/react) (stable)
|
||||
@@ -0,0 +1,100 @@
|
||||
---
|
||||
title: Svelte
|
||||
description: Using @hanzo/ui components in Svelte and SvelteKit applications.
|
||||
---
|
||||
|
||||
# Svelte Guide (Beta)
|
||||
|
||||
@hanzo/ui provides Svelte 5 components with full TypeScript support. This integration is currently in beta.
|
||||
|
||||
## Installation
|
||||
|
||||
<Steps>
|
||||
|
||||
### Install the package
|
||||
|
||||
```bash
|
||||
npm install @hanzo/ui
|
||||
# or
|
||||
pnpm add @hanzo/ui
|
||||
```
|
||||
|
||||
### Install Svelte and dependencies
|
||||
|
||||
```bash
|
||||
npm install svelte
|
||||
npm install -D tailwindcss postcss autoprefixer
|
||||
```
|
||||
|
||||
### Configure Tailwind
|
||||
|
||||
```js title="tailwind.config.js"
|
||||
/** @type {import('tailwindcss').Config} */
|
||||
module.exports = {
|
||||
content: [
|
||||
"./src/**/*.{html,js,svelte,ts}",
|
||||
"./node_modules/@hanzo/ui/**/*.{js,ts,svelte}",
|
||||
],
|
||||
theme: {
|
||||
extend: {},
|
||||
},
|
||||
plugins: [],
|
||||
}
|
||||
```
|
||||
|
||||
</Steps>
|
||||
|
||||
## Usage
|
||||
|
||||
```svelte
|
||||
<script lang="ts">
|
||||
import { Button, Card, Input } from '@hanzo/ui/svelte'
|
||||
|
||||
let count = $state(0)
|
||||
</script>
|
||||
|
||||
<Card>
|
||||
<Button onclick={() => count++}>
|
||||
Clicked {count} times
|
||||
</Button>
|
||||
<Input bind:value={count} type="number" />
|
||||
</Card>
|
||||
```
|
||||
|
||||
## SvelteKit Integration
|
||||
|
||||
```svelte
|
||||
<!-- src/routes/+page.svelte -->
|
||||
<script lang="ts">
|
||||
import { Button, Card } from '@hanzo/ui/svelte'
|
||||
</script>
|
||||
|
||||
<Card>
|
||||
<h1>Welcome to SvelteKit</h1>
|
||||
<Button>Click me</Button>
|
||||
</Card>
|
||||
```
|
||||
|
||||
## Component Coverage
|
||||
|
||||
**Available:**
|
||||
|
||||
- Button, Card, Input, Label, Badge
|
||||
- Checkbox, Dialog, Select, Separator
|
||||
- Tabs, Toggle, Tooltip
|
||||
|
||||
**In Progress:**
|
||||
|
||||
- Form components
|
||||
- Data Table
|
||||
- Advanced animations
|
||||
|
||||
## Status
|
||||
|
||||
This integration is in **beta**. Some components may have limited functionality compared to the React version. Please report issues on [GitHub](https://github.com/hanzoai/ui/issues).
|
||||
|
||||
## Next Steps
|
||||
|
||||
- [View all components](/docs/components)
|
||||
- [React Guide](/docs/frameworks/react) (stable)
|
||||
- [Vue Guide](/docs/frameworks/vue) (stable)
|
||||
+8
-19
@@ -1,5 +1,5 @@
|
||||
import js from "@eslint/js";
|
||||
import tseslint from "typescript-eslint";
|
||||
import tsParser from "@typescript-eslint/parser";
|
||||
|
||||
export default [
|
||||
{
|
||||
@@ -12,14 +12,16 @@ export default [
|
||||
"dist/**",
|
||||
".turbo/**",
|
||||
"node_modules/**",
|
||||
"coverage/**",
|
||||
"*.config.js",
|
||||
"*.config.mjs",
|
||||
],
|
||||
},
|
||||
// Use only base configs without rules
|
||||
// JavaScript files
|
||||
{
|
||||
files: ["**/*.{js,jsx,mjs,cjs}"],
|
||||
...js.configs.recommended,
|
||||
rules: {
|
||||
// Turn off all rules
|
||||
"no-unused-vars": "off",
|
||||
"no-undef": "off",
|
||||
"no-empty": "off",
|
||||
@@ -30,26 +32,13 @@ export default [
|
||||
"prefer-const": "off",
|
||||
},
|
||||
},
|
||||
// TypeScript files
|
||||
// TypeScript files - parser only, all rules off (rely on TypeScript compiler)
|
||||
{
|
||||
files: ["**/*.{ts,tsx,mts}"],
|
||||
languageOptions: {
|
||||
parser: tseslint.parser,
|
||||
},
|
||||
plugins: {
|
||||
"@typescript-eslint": tseslint.plugin,
|
||||
},
|
||||
rules: {
|
||||
// Turn off ALL rules - rely on TypeScript compiler
|
||||
"@typescript-eslint/no-explicit-any": "off",
|
||||
"@typescript-eslint/no-empty-object-type": "off",
|
||||
"@typescript-eslint/ban-ts-comment": "off",
|
||||
"@typescript-eslint/no-unused-vars": "off",
|
||||
"@typescript-eslint/no-require-imports": "off",
|
||||
"@typescript-eslint/no-this-alias": "off",
|
||||
"@typescript-eslint/no-unsafe-function-type": "off",
|
||||
"@typescript-eslint/no-empty-interface": "off",
|
||||
parser: tsParser,
|
||||
},
|
||||
rules: {},
|
||||
},
|
||||
// CommonJS files
|
||||
{
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
/**
|
||||
* Hanzo Cloud search configuration for ui.hanzo.ai
|
||||
*
|
||||
* Uses the same infrastructure as docs.hanzo.ai:
|
||||
* - Meilisearch for fulltext search
|
||||
* - Qdrant for vector/semantic search
|
||||
* - Hanzo Cloud API for RAG chat
|
||||
*/
|
||||
|
||||
export const searchBackend =
|
||||
(process.env.NEXT_PUBLIC_HANZO_SEARCH_BACKEND as 'cloud' | 'meilisearch' | undefined) ??
|
||||
'cloud'
|
||||
|
||||
export const searchEndpoint =
|
||||
process.env.NEXT_PUBLIC_HANZO_SEARCH_ENDPOINT ??
|
||||
'https://cloud-api.hanzo.ai/api/search-docs'
|
||||
|
||||
export const searchIndex =
|
||||
process.env.NEXT_PUBLIC_HANZO_SEARCH_INDEX ?? 'app-ui-hanzo-ai'
|
||||
|
||||
export const indexEndpoint =
|
||||
process.env.HANZO_SEARCH_INDEX_ENDPOINT ??
|
||||
'https://cloud-api.hanzo.ai/api/index-docs'
|
||||
|
||||
export const chatEndpoint =
|
||||
process.env.NEXT_PUBLIC_HANZO_CHAT_ENDPOINT ??
|
||||
'https://cloud-api.hanzo.ai/api/chat-docs'
|
||||
|
||||
export const publishableKey =
|
||||
process.env.NEXT_PUBLIC_HANZO_SEARCH_KEY ?? 'pk-hanzo-ui-search-2026'
|
||||
|
||||
export const adminKey =
|
||||
process.env.HANZO_SEARCH_ADMIN_KEY ?? ''
|
||||
+2
-2
@@ -5,8 +5,8 @@ const nextConfig = {
|
||||
// Transpile packages that might have issues with pnpm symlinks
|
||||
transpilePackages: ["chrono-node", "@hanzo/ui"],
|
||||
|
||||
// Enable static export for GitHub Pages deployment (but not for E2E tests)
|
||||
output: process.env.GITHUB_ACTIONS && !process.env.E2E_TEST ? "export" : undefined,
|
||||
// Enable static export for GitHub Pages / Cloudflare Pages deployment (but not for E2E tests)
|
||||
output: (process.env.GITHUB_ACTIONS || process.env.CF_PAGES) && !process.env.E2E_TEST ? "export" : undefined,
|
||||
|
||||
// Use trailing slashes for GitHub Pages compatibility
|
||||
trailingSlash: true,
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user