Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
891818443a | ||
|
|
532d8e692c | ||
|
|
e45833b5f2 | ||
|
|
dffa6cef34 | ||
|
|
04407e9cb5 | ||
|
|
9f0e37d8c3 | ||
|
|
bf18ef7a35 | ||
|
|
edda5a41b8 | ||
|
|
320885b073 | ||
|
|
d97b670580 | ||
|
|
b8ecd6c408 | ||
|
|
4086d54305 | ||
|
|
6831117b97 | ||
|
|
6ea1042d44 | ||
|
|
d9f090bb53 | ||
|
|
e1184390dc | ||
|
|
a42e158a07 | ||
|
|
6347fe0aeb | ||
|
|
f08abfe55a | ||
|
|
fbe58782d9 | ||
|
|
16ffaea878 | ||
|
|
a4f1a1c16f | ||
|
|
8ed0e27d73 | ||
|
|
9958dde439 | ||
|
|
e64e9cdbdc | ||
|
|
095773b2da | ||
|
|
926985e5ad | ||
|
|
24ace5354a | ||
|
|
412ee93a25 | ||
|
|
751111a0d7 | ||
|
|
8dab1688f7 | ||
|
|
7887485e95 | ||
|
|
3e1283365a | ||
|
|
8919c32d0f | ||
|
|
be95271bc4 | ||
|
|
f2752141ff | ||
|
|
b6311022f3 | ||
|
|
07f1182d52 | ||
|
|
bacd545b67 | ||
|
|
d95693b61a | ||
|
|
dce79e1309 |
@@ -1,3 +0,0 @@
|
||||
{
|
||||
"extends": ["@commitlint/config-conventional"]
|
||||
}
|
||||
@@ -4,43 +4,3 @@ 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"
|
||||
|
||||
@@ -1,9 +0,0 @@
|
||||
<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>
|
||||
|
Before Width: | Height: | Size: 1.2 KiB |
@@ -4,7 +4,7 @@
|
||||
import { exec } from "child_process"
|
||||
import fs from "fs"
|
||||
|
||||
const pkgJsonPath = "pkgs/shadcn/package.json"
|
||||
const pkgJsonPath = "pkg/cli/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 = "pkgs/shadcn/package.json"
|
||||
const pkgJsonPath = "pkg/cli/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: hanzo-build-linux-amd64
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
@@ -47,14 +47,14 @@ jobs:
|
||||
${{ runner.os }}-pnpm-store-
|
||||
|
||||
- name: Install dependencies
|
||||
run: pnpm install --no-frozen-lockfile
|
||||
run: pnpm install --frozen-lockfile
|
||||
|
||||
- name: Run linting
|
||||
run: pnpm run lint
|
||||
|
||||
typecheck:
|
||||
name: Type Check
|
||||
runs-on: hanzo-build-linux-amd64
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
@@ -83,19 +83,19 @@ jobs:
|
||||
${{ runner.os }}-pnpm-store-
|
||||
|
||||
- name: Install dependencies
|
||||
run: pnpm install --no-frozen-lockfile
|
||||
run: pnpm install --frozen-lockfile
|
||||
|
||||
- name: Build UI package
|
||||
run: cd pkgs/ui && pnpm run build
|
||||
run: cd pkg/ui && pnpm run build
|
||||
|
||||
- name: Run type checking
|
||||
run: |
|
||||
cd app && pnpm run typecheck
|
||||
cd ../pkgs/ui && pnpm run tc --noEmit
|
||||
cd ../pkg/ui && pnpm run tc --noEmit
|
||||
|
||||
build:
|
||||
name: Build
|
||||
runs-on: hanzo-build-linux-amd64
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
@@ -124,10 +124,10 @@ jobs:
|
||||
${{ runner.os }}-pnpm-store-
|
||||
|
||||
- name: Install dependencies
|
||||
run: pnpm install --no-frozen-lockfile
|
||||
run: pnpm install --frozen-lockfile
|
||||
|
||||
- name: Build UI package
|
||||
run: cd pkgs/ui && pnpm run build:full
|
||||
run: cd pkg/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
|
||||
pkgs/ui/dist
|
||||
pkg/ui/dist
|
||||
|
||||
test:
|
||||
name: Test
|
||||
runs-on: hanzo-build-linux-amd64
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
@@ -173,10 +173,10 @@ jobs:
|
||||
${{ runner.os }}-pnpm-store-
|
||||
|
||||
- name: Install dependencies
|
||||
run: pnpm install --no-frozen-lockfile
|
||||
run: pnpm install --frozen-lockfile
|
||||
|
||||
- name: Run pkgs/ui tests
|
||||
run: cd pkgs/ui && pnpm test -- --run
|
||||
- name: Run pkg/ui tests
|
||||
run: cd pkg/ui && pnpm test -- --run
|
||||
|
||||
- name: Run app tests
|
||||
run: |
|
||||
@@ -190,12 +190,12 @@ jobs:
|
||||
if: always()
|
||||
uses: codecov/codecov-action@v3
|
||||
with:
|
||||
files: ./pkgs/ui/coverage/lcov.info
|
||||
files: ./pkg/ui/coverage/lcov.info
|
||||
flags: unit
|
||||
|
||||
deploy-preview:
|
||||
name: Deploy Preview
|
||||
runs-on: hanzo-deploy-linux-amd64
|
||||
runs-on: ubuntu-latest
|
||||
needs: [lint, typecheck, build]
|
||||
if: github.event_name == 'pull_request'
|
||||
steps:
|
||||
@@ -244,7 +244,7 @@ jobs:
|
||||
|
||||
status:
|
||||
name: CI Status
|
||||
runs-on: hanzo-build-linux-amd64
|
||||
runs-on: ubuntu-latest
|
||||
needs: [lint, typecheck, build, test]
|
||||
if: always()
|
||||
steps:
|
||||
|
||||
@@ -6,7 +6,7 @@ on:
|
||||
|
||||
jobs:
|
||||
lint:
|
||||
runs-on: hanzo-build-linux-amd64
|
||||
runs-on: ubuntu-latest
|
||||
name: pnpm lint
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
@@ -18,11 +18,11 @@ jobs:
|
||||
with:
|
||||
node-version: 20
|
||||
|
||||
- uses: pnpm/action-setup@v4
|
||||
- uses: pnpm/action-setup@v2.2.4
|
||||
name: Install pnpm
|
||||
id: pnpm-install
|
||||
with:
|
||||
version: 9.0.6
|
||||
version: 8.6.1
|
||||
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: hanzo-build-linux-amd64
|
||||
runs-on: ubuntu-latest
|
||||
name: pnpm format:check
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
@@ -54,11 +54,11 @@ jobs:
|
||||
with:
|
||||
node-version: 20
|
||||
|
||||
- uses: pnpm/action-setup@v4
|
||||
- uses: pnpm/action-setup@v2.2.4
|
||||
name: Install pnpm
|
||||
id: pnpm-install
|
||||
with:
|
||||
version: 9.0.6
|
||||
version: 8.6.1
|
||||
run_install: false
|
||||
|
||||
- name: Get pnpm store directory
|
||||
@@ -77,13 +77,10 @@ jobs:
|
||||
- name: Install dependencies
|
||||
run: pnpm install
|
||||
|
||||
- name: Build packages
|
||||
run: pnpm --filter=shadcn build
|
||||
|
||||
- run: pnpm format:check
|
||||
|
||||
tsc:
|
||||
runs-on: hanzo-build-linux-amd64
|
||||
runs-on: ubuntu-latest
|
||||
name: pnpm typecheck
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
@@ -95,11 +92,11 @@ jobs:
|
||||
with:
|
||||
node-version: 20
|
||||
|
||||
- uses: pnpm/action-setup@v4
|
||||
- uses: pnpm/action-setup@v2.2.4
|
||||
name: Install pnpm
|
||||
id: pnpm-install
|
||||
with:
|
||||
version: 9.0.6
|
||||
version: 8.6.1
|
||||
run_install: false
|
||||
|
||||
- name: Get pnpm store directory
|
||||
@@ -116,7 +113,4 @@ 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: hanzo-build-linux-amd64
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
@@ -26,15 +26,15 @@ jobs:
|
||||
version: 9
|
||||
|
||||
- name: Install dependencies
|
||||
run: pnpm install --no-frozen-lockfile
|
||||
run: pnpm install --frozen-lockfile
|
||||
|
||||
- name: Run tests with coverage
|
||||
run: cd pkgs/ui && pnpm test:coverage
|
||||
run: cd pkg/ui && pnpm test:coverage
|
||||
|
||||
- name: Upload coverage to Codecov
|
||||
uses: codecov/codecov-action@v3
|
||||
with:
|
||||
files: ./pkgs/ui/coverage/lcov.info
|
||||
files: ./pkg/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('./pkgs/ui/coverage/coverage-summary.json', 'utf8'));
|
||||
const coverage = JSON.parse(fs.readFileSync('./pkg/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: hanzo-build-linux-amd64
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 10
|
||||
steps:
|
||||
- name: Checkout
|
||||
@@ -53,11 +53,11 @@ jobs:
|
||||
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
pnpm install --no-frozen-lockfile
|
||||
pnpm install --frozen-lockfile
|
||||
|
||||
- name: Build @hanzo/ui package
|
||||
run: |
|
||||
cd pkgs/ui && pnpm build
|
||||
cd pkg/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: hanzo-deploy-linux-amd64
|
||||
runs-on: ubuntu-latest
|
||||
needs: build
|
||||
steps:
|
||||
- name: Deploy to GitHub Pages
|
||||
|
||||
@@ -11,7 +11,7 @@ permissions:
|
||||
|
||||
jobs:
|
||||
deprecated:
|
||||
runs-on: hanzo-build-linux-amd64
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout PR
|
||||
uses: actions/checkout@v4
|
||||
|
||||
@@ -8,7 +8,7 @@ on:
|
||||
|
||||
jobs:
|
||||
stale:
|
||||
runs-on: hanzo-build-linux-amd64
|
||||
runs-on: ubuntu-latest
|
||||
if: github.repository_owner == 'shadcn-ui'
|
||||
steps:
|
||||
- uses: actions/stale@v9
|
||||
|
||||
@@ -31,7 +31,7 @@ on:
|
||||
|
||||
jobs:
|
||||
publish:
|
||||
runs-on: hanzo-build-linux-amd64
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
@@ -43,6 +43,7 @@ jobs:
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: '20'
|
||||
cache: 'pnpm'
|
||||
registry-url: 'https://registry.npmjs.org'
|
||||
|
||||
- name: Install dependencies
|
||||
@@ -51,31 +52,31 @@ jobs:
|
||||
- name: Build packages
|
||||
run: |
|
||||
if [ "${{ github.event.inputs.package }}" = "all" ] || [ "${{ github.event.inputs.package }}" = "ui" ]; then
|
||||
cd pkgs/ui && pnpm build
|
||||
cd pkg/ui && pnpm build
|
||||
cd ../..
|
||||
fi
|
||||
if [ "${{ github.event.inputs.package }}" = "all" ] || [ "${{ github.event.inputs.package }}" = "ui-mcp" ]; then
|
||||
cd pkgs/ui-mcp && pnpm build
|
||||
cd pkg/ui-mcp && pnpm build
|
||||
cd ../..
|
||||
fi
|
||||
if [ "${{ github.event.inputs.package }}" = "all" ] || [ "${{ github.event.inputs.package }}" = "auth" ]; then
|
||||
cd pkgs/auth && pnpm build
|
||||
cd pkg/auth && pnpm build
|
||||
cd ../..
|
||||
fi
|
||||
if [ "${{ github.event.inputs.package }}" = "all" ] || [ "${{ github.event.inputs.package }}" = "commerce" ]; then
|
||||
cd pkgs/commerce && pnpm build
|
||||
cd pkg/commerce && pnpm build
|
||||
cd ../..
|
||||
fi
|
||||
if [ "${{ github.event.inputs.package }}" = "all" ] || [ "${{ github.event.inputs.package }}" = "checkout" ]; then
|
||||
cd pkgs/checkout && pnpm build
|
||||
cd pkg/checkout && pnpm build
|
||||
cd ../..
|
||||
fi
|
||||
if [ "${{ github.event.inputs.package }}" = "all" ] || [ "${{ github.event.inputs.package }}" = "brand" ]; then
|
||||
cd pkgs/brand && pnpm build
|
||||
cd pkg/brand && pnpm build
|
||||
cd ../..
|
||||
fi
|
||||
if [ "${{ github.event.inputs.package }}" = "all" ] || [ "${{ github.event.inputs.package }}" = "react" ]; then
|
||||
cd pkgs/react && pnpm build
|
||||
cd pkg/react && pnpm build
|
||||
cd ../..
|
||||
fi
|
||||
|
||||
@@ -141,34 +142,34 @@ jobs:
|
||||
|
||||
case "${{ github.event.inputs.package }}" in
|
||||
ui)
|
||||
publish_package "pkgs/ui"
|
||||
publish_package "pkg/ui"
|
||||
;;
|
||||
ui-mcp)
|
||||
publish_package "pkgs/ui-mcp"
|
||||
publish_package "pkg/ui-mcp"
|
||||
;;
|
||||
auth)
|
||||
publish_package "pkgs/auth"
|
||||
publish_package "pkg/auth"
|
||||
;;
|
||||
commerce)
|
||||
publish_package "pkgs/commerce"
|
||||
publish_package "pkg/commerce"
|
||||
;;
|
||||
checkout)
|
||||
publish_package "pkgs/checkout"
|
||||
publish_package "pkg/checkout"
|
||||
;;
|
||||
brand)
|
||||
publish_package "pkgs/brand"
|
||||
publish_package "pkg/brand"
|
||||
;;
|
||||
react)
|
||||
publish_package "pkgs/react"
|
||||
publish_package "pkg/react"
|
||||
;;
|
||||
all)
|
||||
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"
|
||||
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"
|
||||
;;
|
||||
esac
|
||||
|
||||
|
||||
@@ -10,9 +10,9 @@ on:
|
||||
jobs:
|
||||
comment:
|
||||
if: |
|
||||
github.repository_owner == 'shadcn-ui' &&
|
||||
github.repository_owner == 'hanzoai-ui' &&
|
||||
${{ github.event.workflow_run.conclusion == 'success' }}
|
||||
runs-on: hanzo-build-linux-amd64
|
||||
runs-on: ubuntu-latest
|
||||
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-shadcn@(.*?)-pr-(\d+)/.exec(artifact.name);
|
||||
|
||||
const match = /^npm-package-hanzoai-ui@(.*?)-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
|
||||
pnpm dlx shadcn@${{ env.BETA_PACKAGE_VERSION }}
|
||||
npx hanzoai-ui@${{ env.BETA_PACKAGE_VERSION }}
|
||||
```
|
||||
|
||||
- name: "Remove the autorelease label once published"
|
||||
|
||||
@@ -7,39 +7,31 @@ on:
|
||||
types: [labeled]
|
||||
branches:
|
||||
- main
|
||||
|
||||
permissions:
|
||||
id-token: write
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
prerelease:
|
||||
if: |
|
||||
github.repository_owner == 'shadcn-ui' &&
|
||||
github.repository_owner == 'hanzoai' &&
|
||||
contains(github.event.pull_request.labels.*.name, '🚀 autorelease')
|
||||
name: Build & Publish a beta release to NPM
|
||||
runs-on: hanzo-build-linux-amd64
|
||||
runs-on: ubuntu-latest
|
||||
environment: Preview
|
||||
|
||||
steps:
|
||||
- name: Checkout Repo
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@v3
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Use PNPM
|
||||
uses: pnpm/action-setup@v4
|
||||
uses: pnpm/action-setup@v2.2.4
|
||||
with:
|
||||
version: 9.0.6
|
||||
version: 8.6.1
|
||||
|
||||
- name: Use Node.js 20
|
||||
uses: actions/setup-node@v4
|
||||
- name: Use Node.js 18
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: 20
|
||||
registry-url: "https://registry.npmjs.org"
|
||||
|
||||
- name: Update npm for OIDC support
|
||||
run: npm install -g npm@latest
|
||||
node-version: 18
|
||||
cache: "pnpm"
|
||||
|
||||
- name: Install NPM Dependencies
|
||||
run: pnpm install
|
||||
@@ -47,6 +39,11 @@ 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
|
||||
|
||||
@@ -54,10 +51,10 @@ jobs:
|
||||
id: package-version
|
||||
uses: martinbeentjes/npm-get-version-action@main
|
||||
with:
|
||||
path: pkgs/shadcn
|
||||
path: pkg/cli
|
||||
|
||||
- name: Upload packaged artifact
|
||||
uses: actions/upload-artifact@v4
|
||||
uses: actions/upload-artifact@v2
|
||||
with:
|
||||
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
|
||||
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
|
||||
|
||||
@@ -8,7 +8,7 @@ on:
|
||||
jobs:
|
||||
test:
|
||||
name: Run Tests
|
||||
runs-on: hanzo-build-linux-amd64
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
@@ -20,27 +20,29 @@ jobs:
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: '20'
|
||||
cache: 'pnpm'
|
||||
|
||||
- name: Install dependencies
|
||||
run: pnpm install --no-frozen-lockfile
|
||||
run: pnpm install --frozen-lockfile
|
||||
|
||||
- name: Build packages
|
||||
run: |
|
||||
cd pkgs/ui && pnpm build && cd ../..
|
||||
cd pkgs/commerce && pnpm build && cd ../..
|
||||
cd pkgs/brand && pnpm build && cd ../..
|
||||
cd pkgs/react && pnpm build && cd ../..
|
||||
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 ../..
|
||||
|
||||
- name: Run tests
|
||||
run: |
|
||||
cd pkgs/ui && pnpm test
|
||||
cd pkg/ui && pnpm test
|
||||
cd ../react && pnpm test
|
||||
cd ../..
|
||||
|
||||
publish:
|
||||
name: Publish to NPM
|
||||
needs: test
|
||||
runs-on: hanzo-build-linux-amd64
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: write
|
||||
id-token: write
|
||||
@@ -55,28 +57,30 @@ jobs:
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: '20'
|
||||
cache: 'pnpm'
|
||||
registry-url: 'https://registry.npmjs.org'
|
||||
|
||||
- name: Install dependencies
|
||||
run: pnpm install --no-frozen-lockfile
|
||||
run: pnpm install --frozen-lockfile
|
||||
|
||||
- name: Build all packages
|
||||
run: |
|
||||
cd pkgs/ui && pnpm build && cd ../..
|
||||
cd pkgs/commerce && pnpm build && cd ../..
|
||||
cd pkgs/brand && pnpm build && cd ../..
|
||||
cd pkgs/react && pnpm build && cd ../..
|
||||
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 ../..
|
||||
|
||||
- name: Configure npm authentication
|
||||
env:
|
||||
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
|
||||
NODE_AUTH_TOKEN: ${{ secrets.NPM_AUTH_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_TOKEN }}
|
||||
NODE_AUTH_TOKEN: ${{ secrets.NPM_AUTH_TOKEN }}
|
||||
run: |
|
||||
echo "Checking all packages for unpublished versions..."
|
||||
|
||||
@@ -84,8 +88,8 @@ jobs:
|
||||
SKIPPED_COUNT=0
|
||||
PUBLISHED_PACKAGES=""
|
||||
|
||||
for package in ui commerce brand react; do
|
||||
cd "pkgs/$package"
|
||||
for package in ui auth commerce brand react; do
|
||||
cd "pkg/$package"
|
||||
|
||||
CURRENT_VERSION=$(node -p "require('./package.json').version")
|
||||
PACKAGE_NAME=$(node -p "require('./package.json').name")
|
||||
@@ -142,7 +146,7 @@ jobs:
|
||||
|
||||
```bash
|
||||
# Install latest versions
|
||||
npm install @hanzo/ui @hanzo/commerce @hanzo/brand @hanzo/react
|
||||
npm install @hanzo/ui @hanzo/auth @hanzo/commerce @hanzo/brand @hanzo/react
|
||||
```
|
||||
files: |
|
||||
CHANGELOG.md
|
||||
|
||||
@@ -4,12 +4,11 @@ on:
|
||||
push:
|
||||
branches: [main]
|
||||
paths:
|
||||
- 'pkgs/*/package.json'
|
||||
- 'pkgs/*/package.json'
|
||||
- 'pkg/*/package.json'
|
||||
|
||||
jobs:
|
||||
detect-changes:
|
||||
runs-on: hanzo-build-linux-amd64
|
||||
runs-on: ubuntu-latest
|
||||
outputs:
|
||||
packages: ${{ steps.changed.outputs.packages }}
|
||||
steps:
|
||||
@@ -20,47 +19,37 @@ jobs:
|
||||
- name: Detect changed packages
|
||||
id: changed
|
||||
run: |
|
||||
CHANGED=()
|
||||
for pkg_json in pkgs/*/package.json pkgs/*/package.json; do
|
||||
[ ! -f "$pkg_json" ] && continue
|
||||
CHANGED_PACKAGES=()
|
||||
for pkg_json in pkg/*/package.json; do
|
||||
pkg_dir=$(dirname "$pkg_json")
|
||||
pkg_name=$(basename "$pkg_dir")
|
||||
|
||||
# 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
|
||||
# Check if package.json version changed
|
||||
if git diff HEAD~1 HEAD --quiet "$pkg_json" 2>/dev/null; then
|
||||
continue
|
||||
fi
|
||||
|
||||
OLD=$(git show HEAD~1:"$pkg_json" 2>/dev/null | jq -r '.version' || echo "0.0.0")
|
||||
NEW=$(jq -r '.version' "$pkg_json")
|
||||
# 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")
|
||||
|
||||
if [ "$OLD" != "$NEW" ]; then
|
||||
echo "Version change: $PKG_NAME ($OLD -> $NEW)"
|
||||
CHANGED+=("$(basename "$pkg_dir")")
|
||||
if [ "$OLD_VERSION" != "$NEW_VERSION" ]; then
|
||||
echo "Version change detected: $pkg_name ($OLD_VERSION -> $NEW_VERSION)"
|
||||
CHANGED_PACKAGES+=("$pkg_name")
|
||||
fi
|
||||
done
|
||||
|
||||
if [ ${#CHANGED[@]} -eq 0 ]; then
|
||||
if [ ${#CHANGED_PACKAGES[@]} -eq 0 ]; then
|
||||
echo "packages=[]" >> $GITHUB_OUTPUT
|
||||
else
|
||||
JSON=$(printf '%s\n' "${CHANGED[@]}" | jq -R . | jq -s -c .)
|
||||
JSON=$(printf '%s\n' "${CHANGED_PACKAGES[@]}" | jq -R . | jq -s -c .)
|
||||
echo "packages=$JSON" >> $GITHUB_OUTPUT
|
||||
fi
|
||||
|
||||
publish:
|
||||
needs: detect-changes
|
||||
if: needs.detect-changes.outputs.packages != '[]'
|
||||
runs-on: hanzo-build-linux-amd64
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
matrix:
|
||||
package: ${{ fromJson(needs.detect-changes.outputs.packages) }}
|
||||
@@ -73,6 +62,7 @@ jobs:
|
||||
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 == 'shadcn-ui' }}
|
||||
if: ${{ github.repository_owner == 'hanzoai-ui' }}
|
||||
name: Create a PR for release workflow
|
||||
runs-on: hanzo-build-linux-amd64
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout Repo
|
||||
uses: actions/checkout@v3
|
||||
@@ -33,6 +33,7 @@ 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
|
||||
|
||||
@@ -0,0 +1,100 @@
|
||||
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
|
||||
+61
-30
@@ -1,45 +1,76 @@
|
||||
name: Test
|
||||
name: E2E and Visual Tests
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [main, develop]
|
||||
pull_request:
|
||||
branches: ["*"]
|
||||
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
|
||||
|
||||
jobs:
|
||||
test:
|
||||
runs-on: hanzo-build-linux-amd64
|
||||
name: pnpm test
|
||||
env:
|
||||
NEXT_PUBLIC_APP_URL: http://localhost:4000
|
||||
NEXT_PUBLIC_V0_URL: https://v0.dev
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 60
|
||||
strategy:
|
||||
matrix:
|
||||
node-version: [20.x]
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Install Node.js
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: 22
|
||||
|
||||
- uses: pnpm/action-setup@v4
|
||||
name: Install pnpm
|
||||
id: pnpm-install
|
||||
- name: Setup pnpm
|
||||
uses: pnpm/action-setup@v2
|
||||
with:
|
||||
version: 9.0.6
|
||||
run_install: false
|
||||
|
||||
- name: Get pnpm store directory
|
||||
id: pnpm-cache
|
||||
run: |
|
||||
echo "pnpm_cache_dir=$(pnpm store path)" >> $GITHUB_OUTPUT
|
||||
- uses: actions/cache@v3
|
||||
name: Setup pnpm cache
|
||||
- name: Setup Node.js ${{ matrix.node-version }}
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
path: ${{ steps.pnpm-cache.outputs.pnpm_cache_dir }}
|
||||
key: ${{ runner.os }}-pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-pnpm-store-
|
||||
node-version: ${{ matrix.node-version }}
|
||||
cache: 'pnpm'
|
||||
|
||||
- name: Install dependencies
|
||||
run: pnpm install
|
||||
|
||||
- run: pnpm test
|
||||
- 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
|
||||
NEXT_PUBLIC_APP_URL: https://ui.hanzo.ai
|
||||
|
||||
- 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
|
||||
|
||||
@@ -13,46 +13,8 @@ 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: hanzo-build-linux-amd64
|
||||
runs-on: ubuntu-latest
|
||||
name: pnpm validate:registries
|
||||
permissions:
|
||||
contents: read
|
||||
@@ -66,44 +28,6 @@ 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,7 +5,6 @@ node_modules
|
||||
**/node_modules
|
||||
.pnp
|
||||
.pnp.js
|
||||
/pnpm-lock.yaml
|
||||
|
||||
# testing
|
||||
coverage
|
||||
@@ -81,4 +80,3 @@ playwright-report/
|
||||
.notes
|
||||
.claude/settings.local.json
|
||||
pkg/ui/dist-test/
|
||||
app/public/api/
|
||||
|
||||
@@ -1,3 +1,2 @@
|
||||
auto-install-peers=true
|
||||
link-workspace-packages=true
|
||||
puppeteer_skip_download=true
|
||||
|
||||
@@ -1,8 +0,0 @@
|
||||
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": "pkgs/*/" },
|
||||
{ "pattern": "pkgs/*/" }
|
||||
{ "pattern": "packages/*/" },
|
||||
{ "pattern": "pkg/*/" }
|
||||
],
|
||||
"tailwindCSS.classFunctions": ["cva", "cn"],
|
||||
"vitest.debugExclude": [
|
||||
@@ -19,6 +19,6 @@
|
||||
"search.exclude": {
|
||||
"apps/v4/registry/radix-*": true,
|
||||
"apps/v4/public/r/*": true,
|
||||
"pkgs/shadcn/test/fixtures/*": true
|
||||
"packages/shadcn/test/fixtures/*": true
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,190 +0,0 @@
|
||||
# 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).
|
||||
+48
-30
@@ -1,10 +1,10 @@
|
||||
# Contributing
|
||||
|
||||
Thanks for your interest in contributing to ui.shadcn.com. We're happy to have you here.
|
||||
Thanks for your interest in contributing to ui.hanzo.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 [@shadcn](https://twitter.com/shadcn).
|
||||
If you need any help, feel free to reach out to [@hanzo](https://x.com/hanzoai).
|
||||
|
||||
## About this repository
|
||||
|
||||
@@ -20,25 +20,28 @@ This repository is structured as follows:
|
||||
|
||||
```
|
||||
apps
|
||||
└── v4
|
||||
└── www
|
||||
├── app
|
||||
├── components
|
||||
├── content
|
||||
└── registry
|
||||
└── new-york-v4
|
||||
├── default
|
||||
│ ├── example
|
||||
│ └── ui
|
||||
└── new-york
|
||||
├── example
|
||||
└── ui
|
||||
packages
|
||||
└── shadcn
|
||||
└── cli
|
||||
```
|
||||
|
||||
| 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. |
|
||||
| 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. |
|
||||
|
||||
## Development
|
||||
|
||||
@@ -76,61 +79,76 @@ You can use the `pnpm --filter=[WORKSPACE]` command to start the development pro
|
||||
|
||||
#### Examples
|
||||
|
||||
1. To run the `ui.shadcn.com` website:
|
||||
1. To run the `ui.hanzo.com` website:
|
||||
|
||||
```bash
|
||||
pnpm --filter=v4 dev
|
||||
pnpm --filter=www dev
|
||||
```
|
||||
|
||||
2. To run the `shadcn` package:
|
||||
2. To run the `hanzo-ui` package:
|
||||
|
||||
```bash
|
||||
pnpm --filter=shadcn dev
|
||||
pnpm --filter=hanzo-ui dev
|
||||
```
|
||||
|
||||
## Running the CLI Locally
|
||||
|
||||
To run the CLI locally, you can follow the workflow:
|
||||
|
||||
1. Start by running the dev server:
|
||||
1. Start by running the registry (main site) to make sure the components are up to date:
|
||||
|
||||
```bash
|
||||
pnpm dev
|
||||
pnpm v4:dev
|
||||
```
|
||||
|
||||
2. In another terminal tab, test the CLI by running:
|
||||
2. Run the development script for the CLI:
|
||||
|
||||
```bash
|
||||
pnpm shadcn
|
||||
pnpm hanzo:dev
|
||||
```
|
||||
|
||||
3. In another terminal tab, test the CLI by running:
|
||||
|
||||
```bash
|
||||
pnpm hanzo
|
||||
```
|
||||
|
||||
To test the CLI in a specific app, use a command like:
|
||||
|
||||
```bash
|
||||
pnpm shadcn <init | add | ...> -c ~/Desktop/my-app
|
||||
pnpm hanzo <init | add | ...> -c ~/Desktop/my-app
|
||||
```
|
||||
|
||||
4. To run the tests for the CLI:
|
||||
|
||||
```bash
|
||||
pnpm --filter=hanzo test
|
||||
```
|
||||
|
||||
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 `v4` workspace. You can run the documentation locally by running the following command:
|
||||
The documentation for this project is located in the `www` workspace. You can run the documentation locally by running the following command:
|
||||
|
||||
```bash
|
||||
pnpm --filter=v4 dev
|
||||
pnpm --filter=www dev
|
||||
```
|
||||
|
||||
Documentation is written using [MDX](https://mdxjs.com). You can find the documentation files in the `apps/v4/content/docs` directory.
|
||||
Documentation is written using [MDX](https://mdxjs.com). You can find the documentation files in the `apps/www/content/docs` directory.
|
||||
|
||||
## Components
|
||||
|
||||
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.
|
||||
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.
|
||||
|
||||
```bash
|
||||
apps
|
||||
└── v4
|
||||
└── www
|
||||
└── registry
|
||||
└── new-york-v4
|
||||
├── default
|
||||
│ ├── example
|
||||
│ └── ui
|
||||
└── new-york
|
||||
├── example
|
||||
└── ui
|
||||
```
|
||||
@@ -139,7 +157,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 registry:build` to update the registry.
|
||||
3. You run `pnpm build:registry` to update the registry.
|
||||
|
||||
## Commit Convention
|
||||
|
||||
@@ -178,9 +196,9 @@ If you have a request for a new component, please open a discussion on GitHub. W
|
||||
|
||||
## 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).
|
||||
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).
|
||||
|
||||
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.
|
||||
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.
|
||||
|
||||
## Testing
|
||||
|
||||
|
||||
@@ -10,7 +10,7 @@ React component library (shadcn/ui fork). 161 components, 24+ blocks, two themes
|
||||
|
||||
```
|
||||
ui/
|
||||
app/ Hanzo documentation site (Next.js 15.3.1, React 19)
|
||||
app/ Documentation site (Next.js 15.3.1, React 19)
|
||||
registry/ Component registry (SOURCE OF TRUTH)
|
||||
default/ui/ 150+ components
|
||||
default/example/ Usage demos
|
||||
@@ -18,25 +18,13 @@ 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)
|
||||
react/ React primitives
|
||||
brand/ Branding system
|
||||
auth/ Auth components (Firebase optional since v2.6.0)
|
||||
auth-firebase/ Firebase auth (opt-in package)
|
||||
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
|
||||
brand/ Branding system
|
||||
brands/ White-label configs (Zoo, Lux)
|
||||
```
|
||||
|
||||
## Critical: Build Order
|
||||
@@ -90,19 +78,7 @@ import { cn } from '@hanzo/ui/lib/utils'
|
||||
|
||||
## Tech Stack
|
||||
|
||||
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
|
||||
React 18.3.1 (19 experimental), Next.js 15.3.1, Tailwind CSS (OKLCH colors), Radix UI, Turborepo + pnpm, Fumadocs (MDX), class-variance-authority.
|
||||
|
||||
## Key Features
|
||||
|
||||
@@ -119,12 +95,11 @@ Strategy: file-level checkout from shadcn/main for specific directories (not git
|
||||
- Firebase split to optional `@hanzo/auth-firebase` package (Jan 2025)
|
||||
- `@hanzo/auth` v2.6.0 uses pluggable provider registry: `registerAuthProvider('firebase', FirebaseAuthService)`
|
||||
|
||||
## Component Stats
|
||||
## Component Stats (2025-10-18)
|
||||
|
||||
- 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
|
||||
|
||||
|
||||
@@ -1,6 +0,0 @@
|
||||
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,5 +1,3 @@
|
||||
<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](https://github.com/shadcn-ui/ui/security/advisories/new) button.
|
||||
To do this, please visit the security tab of the repository and click the "Report a vulnerability" button.
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
node_modules/
|
||||
.next/
|
||||
out/
|
||||
build/
|
||||
dist/
|
||||
next-env.d.ts
|
||||
__registry__/
|
||||
.source/
|
||||
*.config.js
|
||||
*.config.mjs
|
||||
.turbo/
|
||||
coverage/
|
||||
@@ -0,0 +1,40 @@
|
||||
{
|
||||
"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"
|
||||
}
|
||||
}
|
||||
@@ -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 pkgs/ui/package.json.
|
||||
* Build artifacts exist in pkgs/ui/dist/{code,3d,pattern}/.
|
||||
* The package exports are properly configured in pkg/ui/package.json.
|
||||
* Build artifacts exist in pkg/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
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,427 @@
|
||||
"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,18 +0,0 @@
|
||||
/**
|
||||
* 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 },
|
||||
)
|
||||
}
|
||||
@@ -1,38 +0,0 @@
|
||||
/**
|
||||
* 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": "*",
|
||||
},
|
||||
})
|
||||
}
|
||||
@@ -1,22 +0,0 @@
|
||||
/**
|
||||
* 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": "*",
|
||||
},
|
||||
})
|
||||
}
|
||||
@@ -1,117 +0,0 @@
|
||||
/**
|
||||
* 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
|
||||
}
|
||||
@@ -1,30 +0,0 @@
|
||||
/**
|
||||
* 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": "*",
|
||||
},
|
||||
}
|
||||
)
|
||||
}
|
||||
@@ -1,17 +0,0 @@
|
||||
/**
|
||||
* 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 },
|
||||
)
|
||||
}
|
||||
@@ -62,15 +62,7 @@ export function CommandMenu({ ...props }: DialogProps) {
|
||||
const { recentSearches, addRecentSearch, clearRecentSearches } =
|
||||
useRecentSearches()
|
||||
|
||||
// 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("⌘")
|
||||
}, [])
|
||||
const modKey = React.useMemo(() => (isMacOS() ? "⌘" : "Ctrl"), [])
|
||||
|
||||
React.useEffect(() => {
|
||||
const down = (e: KeyboardEvent) => {
|
||||
|
||||
@@ -0,0 +1,71 @@
|
||||
---
|
||||
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 |
|
||||
+19
-8
@@ -1,5 +1,5 @@
|
||||
import js from "@eslint/js";
|
||||
import tsParser from "@typescript-eslint/parser";
|
||||
import tseslint from "typescript-eslint";
|
||||
|
||||
export default [
|
||||
{
|
||||
@@ -12,16 +12,14 @@ export default [
|
||||
"dist/**",
|
||||
".turbo/**",
|
||||
"node_modules/**",
|
||||
"coverage/**",
|
||||
"*.config.js",
|
||||
"*.config.mjs",
|
||||
],
|
||||
},
|
||||
// JavaScript files
|
||||
// Use only base configs without rules
|
||||
{
|
||||
files: ["**/*.{js,jsx,mjs,cjs}"],
|
||||
...js.configs.recommended,
|
||||
rules: {
|
||||
// Turn off all rules
|
||||
"no-unused-vars": "off",
|
||||
"no-undef": "off",
|
||||
"no-empty": "off",
|
||||
@@ -32,13 +30,26 @@ export default [
|
||||
"prefer-const": "off",
|
||||
},
|
||||
},
|
||||
// TypeScript files - parser only, all rules off (rely on TypeScript compiler)
|
||||
// TypeScript files
|
||||
{
|
||||
files: ["**/*.{ts,tsx,mts}"],
|
||||
languageOptions: {
|
||||
parser: tsParser,
|
||||
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",
|
||||
},
|
||||
rules: {},
|
||||
},
|
||||
// CommonJS files
|
||||
{
|
||||
|
||||
@@ -1,33 +0,0 @@
|
||||
/**
|
||||
* 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 / Cloudflare Pages deployment (but not for E2E tests)
|
||||
output: (process.env.GITHUB_ACTIONS || process.env.CF_PAGES) && !process.env.E2E_TEST ? "export" : undefined,
|
||||
// Enable static export for GitHub Pages deployment (but not for E2E tests)
|
||||
output: process.env.GITHUB_ACTIONS && !process.env.E2E_TEST ? "export" : undefined,
|
||||
|
||||
// Use trailing slashes for GitHub Pages compatibility
|
||||
trailingSlash: true,
|
||||
|
||||
+72
-76
@@ -5,7 +5,7 @@
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "next dev --port 3333",
|
||||
"build": "pnpm registry:build && pnpm registry:api && (pnpm registry:capture || echo 'Skipping screenshots in CI') && next build",
|
||||
"build": "pnpm registry:build && (pnpm registry:capture || echo 'Skipping screenshots in CI') && next build",
|
||||
"start": "next start --port 3001",
|
||||
"lint": "eslint .",
|
||||
"lint:fix": "eslint --fix .",
|
||||
@@ -17,27 +17,24 @@
|
||||
"format:write": "prettier --write \"**/*.{ts,tsx,mdx}\" --cache",
|
||||
"format:check": "prettier --check \"**/*.{ts,tsx,mdx}\" --cache",
|
||||
"registry:build": "tsx --tsconfig ./tsconfig.scripts.json ./scripts/build-registry.mts",
|
||||
"registry:api": "tsx --tsconfig ./tsconfig.scripts.json ./scripts/build-registry-api.mts",
|
||||
"registry:capture": "tsx --tsconfig ./tsconfig.scripts.json ./scripts/capture-registry.mts",
|
||||
"validate:registries": "tsx --tsconfig ./tsconfig.scripts.json ./scripts/validate-registries.mts",
|
||||
"postinstall": "docs-mdx source.config.ts .source"
|
||||
},
|
||||
"dependencies": {
|
||||
"@ai-sdk/openai-compatible": "^0.2.0",
|
||||
"ai": "^4.3.0",
|
||||
"@ctrl/tinycolor": "^4.2.0",
|
||||
"@dnd-kit/core": "^6.3.1",
|
||||
"@dnd-kit/modifiers": "^9.0.0",
|
||||
"@dnd-kit/sortable": "^10.0.0",
|
||||
"@dnd-kit/utilities": "^3.2.2",
|
||||
"@faker-js/faker": "^10.4.0",
|
||||
"@hanzo/docs-core": "16.5.3",
|
||||
"@hanzo/docs-docgen": "3.0.7",
|
||||
"@faker-js/faker": "^10.0.0",
|
||||
"@hanzo/docs-core": "16.2.6",
|
||||
"@hanzo/docs-docgen": "3.0.4",
|
||||
"@hanzo/docs-mdx": "14.3.0",
|
||||
"@hanzo/docs-ui": "16.5.3",
|
||||
"@hanzo/logo": "^1.0.5",
|
||||
"@hanzo/ui": "workspace:@hanzo/ui-shadcn@^",
|
||||
"@hookform/resolvers": "5.2.2",
|
||||
"@hanzo/docs-ui": "16.2.6",
|
||||
"@hanzo/logo": "^1.0.3",
|
||||
"@hanzo/ui": "workspace:^",
|
||||
"@hookform/resolvers": "^5.2.2",
|
||||
"@monaco-editor/react": "^4.7.0",
|
||||
"@radix-ui/react-accessible-icon": "^1.1.8",
|
||||
"@radix-ui/react-accordion": "^1.2.12",
|
||||
@@ -70,18 +67,18 @@
|
||||
"@radix-ui/react-toggle": "^1.1.10",
|
||||
"@radix-ui/react-toggle-group": "^1.1.11",
|
||||
"@radix-ui/react-tooltip": "^1.2.8",
|
||||
"@rainbow-me/rainbowkit": "^2.2.10",
|
||||
"@rc-component/color-picker": "^3.1.1",
|
||||
"@tabler/icons-react": "^3.40.0",
|
||||
"@tanstack/react-query": "^5.94.5",
|
||||
"@rainbow-me/rainbowkit": "^2.2.9",
|
||||
"@rc-component/color-picker": "^3.0.2",
|
||||
"@tabler/icons-react": "^3.35.0",
|
||||
"@tanstack/react-query": "^5.90.7",
|
||||
"@tanstack/react-table": "^8.21.3",
|
||||
"@tiptap/extension-placeholder": "^3.20.4",
|
||||
"@tiptap/react": "^3.20.4",
|
||||
"@tiptap/starter-kit": "^3.20.4",
|
||||
"@tiptap/extension-placeholder": "^3.10.2",
|
||||
"@tiptap/react": "^3.10.2",
|
||||
"@tiptap/starter-kit": "^3.10.2",
|
||||
"@types/react-syntax-highlighter": "^15.5.13",
|
||||
"@vercel/analytics": "^2.0.1",
|
||||
"@vercel/og": "^0.11.1",
|
||||
"@xyflow/react": "^12.10.1",
|
||||
"@vercel/analytics": "^1.5.0",
|
||||
"@vercel/og": "^0.8.5",
|
||||
"@xyflow/react": "^12.8.6",
|
||||
"chrono-node": "^2.9.0",
|
||||
"class-variance-authority": "^0.7.1",
|
||||
"clsx": "^2.1.1",
|
||||
@@ -90,83 +87,82 @@
|
||||
"date-fns": "^4.1.0",
|
||||
"embla-carousel-autoplay": "8.6.0",
|
||||
"embla-carousel-react": "8.6.0",
|
||||
"fumadocs-core": "^16.7.4",
|
||||
"fumadocs-mdx": "^14.2.11",
|
||||
"geist": "^1.7.0",
|
||||
"fumadocs-core": "^16.4.8",
|
||||
"fumadocs-mdx": "^14.2.6",
|
||||
"geist": "^1.5.1",
|
||||
"input-otp": "^1.4.2",
|
||||
"jotai": "^2.18.1",
|
||||
"js-yaml": "^4.1.1",
|
||||
"lodash": "^4.17.23",
|
||||
"lucide-react": "^0.577.0",
|
||||
"monaco-editor": "^0.55.1",
|
||||
"motion": "^12.38.0",
|
||||
"jotai": "^2.15.0",
|
||||
"js-yaml": "^4.1.0",
|
||||
"lodash": "^4.17.21",
|
||||
"lucide-react": "^0.544.0",
|
||||
"monaco-editor": "^0.53.0",
|
||||
"motion": "^12.34.0",
|
||||
"next-themes": "0.4.6",
|
||||
"qrcode.react": "^4.2.0",
|
||||
"react": "19.2.4",
|
||||
"react": "19.2.0",
|
||||
"react-colorful": "^5.6.1",
|
||||
"react-day-picker": "^9.14.0",
|
||||
"react-dom": "19.2.4",
|
||||
"react-dropzone": "^15.0.0",
|
||||
"react-hook-form": "^7.71.2",
|
||||
"react-day-picker": "^9.11.0",
|
||||
"react-dom": "19.2.0",
|
||||
"react-dropzone": "^14.3.8",
|
||||
"react-hook-form": "^7.64.0",
|
||||
"react-image-crop": "^11.0.10",
|
||||
"react-medium-image-zoom": "^5.4.1",
|
||||
"react-medium-image-zoom": "^5.4.0",
|
||||
"react-resizable-panels": "^3.0.6",
|
||||
"react-syntax-highlighter": "^16.1.1",
|
||||
"react-syntax-highlighter": "^15.6.6",
|
||||
"react-wrap-balancer": "^1.1.1",
|
||||
"recharts": "3.8.0",
|
||||
"recharts": "3.3.0",
|
||||
"sonner": "^2.0.7",
|
||||
"swr": "^2.4.1",
|
||||
"tailwind-merge": "^3.5.0",
|
||||
"swr": "^2.3.6",
|
||||
"tailwind-merge": "^3.3.1",
|
||||
"tailwindcss-animate": "^1.0.7",
|
||||
"ts-morph": "27.0.2",
|
||||
"vaul": "1.1.2",
|
||||
"viem": "^2.47.6",
|
||||
"wagmi": "^3.5.0",
|
||||
"zod": "^4.3.6",
|
||||
"zustand": "^5.0.12"
|
||||
"viem": "^2.38.6",
|
||||
"wagmi": "^2.19.2",
|
||||
"zod": "^4.1.11",
|
||||
"zustand": "^5.0.8"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@eslint/eslintrc": "^3.3.5",
|
||||
"@ianvs/prettier-plugin-sort-imports": "^4.7.1",
|
||||
"@playwright/test": "^1.58.2",
|
||||
"@tailwindcss/postcss": "^4.2.2",
|
||||
"@eslint/eslintrc": "^3.3.1",
|
||||
"@ianvs/prettier-plugin-sort-imports": "^4.7.0",
|
||||
"@playwright/test": "^1.55.1",
|
||||
"@tailwindcss/postcss": "^4.1.17",
|
||||
"@testing-library/jest-dom": "^6.9.1",
|
||||
"@testing-library/react": "^16.3.2",
|
||||
"@testing-library/react": "^16.3.0",
|
||||
"@types/js-yaml": "^4.0.9",
|
||||
"@types/lodash": "^4.17.24",
|
||||
"@types/lodash": "^4.17.20",
|
||||
"@types/mdx": "^2.0.13",
|
||||
"@types/node": "^25.5.0",
|
||||
"@types/react": "19.2.14",
|
||||
"@types/react-dom": "19.2.3",
|
||||
"@types/node": "^24.6.2",
|
||||
"@types/react": "19.2.2",
|
||||
"@types/react-dom": "19.2.2",
|
||||
"@types/unist": "^3.0.3",
|
||||
"@typescript-eslint/parser": "^8.57.1",
|
||||
"@vitejs/plugin-react": "^6.0.1",
|
||||
"@vitest/ui": "^4.1.0",
|
||||
"autoprefixer": "^10.4.27",
|
||||
"@typescript-eslint/parser": "^8.46.3",
|
||||
"@vitejs/plugin-react": "^5.0.4",
|
||||
"@vitest/ui": "^4.0.7",
|
||||
"autoprefixer": "^10.4.21",
|
||||
"del-cli": "^7.0.0",
|
||||
"@eslint/js": "^9.28.0",
|
||||
"eslint": "^10.1.0",
|
||||
"eslint-config-next": "16.2.1",
|
||||
"happy-dom": "^20.8.4",
|
||||
"next": "^16.2.1",
|
||||
"eslint": "^9.39.1",
|
||||
"eslint-config-next": "16.0.1",
|
||||
"happy-dom": "^20.0.10",
|
||||
"next": "^16.1.6",
|
||||
"npm-run-all": "^4.1.5",
|
||||
"playwright": "^1.58.2",
|
||||
"playwright": "^1.55.1",
|
||||
"png-to-ico": "^3.0.1",
|
||||
"postcss": "^8.5.8",
|
||||
"postcss-selector-parser": "^7.1.1",
|
||||
"prettier": "^3.8.1",
|
||||
"prettier-plugin-tailwindcss": "^0.7.2",
|
||||
"puppeteer": "^25.1.0",
|
||||
"rehype-pretty-code": "^0.14.3",
|
||||
"rimraf": "^6.1.3",
|
||||
"sharp": "^0.34.5",
|
||||
"shiki": "^4.0.2",
|
||||
"tailwindcss": "^4.2.2",
|
||||
"tsx": "^4.21.0",
|
||||
"postcss": "^8.5.6",
|
||||
"postcss-selector-parser": "^7.1.0",
|
||||
"prettier": "^3.6.2",
|
||||
"prettier-plugin-tailwindcss": "^0.6.14",
|
||||
"puppeteer": "^23.6.0",
|
||||
"rehype-pretty-code": "^0.14.1",
|
||||
"rimraf": "^6.0.1",
|
||||
"sharp": "^0.34.4",
|
||||
"shiki": "^3.15.0",
|
||||
"tailwindcss": "^4.1.17",
|
||||
"tsx": "^4.20.6",
|
||||
"tw-animate-css": "^1.4.0",
|
||||
"typescript": "^5.9.3",
|
||||
"unist-builder": "4.0.0",
|
||||
"unist-util-visit": "^5.1.0",
|
||||
"vitest": "^4.1.0"
|
||||
"unist-util-visit": "^5.0.0",
|
||||
"vitest": "^4.0.7"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -76,7 +76,6 @@ export default function Component() {
|
||||
strokeWidth={2}
|
||||
dot={({ cx, cy, payload }) => {
|
||||
const r = 24
|
||||
if (cx == null || cy == null) return null
|
||||
return (
|
||||
<GitCommitVertical
|
||||
key={payload.month}
|
||||
|
||||
@@ -749,7 +749,7 @@ const AICode = React.forwardRef<HTMLDivElement, AICodeProps>(
|
||||
|
||||
// Setup hover providers for explanations
|
||||
monaco.languages.registerHoverProvider(currentLanguage, {
|
||||
provideHover: (model: any, position: any) => {
|
||||
provideHover: (model, position) => {
|
||||
const line = position.lineNumber
|
||||
const explanation = explanations.find((e) => e.line === line)
|
||||
|
||||
@@ -768,7 +768,7 @@ const AICode = React.forwardRef<HTMLDivElement, AICodeProps>(
|
||||
|
||||
// Setup code actions for suggestions
|
||||
monaco.languages.registerCodeActionProvider(currentLanguage, {
|
||||
provideCodeActions: (model: any, range: any, context: any, token: any) => {
|
||||
provideCodeActions: (model, range, context, token) => {
|
||||
const actions = suggestions
|
||||
.filter(
|
||||
(s) =>
|
||||
|
||||
@@ -1,119 +0,0 @@
|
||||
/**
|
||||
* Build static API responses from the registry data.
|
||||
*
|
||||
* Generates /public/api/registry/ files that work on CF Pages,
|
||||
* GitHub Pages, or any static hosting. Run after build-registry.mts.
|
||||
*
|
||||
* Output:
|
||||
* public/api/registry/index.json — full manifest (all components + source)
|
||||
* public/api/registry/components.json — component list (no source)
|
||||
* public/api/registry/components/{name}.json — individual component with source
|
||||
* public/api/registry/search-index.json — lightweight search index
|
||||
*/
|
||||
|
||||
import { existsSync, mkdirSync, readFileSync, readdirSync, writeFileSync } from "fs"
|
||||
import path from "path"
|
||||
|
||||
const REGISTRY_DIR = path.join(process.cwd(), "public/registry")
|
||||
const STYLES_DIR = path.join(REGISTRY_DIR, "styles/default")
|
||||
const API_DIR = path.join(process.cwd(), "public/api/registry")
|
||||
const COMPONENTS_DIR = path.join(API_DIR, "components")
|
||||
|
||||
interface RegistryItem {
|
||||
name: string
|
||||
type: string
|
||||
dependencies?: string[]
|
||||
devDependencies?: string[]
|
||||
registryDependencies?: string[]
|
||||
files: Array<{ name: string; content: string } | string>
|
||||
description?: string
|
||||
category?: string
|
||||
}
|
||||
|
||||
function main() {
|
||||
console.log("Building static registry API files...")
|
||||
|
||||
// Read the index
|
||||
const indexPath = path.join(REGISTRY_DIR, "index.json")
|
||||
if (!existsSync(indexPath)) {
|
||||
console.error("Registry index.json not found. Run registry:build first.")
|
||||
process.exit(1)
|
||||
}
|
||||
|
||||
const index: RegistryItem[] = JSON.parse(readFileSync(indexPath, "utf-8"))
|
||||
|
||||
// Read all component files
|
||||
const components = new Map<string, RegistryItem>()
|
||||
if (existsSync(STYLES_DIR)) {
|
||||
const files = readdirSync(STYLES_DIR).filter((f) => f.endsWith(".json"))
|
||||
for (const file of files) {
|
||||
try {
|
||||
const data: RegistryItem = JSON.parse(
|
||||
readFileSync(path.join(STYLES_DIR, file), "utf-8")
|
||||
)
|
||||
components.set(data.name, data)
|
||||
} catch {
|
||||
// skip
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Create output directories
|
||||
mkdirSync(COMPONENTS_DIR, { recursive: true })
|
||||
|
||||
// 1. Component list (no source — lightweight)
|
||||
const componentList = index.map((item) => ({
|
||||
name: item.name,
|
||||
type: item.type,
|
||||
dependencies: item.dependencies,
|
||||
registryDependencies: item.registryDependencies,
|
||||
}))
|
||||
|
||||
writeFileSync(
|
||||
path.join(API_DIR, "components.json"),
|
||||
JSON.stringify({ total: componentList.length, components: componentList })
|
||||
)
|
||||
console.log(` components.json: ${componentList.length} components`)
|
||||
|
||||
// 2. Individual component files (with source)
|
||||
let written = 0
|
||||
for (const [name, data] of components) {
|
||||
writeFileSync(
|
||||
path.join(COMPONENTS_DIR, `${name}.json`),
|
||||
JSON.stringify(data)
|
||||
)
|
||||
written++
|
||||
}
|
||||
console.log(` components/*.json: ${written} files`)
|
||||
|
||||
// 3. Full manifest (single payload with all source)
|
||||
const manifest: Record<string, any> = {}
|
||||
for (const [name, data] of components) {
|
||||
manifest[name] = data
|
||||
}
|
||||
writeFileSync(
|
||||
path.join(API_DIR, "index.json"),
|
||||
JSON.stringify({
|
||||
generated_at: Date.now(),
|
||||
total: components.size,
|
||||
components: manifest,
|
||||
})
|
||||
)
|
||||
console.log(` index.json: full manifest`)
|
||||
|
||||
// 4. Search index (lightweight — names + types for client-side search)
|
||||
const searchIndex = index.map((item) => ({
|
||||
n: item.name,
|
||||
t: item.type,
|
||||
d: (item.dependencies || []).join(","),
|
||||
}))
|
||||
writeFileSync(
|
||||
path.join(API_DIR, "search-index.json"),
|
||||
JSON.stringify(searchIndex)
|
||||
)
|
||||
console.log(` search-index.json: ${searchIndex.length} entries`)
|
||||
|
||||
console.log("Done! Static API files written to public/api/registry/")
|
||||
}
|
||||
|
||||
main()
|
||||
@@ -5,4 +5,3 @@ build
|
||||
.contentlayer
|
||||
registry/__index__.tsx
|
||||
content/docs/components/calendar.mdx
|
||||
registry/styles/**/*.css
|
||||
|
||||
@@ -93,7 +93,7 @@ export function AppearanceSettings() {
|
||||
value={gpuCount}
|
||||
onChange={handleGpuInputChange}
|
||||
size={3}
|
||||
className="h-7 w-14! font-mono"
|
||||
className="h-7 !w-14 font-mono"
|
||||
maxLength={3}
|
||||
/>
|
||||
<Button
|
||||
|
||||
@@ -46,7 +46,7 @@ export function FieldHear() {
|
||||
<FieldLabel
|
||||
htmlFor={option.value}
|
||||
key={option.value}
|
||||
className="w-fit!"
|
||||
className="!w-fit"
|
||||
>
|
||||
<Field
|
||||
orientation="horizontal"
|
||||
|
||||
@@ -19,7 +19,7 @@ import { SpinnerEmpty } from "./spinner-empty"
|
||||
|
||||
export function RootComponents() {
|
||||
return (
|
||||
<div className="mx-auto grid gap-8 py-1 theme-container md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 xl:gap-6 2xl:gap-8">
|
||||
<div className="theme-container mx-auto grid gap-8 py-1 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 xl:gap-6 2xl:gap-8">
|
||||
<div className="flex flex-col gap-6 *:[div]:w-full *:[div]:max-w-full">
|
||||
<FieldDemo />
|
||||
</div>
|
||||
|
||||
@@ -24,7 +24,7 @@ export function InputGroupButtonExample() {
|
||||
Input Secure
|
||||
</Label>
|
||||
<InputGroup className="[--radius:9999px]">
|
||||
<InputGroupInput id="input-secure-19" className="pl-0.5!" />
|
||||
<InputGroupInput id="input-secure-19" className="!pl-0.5" />
|
||||
<Popover>
|
||||
<PopoverTrigger asChild>
|
||||
<InputGroupAddon>
|
||||
@@ -46,7 +46,7 @@ export function InputGroupButtonExample() {
|
||||
<p>You should not enter any sensitive information on this site.</p>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
<InputGroupAddon className="pl-1! text-muted-foreground">
|
||||
<InputGroupAddon className="text-muted-foreground !pl-1">
|
||||
https://
|
||||
</InputGroupAddon>
|
||||
<InputGroupAddon align="inline-end">
|
||||
|
||||
@@ -32,7 +32,7 @@ export function InputGroupDemo() {
|
||||
<InputGroupAddon align="inline-end">12 results</InputGroupAddon>
|
||||
</InputGroup>
|
||||
<InputGroup>
|
||||
<InputGroupInput placeholder="example.com" className="pl-1!" />
|
||||
<InputGroupInput placeholder="example.com" className="!pl-1" />
|
||||
<InputGroupAddon>
|
||||
<InputGroupText>https://</InputGroupText>
|
||||
</InputGroupAddon>
|
||||
@@ -73,7 +73,7 @@ export function InputGroupDemo() {
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
<InputGroupText className="ml-auto">52% used</InputGroupText>
|
||||
<Separator orientation="vertical" className="h-4!" />
|
||||
<Separator orientation="vertical" className="!h-4" />
|
||||
<InputGroupButton
|
||||
variant="default"
|
||||
className="rounded-full"
|
||||
@@ -87,7 +87,7 @@ export function InputGroupDemo() {
|
||||
<InputGroup>
|
||||
<InputGroupInput placeholder="@shadcn" />
|
||||
<InputGroupAddon align="inline-end">
|
||||
<div className="flex size-4 items-center justify-center rounded-full bg-primary text-foreground">
|
||||
<div className="bg-primary text-foreground flex size-4 items-center justify-center rounded-full">
|
||||
<IconCheck className="size-3 text-white" />
|
||||
</div>
|
||||
</InputGroupAddon>
|
||||
|
||||
@@ -42,7 +42,7 @@ export function ItemAvatar() {
|
||||
</Item>
|
||||
<Item variant="outline">
|
||||
<ItemMedia>
|
||||
<div className="flex -space-x-2 *:data-[slot=avatar]:ring-2 *:data-[slot=avatar]:ring-background *:data-[slot=avatar]:grayscale">
|
||||
<div className="*:data-[slot=avatar]:ring-background flex -space-x-2 *:data-[slot=avatar]:ring-2 *:data-[slot=avatar]:grayscale">
|
||||
<Avatar className="hidden sm:flex">
|
||||
<AvatarImage src="https://github.com/shadcn.png" alt="@shadcn" />
|
||||
<AvatarFallback>CN</AvatarFallback>
|
||||
|
||||
@@ -247,7 +247,7 @@ export function NotionPromptForm() {
|
||||
</Command>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
<div className="-m-1.5 no-scrollbar flex gap-1 overflow-y-auto p-1.5">
|
||||
<div className="no-scrollbar -m-1.5 flex gap-1 overflow-y-auto p-1.5">
|
||||
{mentions.map((mention) => {
|
||||
const item = SAMPLE_DATA.mentionable.find(
|
||||
(item) => item.title === mention
|
||||
@@ -262,7 +262,7 @@ export function NotionPromptForm() {
|
||||
key={mention}
|
||||
size="sm"
|
||||
variant="secondary"
|
||||
className="rounded-full pl-2!"
|
||||
className="rounded-full !pl-2"
|
||||
onClick={() => {
|
||||
setMentions((prev) => prev.filter((m) => m !== mention))
|
||||
}}
|
||||
@@ -302,13 +302,9 @@ export function NotionPromptForm() {
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>Select AI model</TooltipContent>
|
||||
</Tooltip>
|
||||
<DropdownMenuContent
|
||||
side="top"
|
||||
align="start"
|
||||
className="min-w-48"
|
||||
>
|
||||
<DropdownMenuGroup>
|
||||
<DropdownMenuLabel className="text-xs text-muted-foreground">
|
||||
<DropdownMenuContent side="top" align="start" className="w-48">
|
||||
<DropdownMenuGroup className="w-48">
|
||||
<DropdownMenuLabel className="text-muted-foreground text-xs">
|
||||
Select Agent Mode
|
||||
</DropdownMenuLabel>
|
||||
{SAMPLE_DATA.models.map((model) => (
|
||||
@@ -426,7 +422,7 @@ export function NotionPromptForm() {
|
||||
<DropdownMenuItem>
|
||||
<IconPlus /> Connect Apps
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuLabel className="text-xs text-muted-foreground">
|
||||
<DropdownMenuLabel className="text-muted-foreground text-xs">
|
||||
We'll only search in the sources selected here.
|
||||
</DropdownMenuLabel>
|
||||
</DropdownMenuGroup>
|
||||
|
||||
@@ -56,7 +56,7 @@ export default function IndexPage() {
|
||||
<PageHeaderDescription>{description}</PageHeaderDescription>
|
||||
<PageActions>
|
||||
<Button asChild size="sm" className="h-[31px] rounded-lg">
|
||||
<Link href="/create">New Project</Link>
|
||||
<Link href="/docs/installation">Get Started</Link>
|
||||
</Button>
|
||||
<Button asChild size="sm" variant="ghost" className="rounded-lg">
|
||||
<Link href="/docs/components">View Components</Link>
|
||||
@@ -64,12 +64,12 @@ export default function IndexPage() {
|
||||
</PageActions>
|
||||
</PageHeader>
|
||||
<PageNav className="hidden md:flex">
|
||||
<ExamplesNav className="flex-1 overflow-hidden [&>a:first-child]:text-primary" />
|
||||
<ExamplesNav className="[&>a:first-child]:text-primary flex-1 overflow-hidden" />
|
||||
<ThemeSelector className="mr-4 hidden md:flex" />
|
||||
</PageNav>
|
||||
<div className="container-wrapper flex-1 section-soft pb-6">
|
||||
<div className="container-wrapper section-soft flex-1 pb-6">
|
||||
<div className="container overflow-hidden">
|
||||
<section className="-mx-4 w-[160vw] overflow-hidden rounded-lg border border-border/50 md:hidden md:w-[150vw]">
|
||||
<section className="border-border/50 -mx-4 w-[160vw] overflow-hidden rounded-lg border md:hidden md:w-[150vw]">
|
||||
<Image
|
||||
src="/r/styles/new-york-v4/dashboard-01-light.png"
|
||||
width={1400}
|
||||
@@ -87,7 +87,7 @@ export default function IndexPage() {
|
||||
priority
|
||||
/>
|
||||
</section>
|
||||
<section className="hidden theme-container md:block">
|
||||
<section className="theme-container hidden md:block">
|
||||
<RootComponents />
|
||||
</section>
|
||||
</div>
|
||||
|
||||
@@ -56,7 +56,7 @@ export default function BlocksLayout({
|
||||
<a href="#blocks">Browse Blocks</a>
|
||||
</Button>
|
||||
<Button asChild variant="ghost" size="sm">
|
||||
<Link href="/docs/components">View Components</Link>
|
||||
<Link href="/docs/blocks">Add a block</Link>
|
||||
</Button>
|
||||
</PageActions>
|
||||
</PageHeader>
|
||||
@@ -71,7 +71,7 @@ export default function BlocksLayout({
|
||||
<Link href="/blocks/sidebar">Browse all blocks</Link>
|
||||
</Button>
|
||||
</PageNav>
|
||||
<div className="container-wrapper flex-1 section-soft md:py-12">
|
||||
<div className="container-wrapper section-soft flex-1 md:py-12">
|
||||
<div className="container">{children}</div>
|
||||
</div>
|
||||
</>
|
||||
|
||||
@@ -62,7 +62,7 @@ export default function ColorsLayout({
|
||||
<div className="hidden">
|
||||
<div className="container-wrapper">
|
||||
<div className="container flex items-center justify-between gap-8 py-4">
|
||||
<ColorsNav className="flex-1 overflow-hidden [&>a:first-child]:text-primary" />
|
||||
<ColorsNav className="[&>a:first-child]:text-primary flex-1 overflow-hidden" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -134,7 +134,7 @@ export default async function Page(props: {
|
||||
</div>
|
||||
</div>
|
||||
{doc.description && (
|
||||
<p className="text-[1.05rem] text-muted-foreground sm:text-base sm:text-balance md:max-w-[80%]">
|
||||
<p className="text-muted-foreground text-[1.05rem] sm:text-base sm:text-balance md:max-w-[80%]">
|
||||
{doc.description}
|
||||
</p>
|
||||
)}
|
||||
|
||||
@@ -57,7 +57,7 @@ export default function ChangelogPage() {
|
||||
</a>
|
||||
</Button>
|
||||
</div>
|
||||
<p className="text-[1.05rem] text-muted-foreground sm:text-base sm:text-balance md:max-w-[80%]">
|
||||
<p className="text-muted-foreground text-[1.05rem] sm:text-base sm:text-balance md:max-w-[80%]">
|
||||
Latest updates and announcements.
|
||||
</p>
|
||||
</div>
|
||||
@@ -79,7 +79,7 @@ export default function ChangelogPage() {
|
||||
})}
|
||||
{olderPages.length > 0 && (
|
||||
<div id="more-updates" className="mb-24 scroll-mt-24">
|
||||
<h2 className="mb-6 font-heading text-xl font-semibold tracking-tight">
|
||||
<h2 className="font-heading mb-6 text-xl font-semibold tracking-tight">
|
||||
More Updates
|
||||
</h2>
|
||||
<div className="grid auto-rows-fr gap-3 sm:grid-cols-2">
|
||||
@@ -91,9 +91,9 @@ export default function ChangelogPage() {
|
||||
<Link
|
||||
key={page.url}
|
||||
href={page.url}
|
||||
className="flex w-full flex-col rounded-xl bg-surface px-4 py-3 text-surface-foreground transition-colors hover:bg-surface/80"
|
||||
className="bg-surface text-surface-foreground hover:bg-surface/80 flex w-full flex-col rounded-xl px-4 py-3 transition-colors"
|
||||
>
|
||||
<span className="text-xs text-muted-foreground">
|
||||
<span className="text-muted-foreground text-xs">
|
||||
{date}
|
||||
</span>
|
||||
<span className="text-sm font-medium">{title}</span>
|
||||
@@ -110,7 +110,7 @@ export default function ChangelogPage() {
|
||||
<div className="h-(--top-spacing) shrink-0"></div>
|
||||
<div className="no-scrollbar flex flex-col gap-8 overflow-y-auto px-8">
|
||||
<div className="flex flex-col gap-2 p-4 pt-0 text-sm">
|
||||
<p className="sticky top-0 h-6 bg-background text-xs font-medium text-muted-foreground">
|
||||
<p className="text-muted-foreground bg-background sticky top-0 h-6 text-xs font-medium">
|
||||
On This Page
|
||||
</p>
|
||||
{latestPages.map((page) => {
|
||||
@@ -119,7 +119,7 @@ export default function ChangelogPage() {
|
||||
<Link
|
||||
key={page.url}
|
||||
href={page.url}
|
||||
className="text-[0.8rem] text-muted-foreground no-underline transition-colors hover:text-foreground"
|
||||
className="text-muted-foreground hover:text-foreground text-[0.8rem] no-underline transition-colors"
|
||||
>
|
||||
{data.title}
|
||||
</Link>
|
||||
@@ -128,7 +128,7 @@ export default function ChangelogPage() {
|
||||
{olderPages.length > 0 && (
|
||||
<a
|
||||
href="#more-updates"
|
||||
className="text-[0.8rem] text-muted-foreground no-underline transition-colors hover:text-foreground"
|
||||
className="text-muted-foreground hover:text-foreground text-[0.8rem] no-underline transition-colors"
|
||||
>
|
||||
More Updates
|
||||
</a>
|
||||
|
||||
@@ -10,7 +10,7 @@ export default function DocsLayout({
|
||||
return (
|
||||
<div className="container-wrapper flex flex-1 flex-col px-2">
|
||||
<SidebarProvider
|
||||
className="min-h-min flex-1 items-start px-0 [--top-spacing:0] lg:grid lg:grid-cols-[var(--sidebar-width)_minmax(0,1fr)] lg:[--top-spacing:calc(var(--spacing)*4)] 3xl:fixed:container 3xl:fixed:px-3"
|
||||
className="3xl:fixed:container 3xl:fixed:px-3 min-h-min flex-1 items-start px-0 [--top-spacing:0] lg:grid lg:grid-cols-[var(--sidebar-width)_minmax(0,1fr)] lg:[--top-spacing:calc(var(--spacing)*4)]"
|
||||
style={
|
||||
{
|
||||
"--sidebar-width": "calc(var(--spacing) * 72)",
|
||||
|
||||
@@ -43,8 +43,8 @@ export default function AuthenticationPage() {
|
||||
>
|
||||
Login
|
||||
</Link>
|
||||
<div className="relative hidden h-full flex-col p-10 text-primary lg:flex dark:border-r">
|
||||
<div className="absolute inset-0 bg-primary/5" />
|
||||
<div className="text-primary relative hidden h-full flex-col p-10 lg:flex dark:border-r">
|
||||
<div className="bg-primary/5 absolute inset-0" />
|
||||
<div className="relative z-20 flex items-center text-lg font-medium">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
@@ -74,7 +74,7 @@ export default function AuthenticationPage() {
|
||||
<h1 className="text-2xl font-semibold tracking-tight">
|
||||
Create an account
|
||||
</h1>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
<p className="text-muted-foreground text-sm">
|
||||
Enter your email below to create your account
|
||||
</p>
|
||||
</div>
|
||||
|
||||
@@ -159,10 +159,10 @@ export function AppSidebar({ ...props }: React.ComponentProps<typeof Sidebar>) {
|
||||
<SidebarMenuItem>
|
||||
<SidebarMenuButton
|
||||
asChild
|
||||
className="data-[slot=sidebar-menu-button]:p-1.5!"
|
||||
className="data-[slot=sidebar-menu-button]:!p-1.5"
|
||||
>
|
||||
<Link href="#">
|
||||
<IconInnerShadowTop className="size-5!" />
|
||||
<IconInnerShadowTop className="!size-5" />
|
||||
<span className="text-base font-semibold">Acme Inc.</span>
|
||||
</Link>
|
||||
</SidebarMenuButton>
|
||||
|
||||
@@ -174,7 +174,7 @@ export function ChartAreaInteractive() {
|
||||
value={timeRange}
|
||||
onValueChange={setTimeRange}
|
||||
variant="outline"
|
||||
className="hidden *:data-[slot=toggle-group-item]:px-4! @[767px]/card:flex"
|
||||
className="hidden *:data-[slot=toggle-group-item]:!px-4 @[767px]/card:flex"
|
||||
>
|
||||
<ToggleGroupItem value="90d">Last 3 months</ToggleGroupItem>
|
||||
<ToggleGroupItem value="30d">Last 30 days</ToggleGroupItem>
|
||||
|
||||
@@ -128,9 +128,9 @@ function DragHandle({ id }: { id: number }) {
|
||||
{...listeners}
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="size-7 text-muted-foreground hover:bg-transparent"
|
||||
className="text-muted-foreground size-7 hover:bg-transparent"
|
||||
>
|
||||
<IconGripVertical className="size-3 text-muted-foreground" />
|
||||
<IconGripVertical className="text-muted-foreground size-3" />
|
||||
<span className="sr-only">Drag to reorder</span>
|
||||
</Button>
|
||||
)
|
||||
@@ -181,7 +181,7 @@ const columns: ColumnDef<z.infer<typeof schema>>[] = [
|
||||
header: "Section Type",
|
||||
cell: ({ row }) => (
|
||||
<div className="w-32">
|
||||
<Badge variant="outline" className="px-1.5 text-muted-foreground">
|
||||
<Badge variant="outline" className="text-muted-foreground px-1.5">
|
||||
{row.original.type}
|
||||
</Badge>
|
||||
</div>
|
||||
@@ -191,7 +191,7 @@ const columns: ColumnDef<z.infer<typeof schema>>[] = [
|
||||
accessorKey: "status",
|
||||
header: "Status",
|
||||
cell: ({ row }) => (
|
||||
<Badge variant="outline" className="px-1.5 text-muted-foreground">
|
||||
<Badge variant="outline" className="text-muted-foreground px-1.5">
|
||||
{row.original.status === "Done" ? (
|
||||
<IconCircleCheckFilled className="fill-green-500 dark:fill-green-400" />
|
||||
) : (
|
||||
@@ -219,7 +219,7 @@ const columns: ColumnDef<z.infer<typeof schema>>[] = [
|
||||
Target
|
||||
</Label>
|
||||
<Input
|
||||
className="h-8 w-16 border-transparent bg-transparent text-right shadow-none hover:bg-input/30 focus-visible:border focus-visible:bg-background dark:bg-transparent dark:hover:bg-input/30 dark:focus-visible:bg-input/30"
|
||||
className="hover:bg-input/30 focus-visible:bg-background dark:hover:bg-input/30 dark:focus-visible:bg-input/30 h-8 w-16 border-transparent bg-transparent text-right shadow-none focus-visible:border dark:bg-transparent"
|
||||
defaultValue={row.original.target}
|
||||
id={`${row.original.id}-target`}
|
||||
/>
|
||||
@@ -244,7 +244,7 @@ const columns: ColumnDef<z.infer<typeof schema>>[] = [
|
||||
Limit
|
||||
</Label>
|
||||
<Input
|
||||
className="h-8 w-16 border-transparent bg-transparent text-right shadow-none hover:bg-input/30 focus-visible:border focus-visible:bg-background dark:bg-transparent dark:hover:bg-input/30 dark:focus-visible:bg-input/30"
|
||||
className="hover:bg-input/30 focus-visible:bg-background dark:hover:bg-input/30 dark:focus-visible:bg-input/30 h-8 w-16 border-transparent bg-transparent text-right shadow-none focus-visible:border dark:bg-transparent"
|
||||
defaultValue={row.original.limit}
|
||||
id={`${row.original.id}-limit`}
|
||||
/>
|
||||
@@ -292,7 +292,7 @@ const columns: ColumnDef<z.infer<typeof schema>>[] = [
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button
|
||||
variant="ghost"
|
||||
className="flex size-8 text-muted-foreground data-[state=open]:bg-muted"
|
||||
className="data-[state=open]:bg-muted text-muted-foreground flex size-8"
|
||||
size="icon"
|
||||
>
|
||||
<IconDotsVertical />
|
||||
@@ -425,7 +425,7 @@ export function DataTable({
|
||||
<SelectItem value="focus-documents">Focus Documents</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<TabsList className="hidden **:data-[slot=badge]:size-5 **:data-[slot=badge]:rounded-full **:data-[slot=badge]:bg-muted-foreground/30 **:data-[slot=badge]:px-1 @4xl/main:flex">
|
||||
<TabsList className="**:data-[slot=badge]:bg-muted-foreground/30 hidden **:data-[slot=badge]:size-5 **:data-[slot=badge]:rounded-full **:data-[slot=badge]:px-1 @4xl/main:flex">
|
||||
<TabsTrigger value="outline">Outline</TabsTrigger>
|
||||
<TabsTrigger value="past-performance">
|
||||
Past Performance <Badge variant="secondary">3</Badge>
|
||||
@@ -488,7 +488,7 @@ export function DataTable({
|
||||
id={sortableId}
|
||||
>
|
||||
<Table>
|
||||
<TableHeader className="sticky top-0 z-10 bg-muted">
|
||||
<TableHeader className="bg-muted sticky top-0 z-10">
|
||||
{table.getHeaderGroups().map((headerGroup) => (
|
||||
<TableRow key={headerGroup.id}>
|
||||
{headerGroup.headers.map((header) => {
|
||||
@@ -531,7 +531,7 @@ export function DataTable({
|
||||
</DndContext>
|
||||
</div>
|
||||
<div className="flex items-center justify-between px-4">
|
||||
<div className="hidden flex-1 text-sm text-muted-foreground lg:flex">
|
||||
<div className="text-muted-foreground hidden flex-1 text-sm lg:flex">
|
||||
{table.getFilteredSelectedRowModel().rows.length} of{" "}
|
||||
{table.getFilteredRowModel().rows.length} row(s) selected.
|
||||
</div>
|
||||
@@ -653,7 +653,7 @@ function TableCellViewer({ item }: { item: z.infer<typeof schema> }) {
|
||||
return (
|
||||
<Drawer direction={isMobile ? "bottom" : "right"}>
|
||||
<DrawerTrigger asChild>
|
||||
<Button variant="link" className="w-fit px-0 text-left text-foreground">
|
||||
<Button variant="link" className="text-foreground w-fit px-0 text-left">
|
||||
{item.header}
|
||||
</Button>
|
||||
</DrawerTrigger>
|
||||
|
||||
@@ -52,7 +52,7 @@ export function NavDocuments({
|
||||
<DropdownMenuTrigger asChild>
|
||||
<SidebarMenuAction
|
||||
showOnHover
|
||||
className="rounded-sm data-[state=open]:bg-accent"
|
||||
className="data-[state=open]:bg-accent rounded-sm"
|
||||
>
|
||||
<IconDots />
|
||||
<span className="sr-only">More</span>
|
||||
|
||||
@@ -55,7 +55,7 @@ export function NavUser({
|
||||
</Avatar>
|
||||
<div className="grid flex-1 text-left text-sm leading-tight">
|
||||
<span className="truncate font-medium">{user.name}</span>
|
||||
<span className="truncate text-xs text-muted-foreground">
|
||||
<span className="text-muted-foreground truncate text-xs">
|
||||
{user.email}
|
||||
</span>
|
||||
</div>
|
||||
@@ -76,7 +76,7 @@ export function NavUser({
|
||||
</Avatar>
|
||||
<div className="grid flex-1 text-left text-sm leading-tight">
|
||||
<span className="truncate font-medium">{user.name}</span>
|
||||
<span className="truncate text-xs text-muted-foreground">
|
||||
<span className="text-muted-foreground truncate text-xs">
|
||||
{user.email}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
@@ -4,7 +4,7 @@ import { Button } from "@/registry/new-york-v4/ui/button"
|
||||
|
||||
export function SiteHeader() {
|
||||
return (
|
||||
<header className="sticky top-0 z-10 flex h-(--header-height) shrink-0 items-center gap-2 border-b bg-background/90 transition-[width,height] ease-linear group-has-data-[collapsible=icon]/sidebar-wrapper:h-(--header-height)">
|
||||
<header className="bg-background/90 sticky top-0 z-10 flex h-(--header-height) shrink-0 items-center gap-2 border-b transition-[width,height] ease-linear group-has-data-[collapsible=icon]/sidebar-wrapper:h-(--header-height)">
|
||||
<div className="flex w-full items-center gap-1 px-4 lg:gap-2 lg:px-6">
|
||||
<h1 className="text-base font-medium">Documents</h1>
|
||||
<div className="ml-auto flex items-center gap-2">
|
||||
|
||||
@@ -65,12 +65,12 @@ export default function ExamplesLayout({
|
||||
</PageActions>
|
||||
</PageHeader>
|
||||
<PageNav id="examples" className="hidden md:flex">
|
||||
<ExamplesNav className="flex-1 overflow-hidden [&>a:first-child]:text-primary" />
|
||||
<ExamplesNav className="[&>a:first-child]:text-primary flex-1 overflow-hidden" />
|
||||
<ThemeSelector className="mr-4 hidden md:flex" />
|
||||
</PageNav>
|
||||
<div className="container-wrapper flex flex-1 flex-col section-soft pb-6">
|
||||
<div className="container flex flex-1 scroll-mt-20 flex-col theme-container">
|
||||
<div className="flex flex-col overflow-hidden rounded-lg border bg-background bg-clip-padding has-[[data-slot=rtl-components]]:overflow-visible has-[[data-slot=rtl-components]]:border-0 has-[[data-slot=rtl-components]]:bg-transparent md:flex-1 xl:rounded-xl">
|
||||
<div className="container-wrapper section-soft flex flex-1 flex-col pb-6">
|
||||
<div className="theme-container container flex flex-1 scroll-mt-20 flex-col">
|
||||
<div className="bg-background flex flex-col overflow-hidden rounded-lg border bg-clip-padding has-[[data-slot=rtl-components]]:overflow-visible has-[[data-slot=rtl-components]]:border-0 has-[[data-slot=rtl-components]]:bg-transparent md:flex-1 xl:rounded-xl">
|
||||
{children}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -76,7 +76,7 @@ export function CodeViewer() {
|
||||
</pre>
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
<p className="text-muted-foreground text-sm">
|
||||
Your API Key can be found here. You should use environment
|
||||
variables or a secret management tool to expose your key to your
|
||||
applications.
|
||||
|
||||
@@ -27,7 +27,7 @@ export function MaxLengthSelector({ defaultValue }: MaxLengthSelectorProps) {
|
||||
<div className="grid gap-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<Label htmlFor="maxlength">Maximum Length</Label>
|
||||
<span className="w-12 rounded-md border border-transparent px-2 py-0.5 text-right text-sm text-muted-foreground hover:border-border">
|
||||
<span className="text-muted-foreground hover:border-border w-12 rounded-md border border-transparent px-2 py-0.5 text-right text-sm">
|
||||
{value}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
@@ -78,7 +78,7 @@ export function ModelSelector({ models, types, ...props }: ModelSelectorProps) {
|
||||
>
|
||||
<div className="grid gap-2">
|
||||
<h4 className="leading-none font-medium">{peekedModel.name}</h4>
|
||||
<div className="text-sm text-muted-foreground">
|
||||
<div className="text-muted-foreground text-sm">
|
||||
{peekedModel.description}
|
||||
</div>
|
||||
{peekedModel.strengths ? (
|
||||
@@ -86,7 +86,7 @@ export function ModelSelector({ models, types, ...props }: ModelSelectorProps) {
|
||||
<h5 className="text-sm leading-none font-medium">
|
||||
Strengths
|
||||
</h5>
|
||||
<ul className="text-sm text-muted-foreground">
|
||||
<ul className="text-muted-foreground text-sm">
|
||||
{peekedModel.strengths}
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
@@ -70,7 +70,7 @@ export function PresetActions() {
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
<div className="py-6">
|
||||
<h4 className="text-sm text-muted-foreground">
|
||||
<h4 className="text-muted-foreground text-sm">
|
||||
Playground Warnings
|
||||
</h4>
|
||||
<div className="flex items-start justify-between gap-4 pt-3">
|
||||
@@ -79,7 +79,7 @@ export function PresetActions() {
|
||||
<span className="font-semibold">
|
||||
Show a warning when content is flagged
|
||||
</span>
|
||||
<span className="text-sm text-muted-foreground">
|
||||
<span className="text-muted-foreground text-sm">
|
||||
A warning will be shown when sexual, hateful, violent or
|
||||
self-harm content is detected.
|
||||
</span>
|
||||
|
||||
@@ -18,7 +18,7 @@ export function PresetShare() {
|
||||
<PopoverContent align="end" className="flex w-[520px] flex-col gap-4">
|
||||
<div className="flex flex-col gap-1 text-center sm:text-left">
|
||||
<h3 className="text-lg font-semibold">Share preset</h3>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
<p className="text-muted-foreground text-sm">
|
||||
Anyone who has this link and an OpenAI account will be able to view
|
||||
this.
|
||||
</p>
|
||||
|
||||
@@ -29,7 +29,7 @@ export function TemperatureSelector({
|
||||
<div className="grid gap-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<Label htmlFor="temperature">Temperature</Label>
|
||||
<span className="w-12 rounded-md border border-transparent px-2 py-0.5 text-right text-sm text-muted-foreground hover:border-border">
|
||||
<span className="text-muted-foreground hover:border-border w-12 rounded-md border border-transparent px-2 py-0.5 text-right text-sm">
|
||||
{value}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
@@ -27,7 +27,7 @@ export function TopPSelector({ defaultValue }: TopPSelectorProps) {
|
||||
<div className="grid gap-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<Label htmlFor="top-p">Top P</Label>
|
||||
<span className="w-12 rounded-md border border-transparent px-2 py-0.5 text-right text-sm text-muted-foreground hover:border-border">
|
||||
<span className="text-muted-foreground hover:border-border w-12 rounded-md border border-transparent px-2 py-0.5 text-right text-sm">
|
||||
{value}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
@@ -278,7 +278,7 @@ export default function PlaygroundPage() {
|
||||
placeholder="We're writing to [inset]. Congrats from OpenAI!"
|
||||
className="h-full min-h-[300px] p-4 lg:min-h-[700px] xl:min-h-[700px]"
|
||||
/>
|
||||
<div className="rounded-md border bg-muted"></div>
|
||||
<div className="bg-muted rounded-md border"></div>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<Button>Submit</Button>
|
||||
@@ -312,7 +312,7 @@ export default function PlaygroundPage() {
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="min-h-[400px] rounded-md border bg-muted lg:min-h-[700px]" />
|
||||
<div className="bg-muted min-h-[400px] rounded-md border lg:min-h-[700px]" />
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<Button>Submit</Button>
|
||||
|
||||
@@ -128,7 +128,7 @@ export function AppearanceSettings() {
|
||||
value={gpuCount}
|
||||
onChange={handleGpuInputChange}
|
||||
size={3}
|
||||
className="h-7 w-14! font-mono"
|
||||
className="h-7 !w-14 font-mono"
|
||||
maxLength={3}
|
||||
/>
|
||||
<Button
|
||||
|
||||
@@ -63,7 +63,7 @@ export function FieldHear() {
|
||||
<FieldLabel
|
||||
htmlFor={`rtl-${option.value}`}
|
||||
key={option.value}
|
||||
className="w-fit!"
|
||||
className="!w-fit"
|
||||
>
|
||||
<Field
|
||||
orientation="horizontal"
|
||||
|
||||
@@ -50,7 +50,7 @@ export function InputGroupButtonExample() {
|
||||
{t.inputLabel}
|
||||
</Label>
|
||||
<InputGroup className="[--radius:9999px]">
|
||||
<InputGroupInput id="input-secure-rtl" className="pr-0.5!" />
|
||||
<InputGroupInput id="input-secure-rtl" className="!pr-0.5" />
|
||||
<InputGroupAddon>
|
||||
<Popover>
|
||||
<PopoverTrigger
|
||||
|
||||
@@ -116,7 +116,7 @@ export function InputGroupDemo() {
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
<InputGroupText className="ms-auto">{t.used}</InputGroupText>
|
||||
<Separator orientation="vertical" className="h-4!" />
|
||||
<Separator orientation="vertical" className="!h-4" />
|
||||
<InputGroupButton
|
||||
variant="default"
|
||||
className="rounded-full"
|
||||
@@ -130,7 +130,7 @@ export function InputGroupDemo() {
|
||||
<InputGroup>
|
||||
<InputGroupInput placeholder="shadcn" />
|
||||
<InputGroupAddon align="inline-end">
|
||||
<div className="flex size-4 items-center justify-center rounded-full bg-primary text-foreground">
|
||||
<div className="bg-primary text-foreground flex size-4 items-center justify-center rounded-full">
|
||||
<IconCheck className="size-3 text-white" />
|
||||
</div>
|
||||
</InputGroupAddon>
|
||||
|
||||
@@ -288,7 +288,7 @@ export function NotionPromptForm() {
|
||||
</Command>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
<div className="-m-1.5 no-scrollbar flex gap-1 overflow-y-auto p-1.5">
|
||||
<div className="no-scrollbar -m-1.5 flex gap-1 overflow-y-auto p-1.5">
|
||||
{mentions.map((mention) => {
|
||||
const item = SAMPLE_DATA.mentionable.find(
|
||||
(item) => item.title === mention
|
||||
@@ -303,7 +303,7 @@ export function NotionPromptForm() {
|
||||
key={mention}
|
||||
size="sm"
|
||||
variant="secondary"
|
||||
className="rounded-full pr-2!"
|
||||
className="rounded-full !pr-2"
|
||||
onClick={() => {
|
||||
setMentions((prev) => prev.filter((m) => m !== mention))
|
||||
}}
|
||||
@@ -352,7 +352,7 @@ export function NotionPromptForm() {
|
||||
dir={t.dir}
|
||||
>
|
||||
<DropdownMenuGroup className="w-48">
|
||||
<DropdownMenuLabel className="text-xs text-muted-foreground">
|
||||
<DropdownMenuLabel className="text-muted-foreground text-xs">
|
||||
{t.selectAgentMode}
|
||||
</DropdownMenuLabel>
|
||||
{SAMPLE_DATA.models.map((model) => (
|
||||
@@ -493,7 +493,7 @@ export function NotionPromptForm() {
|
||||
<DropdownMenuItem>
|
||||
<IconPlus /> {t.connectApps}
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuLabel className="text-xs text-muted-foreground">
|
||||
<DropdownMenuLabel className="text-muted-foreground text-xs">
|
||||
{t.searchSourcesNote}
|
||||
</DropdownMenuLabel>
|
||||
</DropdownMenuGroup>
|
||||
|
||||
@@ -79,7 +79,7 @@ export const columns: ColumnDef<Task>[] = [
|
||||
return (
|
||||
<div className="flex w-[100px] items-center gap-2">
|
||||
{status.icon && (
|
||||
<status.icon className="size-4 text-muted-foreground" />
|
||||
<status.icon className="text-muted-foreground size-4" />
|
||||
)}
|
||||
<span>{status.label}</span>
|
||||
</div>
|
||||
@@ -106,7 +106,7 @@ export const columns: ColumnDef<Task>[] = [
|
||||
return (
|
||||
<div className="flex items-center gap-2">
|
||||
{priority.icon && (
|
||||
<priority.icon className="size-4 text-muted-foreground" />
|
||||
<priority.icon className="text-muted-foreground size-4" />
|
||||
)}
|
||||
<span>{priority.label}</span>
|
||||
</div>
|
||||
|
||||
@@ -33,7 +33,7 @@ export function DataTableColumnHeader<TData, TValue>({
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
className="-ml-3 h-8 data-[state=open]:bg-accent"
|
||||
className="data-[state=open]:bg-accent -ml-3 h-8"
|
||||
>
|
||||
<span>{title}</span>
|
||||
{column.getIsSorted() === "desc" ? (
|
||||
|
||||
@@ -107,18 +107,18 @@ export function DataTableFacetedFilter<TData, TValue>({
|
||||
className={cn(
|
||||
"flex size-4 items-center justify-center rounded-[4px] border",
|
||||
isSelected
|
||||
? "border-primary bg-primary text-primary-foreground"
|
||||
? "bg-primary border-primary text-primary-foreground"
|
||||
: "border-input [&_svg]:invisible"
|
||||
)}
|
||||
>
|
||||
<Check className="size-3.5 text-primary-foreground" />
|
||||
<Check className="text-primary-foreground size-3.5" />
|
||||
</div>
|
||||
{option.icon && (
|
||||
<option.icon className="size-4 text-muted-foreground" />
|
||||
<option.icon className="text-muted-foreground size-4" />
|
||||
)}
|
||||
<span>{option.label}</span>
|
||||
{facets?.get(option.value) && (
|
||||
<span className="ml-auto flex size-4 items-center justify-center font-mono text-xs text-muted-foreground">
|
||||
<span className="text-muted-foreground ml-auto flex size-4 items-center justify-center font-mono text-xs">
|
||||
{facets.get(option.value)}
|
||||
</span>
|
||||
)}
|
||||
|
||||
@@ -24,7 +24,7 @@ export function DataTablePagination<TData>({
|
||||
}: DataTablePaginationProps<TData>) {
|
||||
return (
|
||||
<div className="flex items-center justify-between px-2">
|
||||
<div className="flex-1 text-sm text-muted-foreground">
|
||||
<div className="text-muted-foreground flex-1 text-sm">
|
||||
{table.getFilteredSelectedRowModel().rows.length} of{" "}
|
||||
{table.getFilteredRowModel().rows.length} row(s) selected.
|
||||
</div>
|
||||
|
||||
@@ -36,7 +36,7 @@ export function DataTableRowActions<TData>({
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="size-8 data-[state=open]:bg-muted"
|
||||
className="data-[state=open]:bg-muted size-8"
|
||||
>
|
||||
<MoreHorizontal />
|
||||
<span className="sr-only">Open menu</span>
|
||||
|
||||
@@ -30,7 +30,7 @@ export function UserNav() {
|
||||
<DropdownMenuLabel className="font-normal">
|
||||
<div className="flex flex-col space-y-1">
|
||||
<p className="text-sm leading-none font-medium">shadcn</p>
|
||||
<p className="text-xs leading-none text-muted-foreground">
|
||||
<p className="text-muted-foreground text-xs leading-none">
|
||||
m@example.com
|
||||
</p>
|
||||
</div>
|
||||
|
||||
@@ -5,7 +5,7 @@ export default function AppLayout({ children }: { children: React.ReactNode }) {
|
||||
return (
|
||||
<div
|
||||
data-slot="layout"
|
||||
className="relative z-10 flex min-h-svh flex-col bg-background"
|
||||
className="bg-background relative z-10 flex min-h-svh flex-col"
|
||||
>
|
||||
<SiteHeader />
|
||||
<main className="flex flex-1 flex-col">{children}</main>
|
||||
|
||||
@@ -12,8 +12,8 @@ export default function ThemesPage() {
|
||||
<ThemeCustomizer />
|
||||
</div>
|
||||
</div>
|
||||
<div className="container-wrapper flex flex-1 flex-col section-soft pb-6">
|
||||
<div className="container flex flex-1 flex-col theme-container">
|
||||
<div className="container-wrapper section-soft flex flex-1 flex-col pb-6">
|
||||
<div className="theme-container container flex flex-1 flex-col">
|
||||
<CardsDemo />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -26,23 +26,23 @@ export function MenuAccentPicker({
|
||||
)
|
||||
|
||||
return (
|
||||
<div className="group/picker relative pr-3 md:pr-0">
|
||||
<div className="group/picker relative">
|
||||
<Picker>
|
||||
<PickerTrigger>
|
||||
<div className="flex flex-col justify-start text-left">
|
||||
<div className="text-xs text-muted-foreground">Menu Accent</div>
|
||||
<div className="text-sm font-medium text-foreground">
|
||||
<div className="text-muted-foreground text-xs">Menu Accent</div>
|
||||
<div className="text-foreground text-sm font-medium">
|
||||
{currentAccent?.label}
|
||||
</div>
|
||||
</div>
|
||||
<div className="pointer-events-none absolute top-1/2 right-4 flex size-4 -translate-y-1/2 items-center justify-center text-base text-foreground select-none md:right-2.5">
|
||||
<div className="text-foreground pointer-events-none absolute top-1/2 right-4 flex size-4 -translate-y-1/2 items-center justify-center text-base select-none">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="128"
|
||||
height="128"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
className="size-4 text-foreground"
|
||||
className="text-foreground"
|
||||
>
|
||||
<path
|
||||
d="M19 12.1294L12.9388 18.207C11.1557 19.9949 10.2641 20.8889 9.16993 20.9877C8.98904 21.0041 8.80705 21.0041 8.62616 20.9877C7.53195 20.8889 6.64039 19.9949 4.85726 18.207L2.83687 16.1811C1.72104 15.0622 1.72104 13.2482 2.83687 12.1294M19 12.1294L10.9184 4.02587M19 12.1294H2.83687M10.9184 4.02587L2.83687 12.1294M10.9184 4.02587L8.89805 2"
|
||||
@@ -51,7 +51,7 @@ export function MenuAccentPicker({
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
data-accent={currentAccent?.value}
|
||||
className="fill-muted-foreground/30 data-[accent=bold]:fill-foreground"
|
||||
className="data-[accent=bold]:fill-foreground fill-muted-foreground/30"
|
||||
></path>
|
||||
<path
|
||||
d="M22 20C22 21.1046 21.1046 22 20 22C18.8954 22 18 21.1046 18 20C18 18.8954 20 17 20 17C20 17 22 18.8954 22 20Z"
|
||||
@@ -60,7 +60,7 @@ export function MenuAccentPicker({
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
data-accent={currentAccent?.value}
|
||||
className="fill-muted-foreground/30 data-[accent=bold]:fill-foreground"
|
||||
className="data-[accent=bold]:fill-foreground fill-muted-foreground/30"
|
||||
></path>
|
||||
</svg>
|
||||
</div>
|
||||
@@ -78,16 +78,7 @@ export function MenuAccentPicker({
|
||||
>
|
||||
<PickerGroup>
|
||||
{MENU_ACCENTS.map((accent) => (
|
||||
<PickerRadioItem
|
||||
key={accent.value}
|
||||
value={accent.value}
|
||||
closeOnClick={isMobile}
|
||||
disabled={
|
||||
accent.value === "bold" &&
|
||||
(params.menuColor === "default-translucent" ||
|
||||
params.menuColor === "inverted-translucent")
|
||||
}
|
||||
>
|
||||
<PickerRadioItem key={accent.value} value={accent.value}>
|
||||
{accent.label}
|
||||
</PickerRadioItem>
|
||||
))}
|
||||
@@ -97,7 +88,7 @@ export function MenuAccentPicker({
|
||||
</Picker>
|
||||
<LockButton
|
||||
param="menuAccent"
|
||||
className="absolute top-1/2 right-8 -translate-y-1/2"
|
||||
className="absolute top-1/2 right-10 -translate-y-1/2"
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
|
||||
@@ -1,88 +0,0 @@
|
||||
"use client"
|
||||
|
||||
import Script from "next/script"
|
||||
import {
|
||||
Command,
|
||||
CommandDialog,
|
||||
CommandEmpty,
|
||||
CommandGroup,
|
||||
CommandInput,
|
||||
CommandItem,
|
||||
CommandList,
|
||||
} from "@/examples/base/ui/command"
|
||||
import { type RegistryItem } from "shadcn/schema"
|
||||
|
||||
import { useActionMenu } from "@/app/(create)/hooks/use-action-menu"
|
||||
|
||||
export const CMD_K_FORWARD_TYPE = "cmd-k-forward"
|
||||
|
||||
export function ActionMenu({
|
||||
itemsByBase,
|
||||
}: {
|
||||
itemsByBase: Record<string, Pick<RegistryItem, "name" | "title" | "type">[]>
|
||||
}) {
|
||||
const {
|
||||
activeRegistryName,
|
||||
getCommandValue,
|
||||
groups,
|
||||
handleSelect,
|
||||
open,
|
||||
setOpen,
|
||||
} = useActionMenu(itemsByBase)
|
||||
|
||||
return (
|
||||
<CommandDialog open={open} onOpenChange={setOpen} className="animate-none!">
|
||||
<Command loop>
|
||||
<CommandInput placeholder="Search" />
|
||||
<CommandList>
|
||||
<CommandEmpty>No items found.</CommandEmpty>
|
||||
<CommandGroup>
|
||||
{groups.map((group) =>
|
||||
group.items.map((item) => (
|
||||
<CommandItem
|
||||
key={item.id}
|
||||
value={getCommandValue(item)}
|
||||
data-checked={activeRegistryName === item.registryName}
|
||||
className="px-2"
|
||||
onSelect={() => {
|
||||
handleSelect(item.registryName)
|
||||
}}
|
||||
>
|
||||
{item.label}
|
||||
</CommandItem>
|
||||
))
|
||||
)}
|
||||
</CommandGroup>
|
||||
</CommandList>
|
||||
</Command>
|
||||
</CommandDialog>
|
||||
)
|
||||
}
|
||||
|
||||
export function ActionMenuScript() {
|
||||
return (
|
||||
<Script
|
||||
id="design-system-listener"
|
||||
strategy="beforeInteractive"
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: `
|
||||
(function() {
|
||||
// Forward Cmd/Ctrl + K (and P) to parent
|
||||
document.addEventListener('keydown', function(e) {
|
||||
if ((e.key === 'k' || e.key === 'p') && (e.metaKey || e.ctrlKey)) {
|
||||
e.preventDefault();
|
||||
if (window.parent && window.parent !== window) {
|
||||
window.parent.postMessage({
|
||||
type: '${CMD_K_FORWARD_TYPE}',
|
||||
key: e.key
|
||||
}, '*');
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
})();
|
||||
`,
|
||||
}}
|
||||
/>
|
||||
)
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
"use client"
|
||||
|
||||
import * as React from "react"
|
||||
import { useTheme } from "next-themes"
|
||||
|
||||
import { useMounted } from "@/hooks/use-mounted"
|
||||
import { BASE_COLORS, type BaseColorName } from "@/registry/config"
|
||||
@@ -9,8 +10,10 @@ import {
|
||||
Picker,
|
||||
PickerContent,
|
||||
PickerGroup,
|
||||
PickerItem,
|
||||
PickerRadioGroup,
|
||||
PickerRadioItem,
|
||||
PickerSeparator,
|
||||
PickerTrigger,
|
||||
} from "@/app/(create)/components/picker"
|
||||
import { useDesignSystemSearchParams } from "@/app/(create)/lib/search-params"
|
||||
@@ -22,6 +25,7 @@ export function BaseColorPicker({
|
||||
isMobile: boolean
|
||||
anchorRef: React.RefObject<HTMLDivElement | null>
|
||||
}) {
|
||||
const { resolvedTheme, setTheme } = useTheme()
|
||||
const mounted = useMounted()
|
||||
const [params, setParams] = useDesignSystemSearchParams()
|
||||
|
||||
@@ -35,20 +39,22 @@ export function BaseColorPicker({
|
||||
<Picker>
|
||||
<PickerTrigger>
|
||||
<div className="flex flex-col justify-start text-left">
|
||||
<div className="text-xs text-muted-foreground">Base Color</div>
|
||||
<div className="text-sm font-medium text-foreground">
|
||||
<div className="text-muted-foreground text-xs">Base Color</div>
|
||||
<div className="text-foreground text-sm font-medium">
|
||||
{currentBaseColor?.title}
|
||||
</div>
|
||||
</div>
|
||||
{mounted && (
|
||||
{mounted && resolvedTheme && (
|
||||
<div
|
||||
style={
|
||||
{
|
||||
"--color":
|
||||
currentBaseColor?.cssVars?.dark?.["muted-foreground"],
|
||||
currentBaseColor?.cssVars?.[
|
||||
resolvedTheme as "light" | "dark"
|
||||
]?.["muted-foreground"],
|
||||
} as React.CSSProperties
|
||||
}
|
||||
className="pointer-events-none absolute top-1/2 right-4 size-4 -translate-y-1/2 rounded-full bg-(--color) select-none md:right-2.5"
|
||||
className="pointer-events-none absolute top-1/2 right-4 size-4 -translate-y-1/2 rounded-full bg-(--color) select-none"
|
||||
/>
|
||||
)}
|
||||
</PickerTrigger>
|
||||
@@ -60,26 +66,59 @@ export function BaseColorPicker({
|
||||
<PickerRadioGroup
|
||||
value={currentBaseColor?.name}
|
||||
onValueChange={(value) => {
|
||||
if (value === "dark") {
|
||||
setTheme(resolvedTheme === "dark" ? "light" : "dark")
|
||||
return
|
||||
}
|
||||
|
||||
setParams({ baseColor: value as BaseColorName })
|
||||
}}
|
||||
>
|
||||
<PickerGroup>
|
||||
{BASE_COLORS.map((baseColor) => (
|
||||
<PickerRadioItem
|
||||
key={baseColor.name}
|
||||
value={baseColor.name}
|
||||
closeOnClick={isMobile}
|
||||
>
|
||||
{baseColor.title}
|
||||
<PickerRadioItem key={baseColor.name} value={baseColor.name}>
|
||||
<div className="flex items-center gap-2">
|
||||
{mounted && resolvedTheme && (
|
||||
<div
|
||||
style={
|
||||
{
|
||||
"--color":
|
||||
baseColor.cssVars?.[
|
||||
resolvedTheme as "light" | "dark"
|
||||
]?.["muted-foreground"],
|
||||
} as React.CSSProperties
|
||||
}
|
||||
className="size-4 rounded-full bg-(--color)"
|
||||
/>
|
||||
)}
|
||||
{baseColor.title}
|
||||
</div>
|
||||
</PickerRadioItem>
|
||||
))}
|
||||
</PickerGroup>
|
||||
<PickerSeparator />
|
||||
<PickerGroup>
|
||||
<PickerItem
|
||||
onClick={() => {
|
||||
setTheme(resolvedTheme === "dark" ? "light" : "dark")
|
||||
}}
|
||||
>
|
||||
<div className="flex flex-col justify-start pointer-coarse:gap-1">
|
||||
<div>
|
||||
Switch to {resolvedTheme === "dark" ? "Light" : "Dark"} Mode
|
||||
</div>
|
||||
<div className="text-muted-foreground text-xs pointer-coarse:text-sm">
|
||||
Base colors are easier to see in dark mode.
|
||||
</div>
|
||||
</div>
|
||||
</PickerItem>
|
||||
</PickerGroup>
|
||||
</PickerRadioGroup>
|
||||
</PickerContent>
|
||||
</Picker>
|
||||
<LockButton
|
||||
param="baseColor"
|
||||
className="absolute top-1/2 right-8 -translate-y-1/2"
|
||||
className="absolute top-1/2 right-10 -translate-y-1/2"
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
|
||||
@@ -40,47 +40,49 @@ export function BasePicker({
|
||||
)
|
||||
|
||||
return (
|
||||
<div className="group/picker relative">
|
||||
<Picker>
|
||||
<PickerTrigger>
|
||||
<div className="flex flex-col justify-start text-left">
|
||||
<div className="text-xs text-muted-foreground">Base</div>
|
||||
<div className="text-sm font-medium text-foreground">
|
||||
{currentBase?.title}
|
||||
</div>
|
||||
<Picker>
|
||||
<PickerTrigger>
|
||||
<div className="flex flex-col justify-start text-left">
|
||||
<div className="text-muted-foreground text-xs">Component Library</div>
|
||||
<div className="text-foreground text-sm font-medium">
|
||||
{currentBase?.title}
|
||||
</div>
|
||||
{currentBase?.meta?.logo && (
|
||||
<div
|
||||
className="pointer-events-none absolute top-1/2 right-4 size-4 -translate-y-1/2 text-foreground select-none md:right-2.5 *:[svg]:size-4 *:[svg]:text-foreground!"
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: currentBase.meta.logo,
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</PickerTrigger>
|
||||
<PickerContent
|
||||
anchor={isMobile ? anchorRef : undefined}
|
||||
side={isMobile ? "top" : "right"}
|
||||
align={isMobile ? "center" : "start"}
|
||||
</div>
|
||||
{currentBase?.meta?.logo && (
|
||||
<div
|
||||
className="text-foreground *:[svg]:text-foreground! pointer-events-none absolute top-1/2 right-4 size-4 -translate-y-1/2 select-none *:[svg]:size-4"
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: currentBase.meta.logo,
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</PickerTrigger>
|
||||
<PickerContent
|
||||
anchor={isMobile ? anchorRef : undefined}
|
||||
side={isMobile ? "top" : "right"}
|
||||
align={isMobile ? "center" : "start"}
|
||||
>
|
||||
<PickerRadioGroup
|
||||
value={currentBase?.name}
|
||||
onValueChange={handleValueChange}
|
||||
>
|
||||
<PickerRadioGroup
|
||||
value={currentBase?.name}
|
||||
onValueChange={handleValueChange}
|
||||
>
|
||||
<PickerGroup>
|
||||
{BASES.map((base) => (
|
||||
<PickerRadioItem
|
||||
key={base.name}
|
||||
value={base.name}
|
||||
closeOnClick={isMobile}
|
||||
>
|
||||
{base.title}
|
||||
</PickerRadioItem>
|
||||
))}
|
||||
</PickerGroup>
|
||||
</PickerRadioGroup>
|
||||
</PickerContent>
|
||||
</Picker>
|
||||
</div>
|
||||
<PickerGroup>
|
||||
{BASES.map((base) => (
|
||||
<PickerRadioItem key={base.name} value={base.name}>
|
||||
{base.meta?.logo && (
|
||||
<div
|
||||
className="text-foreground *:[svg]:text-foreground! size-4 shrink-0 [&_svg]:size-4"
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: base.meta.logo,
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
{base.title}
|
||||
</PickerRadioItem>
|
||||
))}
|
||||
</PickerGroup>
|
||||
</PickerRadioGroup>
|
||||
</PickerContent>
|
||||
</Picker>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,136 +0,0 @@
|
||||
"use client"
|
||||
|
||||
import * as React from "react"
|
||||
|
||||
import { useMounted } from "@/hooks/use-mounted"
|
||||
import {
|
||||
BASE_COLORS,
|
||||
getThemesForBaseColor,
|
||||
type ChartColorName,
|
||||
} from "@/registry/config"
|
||||
import { LockButton } from "@/app/(create)/components/lock-button"
|
||||
import {
|
||||
Picker,
|
||||
PickerContent,
|
||||
PickerGroup,
|
||||
PickerRadioGroup,
|
||||
PickerRadioItem,
|
||||
PickerSeparator,
|
||||
PickerTrigger,
|
||||
} from "@/app/(create)/components/picker"
|
||||
import { useDesignSystemSearchParams } from "@/app/(create)/lib/search-params"
|
||||
|
||||
export function ChartColorPicker({
|
||||
isMobile,
|
||||
anchorRef,
|
||||
}: {
|
||||
isMobile: boolean
|
||||
anchorRef: React.RefObject<HTMLDivElement | null>
|
||||
}) {
|
||||
const mounted = useMounted()
|
||||
const [params, setParams] = useDesignSystemSearchParams()
|
||||
|
||||
const availableChartColors = React.useMemo(
|
||||
() => getThemesForBaseColor(params.baseColor),
|
||||
[params.baseColor]
|
||||
)
|
||||
|
||||
const currentChartColor = React.useMemo(
|
||||
() =>
|
||||
availableChartColors.find((theme) => theme.name === params.chartColor),
|
||||
[availableChartColors, params.chartColor]
|
||||
)
|
||||
|
||||
const currentChartColorIsBaseColor = React.useMemo(
|
||||
() => BASE_COLORS.find((baseColor) => baseColor.name === params.chartColor),
|
||||
[params.chartColor]
|
||||
)
|
||||
|
||||
React.useEffect(() => {
|
||||
if (!currentChartColor && availableChartColors.length > 0) {
|
||||
setParams({ chartColor: availableChartColors[0].name })
|
||||
}
|
||||
}, [currentChartColor, availableChartColors, setParams])
|
||||
|
||||
return (
|
||||
<div className="group/picker relative">
|
||||
<Picker>
|
||||
<PickerTrigger>
|
||||
<div className="flex flex-col justify-start text-left">
|
||||
<div className="text-xs text-muted-foreground">Chart Color</div>
|
||||
<div className="text-sm font-medium text-foreground">
|
||||
{currentChartColor?.title}
|
||||
</div>
|
||||
</div>
|
||||
{mounted && (
|
||||
<div
|
||||
style={
|
||||
{
|
||||
"--color":
|
||||
currentChartColor?.cssVars?.dark?.[
|
||||
currentChartColorIsBaseColor
|
||||
? "muted-foreground"
|
||||
: "primary"
|
||||
],
|
||||
} as React.CSSProperties
|
||||
}
|
||||
className="pointer-events-none absolute top-1/2 right-4 size-4 -translate-y-1/2 rounded-full bg-(--color) select-none md:right-2.5"
|
||||
/>
|
||||
)}
|
||||
</PickerTrigger>
|
||||
<PickerContent
|
||||
anchor={isMobile ? anchorRef : undefined}
|
||||
side={isMobile ? "top" : "right"}
|
||||
align={isMobile ? "center" : "start"}
|
||||
className="max-h-92"
|
||||
>
|
||||
<PickerRadioGroup
|
||||
value={currentChartColor?.name}
|
||||
onValueChange={(value) => {
|
||||
setParams({ chartColor: value as ChartColorName })
|
||||
}}
|
||||
>
|
||||
<PickerGroup>
|
||||
{availableChartColors
|
||||
.filter((theme) =>
|
||||
BASE_COLORS.find((baseColor) => baseColor.name === theme.name)
|
||||
)
|
||||
.map((theme) => (
|
||||
<PickerRadioItem
|
||||
key={theme.name}
|
||||
value={theme.name}
|
||||
closeOnClick={isMobile}
|
||||
>
|
||||
{theme.title}
|
||||
</PickerRadioItem>
|
||||
))}
|
||||
</PickerGroup>
|
||||
<PickerSeparator />
|
||||
<PickerGroup>
|
||||
{availableChartColors
|
||||
.filter(
|
||||
(theme) =>
|
||||
!BASE_COLORS.find(
|
||||
(baseColor) => baseColor.name === theme.name
|
||||
)
|
||||
)
|
||||
.map((theme) => (
|
||||
<PickerRadioItem
|
||||
key={theme.name}
|
||||
value={theme.name}
|
||||
closeOnClick={isMobile}
|
||||
>
|
||||
{theme.title}
|
||||
</PickerRadioItem>
|
||||
))}
|
||||
</PickerGroup>
|
||||
</PickerRadioGroup>
|
||||
</PickerContent>
|
||||
</Picker>
|
||||
<LockButton
|
||||
param="chartColor"
|
||||
className="absolute top-1/2 right-8 -translate-y-1/2"
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -1,43 +0,0 @@
|
||||
"use client"
|
||||
|
||||
import * as React from "react"
|
||||
import { Button } from "@/examples/base/ui/button"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
import { copyToClipboardWithMeta } from "@/components/copy-button"
|
||||
import { usePresetCode } from "@/app/(create)/hooks/use-design-system"
|
||||
|
||||
export function CopyPreset({ className }: React.ComponentProps<typeof Button>) {
|
||||
const presetCode = usePresetCode()
|
||||
const [hasCopied, setHasCopied] = React.useState(false)
|
||||
|
||||
React.useEffect(() => {
|
||||
if (hasCopied) {
|
||||
const timer = setTimeout(() => setHasCopied(false), 2000)
|
||||
return () => clearTimeout(timer)
|
||||
}
|
||||
}, [hasCopied])
|
||||
|
||||
const handleCopy = React.useCallback(() => {
|
||||
copyToClipboardWithMeta(`--preset ${presetCode}`, {
|
||||
name: "copy_preset_command",
|
||||
properties: {
|
||||
preset: presetCode,
|
||||
},
|
||||
})
|
||||
setHasCopied(true)
|
||||
}, [presetCode])
|
||||
|
||||
return (
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={handleCopy}
|
||||
className={cn(
|
||||
"touch-manipulation bg-transparent! px-2! py-0! text-sm! transition-none select-none hover:bg-muted! pointer-coarse:h-10!",
|
||||
className
|
||||
)}
|
||||
>
|
||||
<span>{hasCopied ? "Copied" : `--preset ${presetCode}`}</span>
|
||||
</Button>
|
||||
)
|
||||
}
|
||||
@@ -1,41 +1,29 @@
|
||||
"use client"
|
||||
|
||||
import * as React from "react"
|
||||
import {
|
||||
Card,
|
||||
CardContent,
|
||||
CardFooter,
|
||||
CardHeader,
|
||||
} from "@/examples/base/ui/card"
|
||||
import { FieldGroup, FieldSeparator } from "@/examples/base/ui/field"
|
||||
import { type RegistryItem } from "shadcn/schema"
|
||||
import { Settings05Icon } from "@hugeicons/core-free-icons"
|
||||
import { HugeiconsIcon } from "@hugeicons/react"
|
||||
|
||||
import { useIsMobile } from "@/hooks/use-mobile"
|
||||
import { getThemesForBaseColor, STYLES } from "@/registry/config"
|
||||
import { getThemesForBaseColor, PRESETS, STYLES } from "@/registry/config"
|
||||
import { FieldGroup } from "@/registry/new-york-v4/ui/field"
|
||||
import { Separator } from "@/registry/new-york-v4/ui/separator"
|
||||
import { MenuAccentPicker } from "@/app/(create)/components/accent-picker"
|
||||
import { ActionMenu } from "@/app/(create)/components/action-menu"
|
||||
import { BaseColorPicker } from "@/app/(create)/components/base-color-picker"
|
||||
import { BasePicker } from "@/app/(create)/components/base-picker"
|
||||
import { ChartColorPicker } from "@/app/(create)/components/chart-color-picker"
|
||||
import { CopyPreset } from "@/app/(create)/components/copy-preset"
|
||||
import { FontPicker } from "@/app/(create)/components/font-picker"
|
||||
import { IconLibraryPicker } from "@/app/(create)/components/icon-library-picker"
|
||||
import { MainMenu } from "@/app/(create)/components/main-menu"
|
||||
import { MenuColorPicker } from "@/app/(create)/components/menu-picker"
|
||||
import { PresetPicker } from "@/app/(create)/components/preset-picker"
|
||||
import { RadiusPicker } from "@/app/(create)/components/radius-picker"
|
||||
import { RandomButton } from "@/app/(create)/components/random-button"
|
||||
import { ResetDialog } from "@/app/(create)/components/reset-button"
|
||||
import { ResetButton } from "@/app/(create)/components/reset-button"
|
||||
import { StylePicker } from "@/app/(create)/components/style-picker"
|
||||
import { ThemePicker } from "@/app/(create)/components/theme-picker"
|
||||
import { V0Button } from "@/app/(create)/components/v0-button"
|
||||
import { FONT_HEADING_OPTIONS, FONTS } from "@/app/(create)/lib/fonts"
|
||||
import { FONTS } from "@/app/(create)/lib/fonts"
|
||||
import { useDesignSystemSearchParams } from "@/app/(create)/lib/search-params"
|
||||
|
||||
export function Customizer({
|
||||
itemsByBase,
|
||||
}: {
|
||||
itemsByBase: Record<string, Pick<RegistryItem, "name" | "title" | "type">[]>
|
||||
}) {
|
||||
export function Customizer() {
|
||||
const [params] = useDesignSystemSearchParams()
|
||||
const isMobile = useIsMobile()
|
||||
const anchorRef = React.useRef<HTMLDivElement | null>(null)
|
||||
@@ -46,59 +34,40 @@ export function Customizer({
|
||||
)
|
||||
|
||||
return (
|
||||
<Card
|
||||
className="dark top-24 right-12 isolate z-10 max-h-full min-h-0 w-full self-start rounded-2xl bg-card/90 shadow-xl backdrop-blur-xl md:w-(--customizer-width)"
|
||||
<div
|
||||
className="no-scrollbar -mx-2.5 flex flex-col overflow-y-auto p-1 md:mx-0 md:h-[calc(100svh-var(--header-height)-2rem)] md:w-48 md:gap-0 md:py-0"
|
||||
ref={anchorRef}
|
||||
size="sm"
|
||||
>
|
||||
<CardHeader className="hidden items-center justify-between gap-2 border-b group-data-reversed/layout:flex-row-reverse md:flex">
|
||||
<MainMenu />
|
||||
</CardHeader>
|
||||
<CardContent className="no-scrollbar min-h-0 flex-1 overflow-x-auto overflow-y-hidden md:overflow-y-auto">
|
||||
<FieldGroup className="flex-row gap-2.5 py-px **:data-[slot=field-separator]:-mx-4 **:data-[slot=field-separator]:w-auto md:flex-col md:gap-3.25">
|
||||
{isMobile && <BasePicker isMobile={isMobile} anchorRef={anchorRef} />}
|
||||
<div className="no-scrollbar h-14 overflow-x-auto overflow-y-hidden p-px md:h-full md:overflow-x-hidden md:overflow-y-auto">
|
||||
<FieldGroup className="flex h-full flex-1 flex-row gap-2 md:flex-col md:gap-0">
|
||||
<PresetPicker
|
||||
presets={PRESETS}
|
||||
isMobile={isMobile}
|
||||
anchorRef={anchorRef}
|
||||
/>
|
||||
<BasePicker isMobile={isMobile} anchorRef={anchorRef} />
|
||||
<StylePicker
|
||||
styles={STYLES}
|
||||
isMobile={isMobile}
|
||||
anchorRef={anchorRef}
|
||||
/>
|
||||
<FieldSeparator className="hidden md:block" />
|
||||
<BaseColorPicker isMobile={isMobile} anchorRef={anchorRef} />
|
||||
<ThemePicker
|
||||
themes={availableThemes}
|
||||
isMobile={isMobile}
|
||||
anchorRef={anchorRef}
|
||||
/>
|
||||
<ChartColorPicker isMobile={isMobile} anchorRef={anchorRef} />
|
||||
<FieldSeparator className="hidden md:block" />
|
||||
<FontPicker
|
||||
label="Heading"
|
||||
param="fontHeading"
|
||||
fonts={FONT_HEADING_OPTIONS}
|
||||
isMobile={isMobile}
|
||||
anchorRef={anchorRef}
|
||||
/>
|
||||
<FontPicker
|
||||
label="Font"
|
||||
param="font"
|
||||
fonts={FONTS}
|
||||
isMobile={isMobile}
|
||||
anchorRef={anchorRef}
|
||||
/>
|
||||
<FieldSeparator className="hidden md:block" />
|
||||
<IconLibraryPicker isMobile={isMobile} anchorRef={anchorRef} />
|
||||
<FontPicker fonts={FONTS} isMobile={isMobile} anchorRef={anchorRef} />
|
||||
<RadiusPicker isMobile={isMobile} anchorRef={anchorRef} />
|
||||
<FieldSeparator className="hidden md:block" />
|
||||
<MenuColorPicker isMobile={isMobile} anchorRef={anchorRef} />
|
||||
<MenuAccentPicker isMobile={isMobile} anchorRef={anchorRef} />
|
||||
<div className="mt-auto hidden w-full flex-col items-center gap-0 md:flex">
|
||||
<RandomButton />
|
||||
<ResetButton />
|
||||
</div>
|
||||
</FieldGroup>
|
||||
</CardContent>
|
||||
<CardFooter className="flex min-w-0 gap-2 md:flex-col md:**:[button,a]:w-full">
|
||||
<CopyPreset className="flex-1 md:flex-none" />
|
||||
<RandomButton className="flex-1 md:flex-none" />
|
||||
<ActionMenu itemsByBase={itemsByBase} />
|
||||
<ResetDialog />
|
||||
</CardFooter>
|
||||
</Card>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user