Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3e81ba1f1a | ||
|
|
e85a120672 | ||
|
|
b91443b595 | ||
|
|
df2badbe89 | ||
|
|
da9cdd8fd6 | ||
|
|
8226fea5b2 | ||
|
|
fbb57da420 | ||
|
|
75338609e2 | ||
|
|
8f53f8b592 | ||
|
|
aa227aa5ba | ||
|
|
19bdc90e38 | ||
|
|
69d529bbe8 | ||
|
|
070f1c3824 | ||
|
|
849e6b3038 | ||
|
|
a0f859e68d | ||
|
|
2327b8e1f4 | ||
|
|
a0560c9411 | ||
|
|
7377a01af9 | ||
|
|
7eaebb8f6d | ||
|
|
68c92aa2b0 | ||
|
|
43a4e04bf9 | ||
|
|
8e73fd6fb6 | ||
|
|
79b3115e0b | ||
|
|
f05628fb23 | ||
|
|
86c8db25f7 | ||
|
|
99a03ca026 | ||
|
|
290297af00 | ||
|
|
bdcf7098a1 | ||
|
|
de512627ce | ||
|
|
af76c459dc | ||
|
|
2fb8b3527c | ||
|
|
fe13a96567 | ||
|
|
db610914ef | ||
|
|
410d318db2 | ||
|
|
fa25586d32 | ||
|
|
4ba63ec643 | ||
|
|
56dcbe713c | ||
|
|
f2f32ddf35 | ||
|
|
600a20bc1e | ||
|
|
5f61fad5ab | ||
|
|
a67cb0ef2a | ||
|
|
9cf60319c2 | ||
|
|
19505421c6 | ||
|
|
9700a3467f | ||
|
|
314a969aff | ||
|
|
533326fef0 | ||
|
|
3738275280 | ||
|
|
d5e0d4e769 | ||
|
|
818c61d1ef | ||
|
|
1038f37f7f | ||
|
|
70c7083823 | ||
|
|
1b34ed829f | ||
|
|
efc833c58a | ||
|
|
81ed5f7031 | ||
|
|
c6babe939b | ||
|
|
0e3fc6c5cf | ||
|
|
f09bf82384 | ||
|
|
270a90fca3 | ||
|
|
0bdc88694d | ||
|
|
dce7c53e4f | ||
|
|
c2b3d0e311 | ||
|
|
7f97bac978 | ||
|
|
a096054b3c | ||
|
|
3cef56bf0e | ||
|
|
7cf60ccadd | ||
|
|
763510638e | ||
|
|
727efb8aa5 | ||
|
|
8a18dcd9ae | ||
|
|
3a672f5925 | ||
|
|
8ece7b73c1 | ||
|
|
e8f29c5c3b | ||
|
|
540a86c813 | ||
|
|
0117fc9eb0 | ||
|
|
07647ad576 | ||
|
|
2ce7e068a4 | ||
|
|
412227426e | ||
|
|
e09fdb19a4 | ||
|
|
1bce47d3df | ||
|
|
5276eea100 | ||
|
|
88ca2d1d2a | ||
|
|
a03905dcb5 | ||
|
|
c2f4c6d718 | ||
|
|
c77ab3a0dd | ||
|
|
cc3225d2d1 | ||
|
|
1b57c12efe | ||
|
|
6b0009d660 | ||
|
|
2403d9121b | ||
|
|
e1e6413e4d | ||
|
|
8e5b0150f3 | ||
|
|
984bf2a0f7 | ||
|
|
18ac03b505 | ||
|
|
17f789968d | ||
|
|
c994d56efc | ||
|
|
57fb484e03 | ||
|
|
6fb3f14122 | ||
|
|
ff066a1ed5 | ||
|
|
1a21d39d36 | ||
|
|
9a26635bab | ||
|
|
f42a257b7e | ||
|
|
07c2b855d9 | ||
|
|
5640d1aade | ||
|
|
e667b6c082 | ||
|
|
a42b2a7343 | ||
|
|
1e2e37cfc1 | ||
|
|
295f9c82d2 | ||
|
|
d901ee1224 | ||
|
|
9403c5cc71 | ||
|
|
480bebd8f0 | ||
|
|
bd39c2829c | ||
|
|
c7b448a5e8 | ||
|
|
58ffb8c048 | ||
|
|
a2b6e1d728 | ||
|
|
989467ebe0 | ||
|
|
c59a5d2088 | ||
|
|
8de3f6cd92 | ||
|
|
29b402ba07 | ||
|
|
32e6d24741 | ||
|
|
2b9a42adb6 | ||
|
|
facaee251c | ||
|
|
9e7e0e450d | ||
|
|
1d69ceba86 | ||
|
|
0f1cbe73df | ||
|
|
a07116b071 | ||
|
|
8136c7370b | ||
|
|
b2edeb4ce2 | ||
|
|
f207346567 | ||
|
|
f360f34c25 | ||
|
|
1af98a1b94 | ||
|
|
fede6df5b9 | ||
|
|
dc96d7f9cf | ||
|
|
174c54a00c | ||
|
|
0c9dfc5404 | ||
|
|
cafe648d78 | ||
|
|
8c36ca3943 | ||
|
|
aeb968a00e | ||
|
|
e5a3ff56e3 | ||
|
|
14b2fd0fbb | ||
|
|
c674ed0693 | ||
|
|
15c71cddb5 | ||
|
|
4a32aaa0e6 | ||
|
|
90ff6f2bcf | ||
|
|
d1111419fc | ||
|
|
221796c4d3 | ||
|
|
7f9b8fdd02 | ||
|
|
b260a89462 | ||
|
|
743dfecf83 | ||
|
|
d8fd3384a7 | ||
|
|
b95f47243f | ||
|
|
a6bdf3ccc2 | ||
|
|
d9a504621a | ||
|
|
fb6b47f283 | ||
|
|
5843a9036f | ||
|
|
c0c12a4027 | ||
|
|
3171a28f79 | ||
|
|
42bd7a6c82 | ||
|
|
b2848e3a7e | ||
|
|
dee99fd5f6 | ||
|
|
fffdec4e5a | ||
|
|
ef7883fb46 | ||
|
|
43360de49e | ||
|
|
40b5a6cd63 | ||
|
|
17acccf3c8 | ||
|
|
c804549138 | ||
|
|
e94944e32c | ||
|
|
983ec522ac | ||
|
|
8162576082 | ||
|
|
16c06b531e | ||
|
|
1cb9fae778 | ||
|
|
8d5900e552 | ||
|
|
2aa680316d | ||
|
|
2478434376 | ||
|
|
70db4b9094 | ||
|
|
65de31faa2 | ||
|
|
05abadf6e3 | ||
|
|
a972bd07dd | ||
|
|
5c8b8f4a95 | ||
|
|
b7893f42c6 | ||
|
|
89c3be7fdb | ||
|
|
75a6dea48d | ||
|
|
8efa2e2e26 | ||
|
|
c9e2dd3080 | ||
|
|
7a3d6b2ed7 | ||
|
|
2b94bc1e05 | ||
|
|
eb04464a35 | ||
|
|
64e9c5a5a6 | ||
|
|
909e7ba5fe | ||
|
|
453391a521 | ||
|
|
e04b6382bd | ||
|
|
e475d671ca | ||
|
|
ba8df39497 | ||
|
|
06e8b83431 | ||
|
|
07e65f6ee7 | ||
|
|
afcaadcec4 | ||
|
|
7c241a0f9d | ||
|
|
45d760bb8c | ||
|
|
8f4d0ab308 | ||
|
|
5f480caae7 | ||
|
|
5fb0d00f51 | ||
|
|
a6af312ec2 | ||
|
|
96a3eb8e90 | ||
|
|
bec13e0895 | ||
|
|
46edd3ffc6 | ||
|
|
037e29de6b | ||
|
|
6859336b71 | ||
|
|
67904a3be3 | ||
|
|
95e3c1f4a8 | ||
|
|
169cdbd6aa | ||
|
|
b7d2af4a43 | ||
|
|
0a978fc2e9 | ||
|
|
554f32c33b | ||
|
|
39e6914ccd | ||
|
|
3ac93f3796 | ||
|
|
0e3b7c33b0 | ||
|
|
c04f85da55 | ||
|
|
df3cb64265 | ||
|
|
026e6777a1 | ||
|
|
6b0ba23097 | ||
|
|
c070efc3e3 | ||
|
|
770eb3ac60 | ||
|
|
2c756221e6 | ||
|
|
4e42407af3 | ||
|
|
6c4c0f6195 | ||
|
|
0c035c5821 | ||
|
|
12c8d445bc | ||
|
|
54ff4d26f8 | ||
|
|
84f56db1c5 | ||
|
|
9d41b8d61f | ||
|
|
f24a56bea9 | ||
|
|
2bb370a1aa | ||
|
|
a0cbee1457 | ||
|
|
8d1f15eed9 | ||
|
|
db848556e2 | ||
|
|
7d1a326873 | ||
|
|
6efcbdc0d8 | ||
|
|
2f252b8b39 | ||
|
|
fd5adb97be | ||
|
|
bae2c4f72e | ||
|
|
cf2d163098 | ||
|
|
ffa90938d7 | ||
|
|
10ac81350c | ||
|
|
155ed253c6 | ||
|
|
05255d06ea | ||
|
|
e59667b74e | ||
|
|
9e5253da27 | ||
|
|
013672ad27 | ||
|
|
502ad0c46f | ||
|
|
094f9afc7b | ||
|
|
368c0ee148 | ||
|
|
dc79213dfe | ||
|
|
d26ea9adb2 | ||
|
|
a9868cf5c4 | ||
|
|
546eaf7604 | ||
|
|
513a4606cc | ||
|
|
4aabb3574d | ||
|
|
746a8657c3 | ||
|
|
db269af1eb | ||
|
|
d1ee9df661 | ||
|
|
1dbfe2b407 | ||
|
|
eeb73544d2 | ||
|
|
a668a219be | ||
|
|
e7794e4a25 | ||
|
|
91c8b5c2ff | ||
|
|
aa201de7c2 | ||
|
|
2e35fde43d | ||
|
|
848ba3392e | ||
|
|
147e538ba7 | ||
|
|
9932e9cdcd | ||
|
|
17b3df1e7c | ||
|
|
6eeaa0a334 | ||
|
|
55088e08e6 | ||
|
|
afb2860597 | ||
|
|
355c591e59 | ||
|
|
b343bad250 | ||
|
|
67d24c7115 | ||
|
|
d324009381 | ||
|
|
c582b34cbc |
@@ -0,0 +1,8 @@
|
||||
# Changesets
|
||||
|
||||
Hello and welcome! This folder has been automatically generated by `@changesets/cli`, a build tool that works
|
||||
with multi-package repos, or single-package repos to help you version and publish your code. You can
|
||||
find the full documentation for it [in our repository](https://github.com/changesets/changesets)
|
||||
|
||||
We have a quick list of common questions to get you started engaging with this project in
|
||||
[our documentation](https://github.com/changesets/changesets/blob/main/docs/common-questions.md)
|
||||
@@ -0,0 +1,11 @@
|
||||
{
|
||||
"$schema": "https://unpkg.com/@changesets/config@2.3.0/schema.json",
|
||||
"changelog": ["@changesets/changelog-github", { "repo": "luxdefi/ui" }],
|
||||
"commit": false,
|
||||
"fixed": [],
|
||||
"linked": [],
|
||||
"access": "public",
|
||||
"baseBranch": "main",
|
||||
"updateInternalDependencies": "patch",
|
||||
"ignore": ["www", "**-template"]
|
||||
}
|
||||
+8
-2
@@ -1,4 +1,10 @@
|
||||
# editorconfig.org
|
||||
root = true
|
||||
|
||||
[*]
|
||||
quote_type = single
|
||||
charset = utf-8
|
||||
end_of_line = lf
|
||||
indent_size = 2
|
||||
indent_style = space
|
||||
indent_size = 2
|
||||
insert_final_newline = true
|
||||
trim_trailing_whitespace = true
|
||||
|
||||
@@ -1,8 +0,0 @@
|
||||
node_modules/
|
||||
target/
|
||||
.next/
|
||||
build/
|
||||
dist/
|
||||
|
||||
/templates/
|
||||
/fixtures/
|
||||
@@ -8,7 +8,6 @@
|
||||
"plugin:tailwindcss/recommended"
|
||||
],
|
||||
"plugins": ["tailwindcss"],
|
||||
"ignorePatterns": ["**/fixtures/**"],
|
||||
"rules": {
|
||||
"@next/next/no-html-link-for-pages": "off",
|
||||
"tailwindcss/no-custom-classname": "off",
|
||||
|
||||
@@ -1,25 +0,0 @@
|
||||
title: "[blocks]: "
|
||||
labels: ["Blocks Request"]
|
||||
body:
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: |
|
||||
### Thanks for taking the time to create a block request! Please search open/closed requests before submitting, as the block or a similar one may have already been requested.
|
||||
|
||||
- type: textarea
|
||||
id: block-description
|
||||
attributes:
|
||||
label: Description
|
||||
description: Tell us about your block request
|
||||
placeholder: "A dashboard for an e-commerce website showing sales, orders, and customers..."
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: input
|
||||
id: block-example-url
|
||||
attributes:
|
||||
label: Example
|
||||
description: Link to an example of the block
|
||||
placeholder: ex. https://example.com
|
||||
validations:
|
||||
required: false
|
||||
@@ -1,3 +0,0 @@
|
||||
# These are supported funding model platforms
|
||||
|
||||
github: [shadcn]
|
||||
@@ -1,85 +0,0 @@
|
||||
name: "Bug report"
|
||||
description: Report an issue
|
||||
title: '[bug]: '
|
||||
labels: ["bug"]
|
||||
body:
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: |
|
||||
### Thanks for taking the time to create a bug report. Please search open/closed issues before submitting, as the issue may have already been reported/addressed.
|
||||
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: |
|
||||
#### If you aren't sure this is a bug or not, please open a discussion instead:
|
||||
- [Discussions](https://github.com/shadcn-ui/ui/discussions/new?category=general)
|
||||
|
||||
- type: textarea
|
||||
id: bug-description
|
||||
attributes:
|
||||
label: Describe the bug
|
||||
description: A clear and concise description of what the bug is. If you intend to submit a PR for this issue, tell us how in the description. Thanks!
|
||||
placeholder: Bug description
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: input
|
||||
id: components-affected
|
||||
attributes:
|
||||
label: Affected component/components
|
||||
description: Which shadcn/ui components are affected?
|
||||
placeholder: ex. Button, Checkbox...
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
id: reproduction
|
||||
attributes:
|
||||
label: How to reproduce
|
||||
description: A step-by-step description of how to reproduce the bug.
|
||||
placeholder: |
|
||||
1. Go to '...'
|
||||
2. Click on '....'
|
||||
3. See error
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: input
|
||||
id: codesandbox-stackblitz
|
||||
attributes:
|
||||
label: Codesandbox/StackBlitz link
|
||||
description: |
|
||||
A link to a CodeSandbox or StackBlitz that includes a minimal reproduction of the problem. In rare cases when not applicable, you can link to a GitHub repository that we can easily run to recreate the issue. If a report is vague and does not have a reproduction, it will be closed without warning.
|
||||
|
||||
> [!CAUTION]
|
||||
> If you skip this step, this issue might be **labeled** with `please add a reproduction` and **closed**.
|
||||
validations:
|
||||
required: false
|
||||
|
||||
- type: textarea
|
||||
id: logs
|
||||
attributes:
|
||||
label: Logs
|
||||
description: "Please include browser console and server logs around the time this bug occurred. Optional if provided reproduction. Please try not to insert an image but copy paste the log text."
|
||||
render: bash
|
||||
|
||||
- type: textarea
|
||||
id: system-info
|
||||
attributes:
|
||||
label: System Info
|
||||
description: Information about browsers, system or binaries that's relevant.
|
||||
render: bash
|
||||
placeholder: System, Binaries, Browsers
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: checkboxes
|
||||
id: terms
|
||||
attributes:
|
||||
label: Before submitting
|
||||
description: By submitting this issue, you agree to follow our [Contributing Guidelines](https://github.com/shadcn-ui/ui/blob/main/CONTRIBUTING.md).
|
||||
options:
|
||||
- label: I've made research efforts and searched the documentation
|
||||
required: true
|
||||
- label: I've searched for existing issues
|
||||
required: true
|
||||
@@ -1,5 +0,0 @@
|
||||
blank_issues_enabled: false
|
||||
contact_links:
|
||||
- name: Get Help
|
||||
url: https://github.com/shadcn-ui/ui/discussions/new?category=general
|
||||
about: If you can't get something to work the way you expect, open a question in our discussion forums.
|
||||
@@ -1,55 +0,0 @@
|
||||
name: "Feature request"
|
||||
description: Create a feature request for shadcn/ui
|
||||
title: '[feat]: '
|
||||
labels: ['area: request']
|
||||
body:
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: |
|
||||
### Thanks for taking the time to create a feature request! Please search open/closed issues before submitting, as the issue may have already been reported/addressed.
|
||||
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: |
|
||||
#### If you aren't sure this is a bug or not, please open a discussion instead:
|
||||
- [Discussions](https://github.com/shadcn-ui/ui/discussions/new?category=general)
|
||||
|
||||
- type: textarea
|
||||
id: feature-description
|
||||
attributes:
|
||||
label: Feature description
|
||||
description: Tell us about your feature request
|
||||
placeholder: 'I think this feature would be great because...'
|
||||
value: 'Describe your feature request...'
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: input
|
||||
id: components-affected
|
||||
attributes:
|
||||
label: Affected component/components
|
||||
description: Is this feature request relevant to any of the already existing components?
|
||||
placeholder: ex. Button, Checkbox...
|
||||
validations:
|
||||
required: false
|
||||
|
||||
- type: textarea
|
||||
id: context
|
||||
attributes:
|
||||
label: Additional Context
|
||||
description: Add any other context about the feature here.
|
||||
placeholder: ex. screenshots, Stack Overflow links, forum links, etc.
|
||||
value: 'Additional details here...'
|
||||
validations:
|
||||
required: false
|
||||
|
||||
- type: checkboxes
|
||||
id: terms
|
||||
attributes:
|
||||
label: Before submitting
|
||||
description: By submitting this issue, you agree to follow our [Contributing Guidelines](https://github.com/shadcn-ui/ui/blob/main/CONTRIBUTING.md).
|
||||
options:
|
||||
- label: I've made research efforts and searched the documentation
|
||||
required: true
|
||||
- label: I've searched for existing issues and PRs
|
||||
required: true
|
||||
@@ -0,0 +1,12 @@
|
||||
// ORIGINALLY FROM CLOUDFLARE WRANGLER:
|
||||
// https://github.com/cloudflare/wrangler2/blob/main/.github/changeset-version.js
|
||||
|
||||
import { exec } from "child_process"
|
||||
|
||||
// This script is used by the `release.yml` workflow to update the version of the packages being released.
|
||||
// The standard step is only to run `changeset version` but this does not update the package-lock.json file.
|
||||
// So we also run `npm install`, which does this update.
|
||||
// This is a workaround until this is handled automatically by `changeset version`.
|
||||
// See https://github.com/changesets/changesets/issues/421.
|
||||
exec("npx changeset version")
|
||||
exec("npm install")
|
||||
@@ -1,46 +0,0 @@
|
||||
version: 2
|
||||
updates:
|
||||
- package-ecosystem: "npm"
|
||||
directory: "/"
|
||||
schedule:
|
||||
interval: "weekly"
|
||||
- package-ecosystem: "npm"
|
||||
directory: "/templates/astro-app"
|
||||
schedule:
|
||||
interval: "weekly"
|
||||
- package-ecosystem: "npm"
|
||||
directory: "/templates/astro-monorepo"
|
||||
schedule:
|
||||
interval: "weekly"
|
||||
- package-ecosystem: "npm"
|
||||
directory: "/templates/next-app"
|
||||
schedule:
|
||||
interval: "weekly"
|
||||
- package-ecosystem: "npm"
|
||||
directory: "/templates/next-monorepo"
|
||||
schedule:
|
||||
interval: "weekly"
|
||||
- package-ecosystem: "npm"
|
||||
directory: "/templates/react-router-app"
|
||||
schedule:
|
||||
interval: "weekly"
|
||||
- package-ecosystem: "npm"
|
||||
directory: "/templates/react-router-monorepo"
|
||||
schedule:
|
||||
interval: "weekly"
|
||||
- package-ecosystem: "npm"
|
||||
directory: "/templates/start-app"
|
||||
schedule:
|
||||
interval: "weekly"
|
||||
- package-ecosystem: "npm"
|
||||
directory: "/templates/start-monorepo"
|
||||
schedule:
|
||||
interval: "weekly"
|
||||
- package-ecosystem: "npm"
|
||||
directory: "/templates/vite-app"
|
||||
schedule:
|
||||
interval: "weekly"
|
||||
- package-ecosystem: "npm"
|
||||
directory: "/templates/vite-monorepo"
|
||||
schedule:
|
||||
interval: "weekly"
|
||||
@@ -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 = "packages/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 = "packages/cli/package.json"
|
||||
try {
|
||||
const pkg = JSON.parse(fs.readFileSync(pkgJsonPath))
|
||||
exec("git rev-parse --short HEAD", (err, stdout) => {
|
||||
|
||||
@@ -1,9 +0,0 @@
|
||||
name: CI/CD
|
||||
on:
|
||||
push: { branches: [main], tags: ["v*"] }
|
||||
pull_request:
|
||||
workflow_dispatch:
|
||||
jobs:
|
||||
cicd:
|
||||
uses: hanzoai/ci/.github/workflows/build.yml@v1
|
||||
secrets: inherit
|
||||
@@ -0,0 +1,116 @@
|
||||
name: Code check
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
branches: ["*"]
|
||||
|
||||
jobs:
|
||||
lint:
|
||||
runs-on: ubuntu-latest
|
||||
name: pnpm lint
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Install Node.js
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: 18
|
||||
|
||||
- uses: pnpm/action-setup@v2.2.4
|
||||
name: Install pnpm
|
||||
id: pnpm-install
|
||||
with:
|
||||
version: 8.6.1
|
||||
run_install: false
|
||||
|
||||
- name: Get pnpm store directory
|
||||
id: pnpm-cache
|
||||
run: |
|
||||
echo "pnpm_cache_dir=$(pnpm store path)" >> $GITHUB_OUTPUT
|
||||
- uses: actions/cache@v3
|
||||
name: Setup pnpm cache
|
||||
with:
|
||||
path: ${{ steps.pnpm-cache.outputs.pnpm_cache_dir }}
|
||||
key: ${{ runner.os }}-pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-pnpm-store-
|
||||
- name: Install dependencies
|
||||
run: pnpm install
|
||||
|
||||
- run: pnpm lint
|
||||
|
||||
format:
|
||||
runs-on: ubuntu-latest
|
||||
name: pnpm format:check
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Install Node.js
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: 18
|
||||
|
||||
- uses: pnpm/action-setup@v2.2.4
|
||||
name: Install pnpm
|
||||
id: pnpm-install
|
||||
with:
|
||||
version: 8.6.1
|
||||
run_install: false
|
||||
|
||||
- name: Get pnpm store directory
|
||||
id: pnpm-cache
|
||||
run: |
|
||||
echo "pnpm_cache_dir=$(pnpm store path)" >> $GITHUB_OUTPUT
|
||||
|
||||
- uses: actions/cache@v3
|
||||
name: Setup pnpm cache
|
||||
with:
|
||||
path: ${{ steps.pnpm-cache.outputs.pnpm_cache_dir }}
|
||||
key: ${{ runner.os }}-pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-pnpm-store-
|
||||
|
||||
- name: Install dependencies
|
||||
run: pnpm install
|
||||
|
||||
- run: pnpm format:check
|
||||
|
||||
tsc:
|
||||
runs-on: ubuntu-latest
|
||||
name: pnpm typecheck
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Install Node.js
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: 18
|
||||
|
||||
- uses: pnpm/action-setup@v2.2.4
|
||||
name: Install pnpm
|
||||
id: pnpm-install
|
||||
with:
|
||||
version: 8.6.1
|
||||
run_install: false
|
||||
|
||||
- name: Get pnpm store directory
|
||||
id: pnpm-cache
|
||||
run: |
|
||||
echo "pnpm_cache_dir=$(pnpm store path)" >> $GITHUB_OUTPUT
|
||||
- uses: actions/cache@v3
|
||||
name: Setup pnpm cache
|
||||
with:
|
||||
path: ${{ steps.pnpm-cache.outputs.pnpm_cache_dir }}
|
||||
key: ${{ runner.os }}-pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-pnpm-store-
|
||||
- name: Install dependencies
|
||||
run: pnpm install
|
||||
|
||||
- run: pnpm typecheck
|
||||
@@ -0,0 +1,65 @@
|
||||
# Adapted from create-t3-app.
|
||||
name: Write Beta Release comment
|
||||
|
||||
on:
|
||||
workflow_run:
|
||||
workflows: ["Release - Beta"]
|
||||
types:
|
||||
- completed
|
||||
|
||||
jobs:
|
||||
comment:
|
||||
if: |
|
||||
github.repository_owner == 'luxdefi-ui' &&
|
||||
${{ github.event.workflow_run.conclusion == 'success' }}
|
||||
runs-on: ubuntu-latest
|
||||
name: Write comment to the PR
|
||||
steps:
|
||||
- name: "Comment on PR"
|
||||
uses: actions/github-script@v6
|
||||
with:
|
||||
github-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
script: |
|
||||
const allArtifacts = await github.rest.actions.listWorkflowRunArtifacts({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
run_id: context.payload.workflow_run.id,
|
||||
});
|
||||
|
||||
for (const artifact of allArtifacts.data.artifacts) {
|
||||
// Extract the PR number and package version from the artifact name
|
||||
const match = /^npm-package-luxdefi-ui@(.*?)-pr-(\d+)/.exec(artifact.name);
|
||||
|
||||
if (match) {
|
||||
require("fs").appendFileSync(
|
||||
process.env.GITHUB_ENV,
|
||||
`\nBETA_PACKAGE_VERSION=${match[1]}` +
|
||||
`\nWORKFLOW_RUN_PR=${match[2]}` +
|
||||
`\nWORKFLOW_RUN_ID=${context.payload.workflow_run.id}`
|
||||
);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
- name: "Comment on PR with Link"
|
||||
uses: marocchino/sticky-pull-request-comment@v2
|
||||
with:
|
||||
number: ${{ env.WORKFLOW_RUN_PR }}
|
||||
message: |
|
||||
A new prerelease is available for testing:
|
||||
|
||||
```sh
|
||||
npx luxdefi-ui@${{ env.BETA_PACKAGE_VERSION }}
|
||||
```
|
||||
|
||||
- name: "Remove the autorelease label once published"
|
||||
uses: actions/github-script@v6
|
||||
with:
|
||||
github-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
script: |
|
||||
github.rest.issues.removeLabel({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
issue_number: '${{ env.WORKFLOW_RUN_PR }}',
|
||||
name: '🚀 autorelease',
|
||||
});
|
||||
@@ -0,0 +1,60 @@
|
||||
# Adapted from create-t3-app.
|
||||
|
||||
name: Release - Beta
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
types: [labeled]
|
||||
branches:
|
||||
- main
|
||||
jobs:
|
||||
prerelease:
|
||||
if: |
|
||||
github.repository_owner == 'luxdefi' &&
|
||||
contains(github.event.pull_request.labels.*.name, '🚀 autorelease')
|
||||
name: Build & Publish a beta release to NPM
|
||||
runs-on: ubuntu-latest
|
||||
environment: Preview
|
||||
|
||||
steps:
|
||||
- name: Checkout Repo
|
||||
uses: actions/checkout@v3
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Use PNPM
|
||||
uses: pnpm/action-setup@v2.2.4
|
||||
with:
|
||||
version: 8.6.1
|
||||
|
||||
- name: Use Node.js 18
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: 18
|
||||
cache: "pnpm"
|
||||
|
||||
- name: Install NPM Dependencies
|
||||
run: pnpm install
|
||||
|
||||
- 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" >> packages/cli/.npmrc
|
||||
env:
|
||||
NPM_ACCESS_TOKEN: ${{ secrets.NPM_ACCESS_TOKEN }}
|
||||
|
||||
- name: Publish Beta to NPM
|
||||
run: pnpm pub:beta
|
||||
|
||||
- name: get-npm-version
|
||||
id: package-version
|
||||
uses: martinbeentjes/npm-get-version-action@main
|
||||
with:
|
||||
path: packages/cli
|
||||
|
||||
- name: Upload packaged artifact
|
||||
uses: actions/upload-artifact@v2
|
||||
with:
|
||||
name: npm-package-luxdefi-ui@${{ steps.package-version.outputs.current-version }}-pr-${{ github.event.number }} # encode the PR number into the artifact name
|
||||
path: packages/cli/dist/index.js
|
||||
@@ -0,0 +1,53 @@
|
||||
# Adapted from create-t3-app.
|
||||
|
||||
name: Release
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
|
||||
jobs:
|
||||
release:
|
||||
if: ${{ github.repository_owner == 'luxdefi-ui' }}
|
||||
name: Create a PR for release workflow
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout Repo
|
||||
uses: actions/checkout@v3
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Use PNPM
|
||||
uses: pnpm/action-setup@v2.2.4
|
||||
with:
|
||||
version: 8.6.1
|
||||
|
||||
- name: Use Node.js 18
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
version: 8.6.1
|
||||
node-version: 18
|
||||
cache: "pnpm"
|
||||
|
||||
- name: Install NPM Dependencies
|
||||
run: pnpm install
|
||||
|
||||
# - name: Check for errors
|
||||
# run: pnpm check
|
||||
|
||||
- name: Build the package
|
||||
run: pnpm build:cli
|
||||
|
||||
- name: Create Version PR or Publish to NPM
|
||||
id: changesets
|
||||
uses: changesets/action@v1.4.1
|
||||
with:
|
||||
commit: "chore(release): version packages"
|
||||
title: "chore(release): version packages"
|
||||
version: node .github/changeset-version.js
|
||||
publish: npx changeset publish
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
NPM_TOKEN: ${{ secrets.NPM_ACCESS_TOKEN }}
|
||||
NODE_ENV: "production"
|
||||
@@ -0,0 +1,42 @@
|
||||
name: Test
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
branches: ["*"]
|
||||
|
||||
jobs:
|
||||
test:
|
||||
runs-on: ubuntu-latest
|
||||
name: pnpm test
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Install Node.js
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: 18
|
||||
|
||||
- uses: pnpm/action-setup@v2.2.4
|
||||
name: Install pnpm
|
||||
id: pnpm-install
|
||||
with:
|
||||
version: 8.6.1
|
||||
run_install: false
|
||||
|
||||
- name: Get pnpm store directory
|
||||
id: pnpm-cache
|
||||
run: |
|
||||
echo "pnpm_cache_dir=$(pnpm store path)" >> $GITHUB_OUTPUT
|
||||
- uses: actions/cache@v3
|
||||
name: Setup pnpm cache
|
||||
with:
|
||||
path: ${{ steps.pnpm-cache.outputs.pnpm_cache_dir }}
|
||||
key: ${{ runner.os }}-pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-pnpm-store-
|
||||
- name: Install dependencies
|
||||
run: pnpm install
|
||||
|
||||
- run: pnpm test
|
||||
+1
-46
@@ -35,49 +35,4 @@ yarn-error.log*
|
||||
# build
|
||||
**/dist
|
||||
|
||||
**/.npmrc
|
||||
|
||||
**/.next
|
||||
|
||||
etc
|
||||
|
||||
# lock files
|
||||
bun.lockb
|
||||
package-lock.json
|
||||
# pnpm-lock.yaml - needed for CI/CD, do not ignore
|
||||
yarn.lock
|
||||
|
||||
# AI cruft
|
||||
AGENTS.md
|
||||
CLAUDE.md
|
||||
GEMINI.md
|
||||
GROK.md
|
||||
QWEN.md
|
||||
|
||||
.playwright-mcp
|
||||
app/out
|
||||
app/.source
|
||||
|
||||
# test artifacts
|
||||
tests/reports/
|
||||
tests/artifacts/
|
||||
|
||||
# Note: Block screenshots are pre-generated and committed
|
||||
# They cannot be generated in CI (requires display/browser)
|
||||
# app/public/r/styles/*/ -- REMOVED to allow committing screenshots
|
||||
|
||||
|
||||
# Test artifacts
|
||||
test-results/
|
||||
playwright-report/
|
||||
*BADGE_INSPECTION*.md
|
||||
*BADGE_INSPECTION*.txt
|
||||
|
||||
# ide
|
||||
.idea
|
||||
.fleet
|
||||
|
||||
.notes
|
||||
.claude/settings.local.json
|
||||
pkg/ui/dist-test/
|
||||
app/public/api/
|
||||
**/.npmrc
|
||||
@@ -1,26 +0,0 @@
|
||||
# The whole caller. Every decision this pipeline makes — what to test, what to
|
||||
# build, where to roll it — is read from the root hanzo.yml, so this file only
|
||||
# ever names the triggers and the reusable.
|
||||
#
|
||||
# `.hanzo/workflows`, not `.github/workflows`, and that is not a style choice:
|
||||
# CI for this repo runs on git.hanzo.ai, which resolves ONLY `.hanzo/workflows`.
|
||||
# github.com has zero self-hosted runners registered for the `hanzo-build-*`
|
||||
# labels this pipeline asks for, so the same file under `.github/workflows`
|
||||
# would queue there forever and never report anything at all.
|
||||
name: CI/CD
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [main]
|
||||
tags: ['v*']
|
||||
pull_request:
|
||||
workflow_dispatch:
|
||||
|
||||
concurrency:
|
||||
group: cicd-${{ github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
cicd:
|
||||
uses: hanzoai/ci/.hanzo/workflows/build.yml@v1
|
||||
secrets: inherit
|
||||
@@ -1,70 +0,0 @@
|
||||
name: deploy
|
||||
|
||||
# RETIRED as an automatic lane. The image is built by hanzo.yml's `images:` block
|
||||
# through hanzoai/ci (.hanzo/workflows/cicd.yml), and this job is kept only as a
|
||||
# manually-dispatched fallback.
|
||||
#
|
||||
# The reason is one capability, not tidiness. ui.hanzo.ai is a static export, so
|
||||
# its ingest key has to be BAKED IN at build; the key lives in KMS as
|
||||
# `deploy/PUBLISHABLE_KEY`, and hanzoai/ci is the only lane that reads it and
|
||||
# passes it as `--build-arg PUBLISHABLE_KEY=…`. The `docker build -t "$image" .`
|
||||
# below cannot — there is no KMS hop in it and nowhere to put the value — so any
|
||||
# image it produced would ship a client with no key at all, and cloud answers
|
||||
# `401 ingest_key_required` for an unattributed write. With the Dockerfile now
|
||||
# failing closed on an empty key, this job fails on every run instead of quietly
|
||||
# publishing an inert site.
|
||||
#
|
||||
# Left in place rather than deleted, and dispatch-only rather than on push:
|
||||
# deleting a repo's only build lane strands the host with no failing run to show
|
||||
# it, and two push-triggered builders for one image means one commit yields two
|
||||
# images under two tag schemes. Dispatching this publishes `<short-sha>` with an
|
||||
# EMPTY key and the Dockerfile will refuse it — so if you dispatch it, pass the
|
||||
# key yourself or expect the gate to fire.
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
|
||||
concurrency:
|
||||
group: deploy-${{ github.ref }}
|
||||
cancel-in-progress: false
|
||||
|
||||
jobs:
|
||||
image:
|
||||
# hanzo-build-linux-amd64, not ubuntu-latest. This file lives in
|
||||
# .hanzo/workflows, which GitHub never reads -- git.hanzo.ai is the only
|
||||
# thing that can run it, and the fleet deliberately advertises no generic
|
||||
# ubuntu-* label (~1400 mirrored forks all ask for it; one bad job was
|
||||
# retried ~520 times across 10 runners). An unmatched label is not an
|
||||
# error here, it is silence: the job queues until the 24h timeout, so this
|
||||
# deploy has never produced an image and nothing ever said so.
|
||||
runs-on: hanzo-build-linux-amd64
|
||||
timeout-minutes: 60
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: build and push
|
||||
# GHCR_TOKEN/GHCR_USER, which are the org secrets that actually exist.
|
||||
# This job asked for REGISTRY_TOKEN, which was never defined anywhere —
|
||||
# the org has exactly GHCR_TOKEN, GHCR_USER, GH_PAT, KMS_CLIENT_ID,
|
||||
# KMS_CLIENT_SECRET, OCI_TOKEN, OCI_USER — so the guard below fired on
|
||||
# every run and nothing was ever published. Every other image build in
|
||||
# the org already uses the GHCR_* pair; this file and papers were the
|
||||
# two that invented a name.
|
||||
#
|
||||
# The username comes from the secret too, rather than being hardcoded to
|
||||
# hanzo-dev, so rotating the publishing identity is one org-level change.
|
||||
env:
|
||||
GHCR_TOKEN: ${{ secrets.GHCR_TOKEN }}
|
||||
GHCR_USER: ${{ secrets.GHCR_USER }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
if [ -z "${GHCR_TOKEN:-}" ] || [ -z "${GHCR_USER:-}" ]; then
|
||||
echo "::error::GHCR_TOKEN/GHCR_USER are unset, so nothing was published."
|
||||
exit 1
|
||||
fi
|
||||
tag="$(git rev-parse --short HEAD)"
|
||||
image="ghcr.io/hanzoai/ui:${tag}"
|
||||
echo "${GHCR_TOKEN}" | docker login ghcr.io -u "${GHCR_USER}" --password-stdin
|
||||
docker build -t "${image}" .
|
||||
docker push "${image}"
|
||||
echo "published ${image} — set image.tag AND image.digest in universe charts/app/values/hanzo/ui.yaml"
|
||||
@@ -1,343 +0,0 @@
|
||||
name: Publish Packages
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [main]
|
||||
paths:
|
||||
# BOTH package roots, and the singular one is not a typo — `@hanzo/ui` and
|
||||
# `@hanzo/data` live under pkg/, everything else under pkgs/.
|
||||
#
|
||||
# ⚠️ This list and the `for pkg_json in …` loop below are ONE FACT. The loop
|
||||
# was already fixed to walk both roots, with a comment explaining that a
|
||||
# `pkgs/*` glob made pkg/ packages invisible — but this gate was left
|
||||
# matching `pkgs/*` only, so for a pkg/-only change the corrected job could
|
||||
# never be REACHED. The fix was reasoned about in the body and not applied
|
||||
# to the thing that decides whether the body runs.
|
||||
#
|
||||
# Cost: @hanzo/ui 8.0.29 sat unpublished while npmjs served 8.0.28, so
|
||||
# every consumer resolving `^8.0.17` silently got a version without the
|
||||
# `./chat` entrypoint — an import error in the dependent, nothing at all in
|
||||
# this repo. Add a root here whenever one is added to the loop.
|
||||
- 'pkg/*/package.json'
|
||||
- 'pkgs/*/package.json'
|
||||
# A publish that fails for an infrastructure reason — the runner missing a
|
||||
# tool, npmjs answering slowly — could only be retried by pushing another
|
||||
# commit that touches a package.json, i.e. by burning a version number to
|
||||
# re-run a job that was never wrong about the code. detect-changes reads what
|
||||
# npmjs SERVES, so a dispatch is idempotent by construction: it publishes
|
||||
# exactly the packages that are ahead of the registry, and nothing when none
|
||||
# are.
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
detect-changes:
|
||||
runs-on: hanzo-build-linux-amd64
|
||||
outputs:
|
||||
packages: ${{ steps.changed.outputs.packages }}
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 2
|
||||
|
||||
- name: Detect changed packages
|
||||
id: changed
|
||||
run: |
|
||||
CHANGED=()
|
||||
# Both package roots. `@hanzo/ui` and `@hanzo/data` live under pkg/ (singular)
|
||||
# and were invisible to a `pkgs/*` glob, so no version bump of either could
|
||||
# ever reach npmjs from here.
|
||||
for pkg_json in pkg/*/package.json pkgs/*/package.json; do
|
||||
[ ! -f "$pkg_json" ] && continue
|
||||
# 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
|
||||
|
||||
# What npm SERVES, not what the previous commit said. HEAD~1 is the
|
||||
# FIRST PARENT, so a merge whose first parent is the feature branch
|
||||
# already carries the new version and the diff is empty — the publish
|
||||
# is skipped and npm keeps serving the old release, silently. Asking
|
||||
# the registry is independent of merge topology and of how many
|
||||
# commits the runner fetched.
|
||||
NEW=$(jq -r '.version' "$pkg_json")
|
||||
OLD=$(curl -sf "https://registry.npmjs.org/${PKG_NAME}" \
|
||||
| jq -r '."dist-tags".latest // "0.0.0"' 2>/dev/null || echo "0.0.0")
|
||||
|
||||
# FORWARD ONLY. `!=` fires in both directions, and several packages
|
||||
# sit BEHIND npm (a release cut elsewhere, a revert) — publishing
|
||||
# those walks the registry backwards. Only a strictly greater local
|
||||
# version is a release.
|
||||
NEWEST=$(printf '%s\n%s\n' "$OLD" "$NEW" | sort -V | tail -1)
|
||||
if [ "$OLD" != "$NEW" ] && [ "$NEWEST" = "$NEW" ]; then
|
||||
echo "Version change: $PKG_NAME ($OLD -> $NEW)"
|
||||
# The package NAME, because every downstream step spends this as a
|
||||
# `pnpm --filter` argument and a directory basename matches nothing.
|
||||
# An unmatched filter exits 0, so the build silently no-ops and the
|
||||
# publish ships an unbuilt tarball — how 8.0.17 and 8.0.19 shipped broken.
|
||||
CHANGED+=("$PKG_NAME")
|
||||
fi
|
||||
done
|
||||
|
||||
if [ ${#CHANGED[@]} -eq 0 ]; then
|
||||
echo "packages=[]" >> $GITHUB_OUTPUT
|
||||
else
|
||||
JSON=$(printf '%s\n' "${CHANGED[@]}" | jq -R . | jq -s -c .)
|
||||
echo "packages=$JSON" >> $GITHUB_OUTPUT
|
||||
fi
|
||||
|
||||
publish:
|
||||
needs: detect-changes
|
||||
if: needs.detect-changes.outputs.packages != '[]'
|
||||
runs-on: hanzo-build-linux-amd64
|
||||
strategy:
|
||||
# One package's broken build must not withhold every other package's
|
||||
# release: the matrix legs are independent publishes, not one artifact.
|
||||
fail-fast: false
|
||||
matrix:
|
||||
package: ${{ fromJson(needs.detect-changes.outputs.packages) }}
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- uses: pnpm/action-setup@v4
|
||||
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 20
|
||||
registry-url: 'https://registry.npmjs.org'
|
||||
|
||||
- name: Install dependencies
|
||||
run: pnpm install
|
||||
|
||||
- name: Build package
|
||||
# The trailing `...` is load-bearing: it builds the package's WORKSPACE
|
||||
# DEPENDENCIES first, in topological order.
|
||||
#
|
||||
# Without it, `pnpm --filter @hanzo/ui build` runs alone against siblings
|
||||
# that have no `dist/`. Those siblings type themselves through
|
||||
# `"types": "dist/index.d.ts"`, so tsc reports them as missing modules and
|
||||
# the errors do not name the real cause:
|
||||
# src/product/SiteNav.tsx: Cannot find module '@hanzo/products'
|
||||
# src/core/tokens.ts: Cannot find module '@hanzo/tokens'
|
||||
# src/gitops.ts: Cannot find module '@hanzo/cd'
|
||||
# plus a wave of implicit-any errors in the files that imported them,
|
||||
# which read like a source problem in @hanzo/ui and are not.
|
||||
run: pnpm --filter ${{ matrix.package }}... build
|
||||
|
||||
- name: Provision bun (only for a package whose test script asks for it)
|
||||
# @hanzo/canvas declares `bun test` and this runner has no bun, so its
|
||||
# suite has been `spawn ENOENT` on every run — which fails the leg before
|
||||
# the publish and is why canvas has sat at 0.2.2 here against 0.2.1 on
|
||||
# npmjs. The step that exists to stop an untested release was itself the
|
||||
# thing stopping the release.
|
||||
#
|
||||
# Read from the package's OWN manifest, not from a list here, so a
|
||||
# package that adopts or drops bun needs no edit to this file. A package
|
||||
# that does not ask for bun does not pay for it.
|
||||
run: |
|
||||
set -euo pipefail
|
||||
PKG_DIR=$(pnpm --filter ${{ matrix.package }} exec pwd | tail -1)
|
||||
if ! jq -r '.scripts.test // ""' "$PKG_DIR/package.json" | grep -qw bun; then
|
||||
echo "::notice::${{ matrix.package }} does not run bun"
|
||||
exit 0
|
||||
fi
|
||||
if command -v bun >/dev/null 2>&1; then
|
||||
echo "bun already on PATH: $(bun --version)"
|
||||
exit 0
|
||||
fi
|
||||
# From npmjs, not bun.sh/install: this job already reaches the npm
|
||||
# registry (it is what the job exists to publish to) and setup-node
|
||||
# already put the global bin on PATH, so this adds no new host to
|
||||
# trust and no new failure mode. The shell installer would also want
|
||||
# unzip, which a minimal runner image need not carry.
|
||||
npm install -g bun
|
||||
bun --version
|
||||
|
||||
- name: Test package
|
||||
# A published version is IMMUTABLE, so this is the last point at which a
|
||||
# defect costs nothing. Nothing else runs a package's own tests before it
|
||||
# ships: ci.yml's test job runs `cd pkgs/ui && …` — that one directory —
|
||||
# and this workflow went checkout → build → publish. So @hanzo/event's
|
||||
# suite, which pins the version the client stamps on every event, ran in
|
||||
# no workflow that gates a release, and the stale stamp it was written to
|
||||
# catch could still have reached npmjs.
|
||||
#
|
||||
# `--if-present` so a package with no test script is skipped rather than
|
||||
# failing the release: the set of testable packages is read from each
|
||||
# package.json, not listed here, so a new package is covered the day it
|
||||
# adds a `test` script.
|
||||
#
|
||||
# This step needs `test` to mean the same thing in every package — the
|
||||
# package's own suite, no browser, no network. @hanzo/ui briefly chained
|
||||
# its two CONSUMER suites onto it (pack a tarball, npm-install it into a
|
||||
# throwaway app, drive it in chromium), which is a real gate and belongs
|
||||
# exactly where hanzo.yml already runs it: on the push, in its own
|
||||
# `ui-consumer` job, next to the `playwright install` that gives it a
|
||||
# browser. Here there is no such step, so a chained `test` could only
|
||||
# fail — the last gate before an immutable version would have been one
|
||||
# that cannot pass, which is the failure mode this repo has already paid
|
||||
# for twice.
|
||||
run: pnpm --filter ${{ matrix.package }} run --if-present test
|
||||
|
||||
- name: Verify the tarball carries what package.json promises
|
||||
# `types` is a PROMISE to every consumer, and tsup writes dist/*.mjs in a
|
||||
# separate pass from dist/*.d.ts — so a declaration failure yields a
|
||||
# package whose JS is fine and whose types are absent. That is invisible
|
||||
# here and surfaces in dependents as "Could not find a declaration file".
|
||||
#
|
||||
# Asserted from the package's OWN manifest, so it holds for every package
|
||||
# this workflow publishes and needs no per-package list.
|
||||
run: |
|
||||
set -euo pipefail
|
||||
PKG_DIR=$(pnpm --filter ${{ matrix.package }} exec pwd | tail -1)
|
||||
TYPES=$(jq -r '.types // .typings // empty' "$PKG_DIR/package.json")
|
||||
if [ -z "$TYPES" ]; then
|
||||
echo "::notice::${{ matrix.package }} declares no types entrypoint — nothing to verify"
|
||||
exit 0
|
||||
fi
|
||||
if [ ! -f "$PKG_DIR/$TYPES" ]; then
|
||||
echo "::error::${{ matrix.package }} declares types at $TYPES but the build produced no such file."
|
||||
echo "::error::Publishing would ship a package that silently has no types. Fix the declaration build."
|
||||
exit 1
|
||||
fi
|
||||
echo "types present: $TYPES"
|
||||
|
||||
- name: Fetch the npm token from KMS
|
||||
id: npmtok
|
||||
# publish.yml used secrets.NPM_TOKEN, which exists on NEITHER the hanzoai
|
||||
# org nor this repo on git.hanzo.ai. An absent secret interpolates to the
|
||||
# empty string rather than failing, so every publish reached npmjs
|
||||
# unauthenticated and 401'd -- the reason @hanzo/ui has never shipped from
|
||||
# CI and each release was a hand publish.
|
||||
#
|
||||
# KMS is where a secret lives, so the token is read at run time from the
|
||||
# same org path and with the same machine identity every other workflow
|
||||
# here already uses. Nothing new to rotate, and no npm credential is
|
||||
# stored on the forge.
|
||||
env:
|
||||
KMS_CLIENT_ID: ${{ secrets.KMS_CLIENT_ID }}
|
||||
KMS_CLIENT_SECRET: ${{ secrets.KMS_CLIENT_SECRET }}
|
||||
KMS_ENDPOINT: ${{ vars.KMS_ENDPOINT || 'https://kms.hanzo.ai' }}
|
||||
KMS_ORG: ${{ vars.KMS_ORG || 'hanzo' }}
|
||||
KMS_SECRET_ENV: ${{ vars.KMS_SECRET_ENV || 'prod' }}
|
||||
# Where the token actually lives, MEASURED: NPM_TOKEN sits at the org
|
||||
# root, not under deploy/ (which holds CLOUDFLARE_*, GIT_TOKEN,
|
||||
# KUBECONFIG, UNIVERSE_PIN_TOKEN). Empty means root; set the
|
||||
# KMS_NPM_PATH variable to move it without editing this file.
|
||||
KMS_NPM_PATH: ${{ vars.KMS_NPM_PATH || '' }}
|
||||
run: |
|
||||
token=$(curl -fsS "${KMS_ENDPOINT}/v1/kms/auth/login" \
|
||||
-H 'Content-Type: application/json' \
|
||||
-d "{\"clientId\":\"${KMS_CLIENT_ID}\",\"clientSecret\":\"${KMS_CLIENT_SECRET}\"}" \
|
||||
| jq -r '.accessToken // empty')
|
||||
if [ -z "${token}" ]; then
|
||||
echo "::error::KMS login failed at ${KMS_ENDPOINT}/v1/kms/auth/login (check KMS_CLIENT_ID/KMS_CLIENT_SECRET)."
|
||||
exit 1
|
||||
fi
|
||||
# The read is org-scoped by the TOKEN's owner claim, not by a path
|
||||
# segment: the flat form is the contract cloud's embedded KMS serves,
|
||||
# and it answers {name, env, value}. The older
|
||||
# /v1/kms/orgs/<org>/secrets/... form with .secret.value 404s here.
|
||||
secret_url="${KMS_ENDPOINT}/v1/kms/secrets/${KMS_NPM_PATH:+${KMS_NPM_PATH}/}NPM_TOKEN?env=${KMS_SECRET_ENV}"
|
||||
# No -f: a 404 must report WHICH path was empty, not just exit 22.
|
||||
npm_token=$(curl -sS "${secret_url}" -H "Authorization: Bearer ${token}" | jq -r '.value // empty')
|
||||
if [ -z "${npm_token}" ]; then
|
||||
echo "::error::No NPM_TOKEN at ${secret_url}."
|
||||
echo "::error::Put it there, or set the KMS_NPM_PATH variable to the path that has it."
|
||||
exit 1
|
||||
fi
|
||||
# Masked before it is ever an output, so it cannot surface in a log.
|
||||
echo "::add-mask::${npm_token}"
|
||||
echo "token=${npm_token}" >> "$GITHUB_OUTPUT"
|
||||
|
||||
- name: Publish package
|
||||
# Idempotent on purpose: the desired state is "npmjs holds this version", and
|
||||
# a version already there satisfies it. Versions are immutable, so a conflict
|
||||
# means the release already happened (a re-run, or a hand publish) — that is
|
||||
# success, not a failure. Every other error still fails the job.
|
||||
env:
|
||||
NODE_AUTH_TOKEN: ${{ steps.npmtok.outputs.token }}
|
||||
run: |
|
||||
set -o pipefail
|
||||
OUT=$(pnpm --filter ${{ matrix.package }} publish \
|
||||
--no-git-checks --access public 2>&1) || true
|
||||
echo "$OUT"
|
||||
case "$OUT" in
|
||||
*EPUBLISHCONFLICT*|*"cannot publish over"*|*"previously published versions"*)
|
||||
echo "::notice::${{ matrix.package }} is already on npmjs at this version — nothing to do" ;;
|
||||
*)
|
||||
# Not a conflict: confirm the version really landed, else fail loudly.
|
||||
#
|
||||
# RETRIED, because npmjs is read-after-write eventually consistent
|
||||
# and this asked it exactly once, about two seconds after the
|
||||
# write. On run 26906 both @hanzo/og@1.0.0 and @hanzo/shop@1.0.0
|
||||
# failed here having printed npm's own `+ @hanzo/og@1.0.0` success
|
||||
# line moments earlier, and both are on the registry now: the
|
||||
# publishes worked and the check was simply too early.
|
||||
#
|
||||
# A false red here is worse than a slow green. It reports that a
|
||||
# release did not happen when it did, so the next person bumps the
|
||||
# version to "fix" it and burns a number over a replication lag —
|
||||
# and the real signal, a publish that genuinely failed, becomes
|
||||
# indistinguishable from the noise.
|
||||
PKG_DIR=$(pnpm --filter ${{ matrix.package }} exec pwd | tail -1)
|
||||
NAME=$(jq -r '.name' "$PKG_DIR/package.json")
|
||||
WANT=$(jq -r '.version' "$PKG_DIR/package.json")
|
||||
landed=
|
||||
for attempt in 1 2 3 4 5 6; do
|
||||
if npm view "$NAME@$WANT" version >/dev/null 2>&1; then landed=1; break; fi
|
||||
echo "$NAME@$WANT not visible to npmjs yet (attempt $attempt/6); waiting 10s"
|
||||
sleep 10
|
||||
done
|
||||
[ -n "$landed" ] \
|
||||
|| { echo "::error::$NAME@$WANT is not on npmjs 60s after publish"; exit 1; }
|
||||
echo "published $NAME@$WANT" ;;
|
||||
esac
|
||||
|
||||
- name: Publish to the Hanzo registry
|
||||
# Second home for the same tarball: api.hanzo.ai/v1/packages/hanzo/npm, our
|
||||
# own registry (hanzoai/git), so an install does not have to reach npmjs.
|
||||
# Same artifact, same version — this republishes what the step above just
|
||||
# built, it does not rebuild or re-version.
|
||||
#
|
||||
# BEST-EFFORT by construction. No token ⇒ skip with a notice, so this can
|
||||
# land before the credential exists and starts working the moment it does.
|
||||
# An already-published version is a success, not a failure: the registry
|
||||
# holding it is the desired state either way. npmjs is authoritative and is
|
||||
# never affected by anything here.
|
||||
env:
|
||||
# REGISTRY_TOKEN is the fleet-wide name (six other workflows use it and
|
||||
# the hanzoai org carries it). This asked for HANZO_REGISTRY_TOKEN, which
|
||||
# is defined nowhere, so it read as empty and the step below took its
|
||||
# "not set" branch and exited 0 on every run -- the mirror to
|
||||
# api.hanzo.ai has never happened, and said so only as a notice.
|
||||
HANZO_REGISTRY_TOKEN: ${{ secrets.REGISTRY_TOKEN }}
|
||||
run: |
|
||||
set -uo pipefail
|
||||
if [ -z "${HANZO_REGISTRY_TOKEN:-}" ]; then
|
||||
echo "::notice::HANZO_REGISTRY_TOKEN not set — package published to npmjs only."
|
||||
echo "::notice::Add it (a Hanzo Git token with package:write) to mirror to api.hanzo.ai."
|
||||
exit 0
|
||||
fi
|
||||
REG=https://api.hanzo.ai/v1/packages/hanzo/npm/
|
||||
NPMRC="$RUNNER_TEMP/.npmrc-hanzo"
|
||||
{
|
||||
echo "@hanzo:registry=${REG}"
|
||||
echo "//api.hanzo.ai/v1/packages/hanzo/npm/:_authToken=${HANZO_REGISTRY_TOKEN}"
|
||||
} > "$NPMRC"
|
||||
out=$(npm_config_userconfig="$NPMRC" \
|
||||
pnpm --filter ${{ matrix.package }} publish \
|
||||
--no-git-checks --access public --registry "$REG" 2>&1) || true
|
||||
echo "$out"
|
||||
case "$out" in
|
||||
*EPUBLISHCONFLICT*|*"cannot publish over"*|*"already exists"*)
|
||||
echo "::notice::already in the Hanzo registry at this version — nothing to do" ;;
|
||||
*) echo "$out" | grep -qiE 'error|failed' \
|
||||
&& echo "::warning::mirror to api.hanzo.ai failed (npmjs publish unaffected)" \
|
||||
|| echo "mirrored to the Hanzo registry" ;;
|
||||
esac
|
||||
@@ -1,91 +0,0 @@
|
||||
name: Validate Registries
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
paths:
|
||||
- "apps/v4/public/r/registries.json"
|
||||
- "apps/v4/registry/directory.json"
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
paths:
|
||||
- "apps/v4/public/r/registries.json"
|
||||
- "apps/v4/registry/directory.json"
|
||||
|
||||
jobs:
|
||||
validate:
|
||||
runs-on: hanzo-build-linux-amd64
|
||||
name: pnpm validate:registries
|
||||
permissions:
|
||||
contents: read
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Install Node.js
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: 20
|
||||
|
||||
- name: Block reserved registry namespaces
|
||||
env:
|
||||
RESERVED_NAMESPACES: "@shadcn,@ui,@blocks,@components,@block,@component,@util,@utils,@registry,@lib,@hook,@hooks,@theme,@themes,@chart,@charts"
|
||||
run: |
|
||||
node <<'EOF'
|
||||
const fs = require("node:fs")
|
||||
|
||||
const files = [
|
||||
"apps/v4/public/r/registries.json",
|
||||
"apps/v4/registry/directory.json",
|
||||
]
|
||||
const reservedNamespaces = new Set(
|
||||
process.env.RESERVED_NAMESPACES.split(",").filter(Boolean)
|
||||
)
|
||||
|
||||
function readNames(filePath) {
|
||||
return JSON.parse(fs.readFileSync(filePath, "utf8")).map(
|
||||
(entry) => entry.name
|
||||
)
|
||||
}
|
||||
|
||||
const violations = files.flatMap((filePath) => {
|
||||
return readNames(filePath)
|
||||
.filter((name) => reservedNamespaces.has(name))
|
||||
.map((name) => `${filePath}: ${name}`)
|
||||
})
|
||||
|
||||
if (violations.length > 0) {
|
||||
console.error("Reserved registry namespaces are not allowed:")
|
||||
|
||||
for (const violation of violations) {
|
||||
console.error(`- ${violation}`)
|
||||
}
|
||||
|
||||
process.exit(1)
|
||||
}
|
||||
EOF
|
||||
|
||||
- uses: pnpm/action-setup@v4
|
||||
name: Install pnpm
|
||||
id: pnpm-install
|
||||
with:
|
||||
version: 9.0.6
|
||||
run_install: false
|
||||
|
||||
- name: Get pnpm store directory
|
||||
id: pnpm-cache
|
||||
run: |
|
||||
echo "pnpm_cache_dir=$(pnpm store path)" >> $GITHUB_OUTPUT
|
||||
- uses: actions/cache@v3
|
||||
name: Setup pnpm cache
|
||||
with:
|
||||
path: ${{ steps.pnpm-cache.outputs.pnpm_cache_dir }}
|
||||
key: ${{ runner.os }}-pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-pnpm-store-
|
||||
- name: Install dependencies
|
||||
run: pnpm install
|
||||
|
||||
- name: Validate registries
|
||||
run: pnpm --filter=v4 validate:registries
|
||||
@@ -1,3 +1 @@
|
||||
auto-install-peers=true
|
||||
link-workspace-packages=true
|
||||
puppeteer_skip_download=true
|
||||
|
||||
+1
-9
@@ -3,12 +3,4 @@ node_modules
|
||||
.next
|
||||
build
|
||||
.contentlayer
|
||||
**/fixtures
|
||||
deprecated
|
||||
apps/v4/registry/styles/**/*.css
|
||||
|
||||
# Byte-coupled: hz.js carries the marked region of src/anon.js VERBATIM, and
|
||||
# pkgs/event/src/anon.test.ts compares them byte for byte. Reformatting either
|
||||
# one alone breaks the build; the same region is also vendored by hanzoai/cloud.
|
||||
pkgs/event/src/anon.js
|
||||
pkgs/event/hz.js
|
||||
apps/www/pages/api/registry.json
|
||||
Vendored
+2
-22
@@ -1,24 +1,4 @@
|
||||
{
|
||||
"typescript.tsdk": "node_modules/.pnpm/typescript@5.3.3/node_modules/typescript/lib",
|
||||
"editor.gotoLocation.multipleDefinitions": "goto",
|
||||
"typescript.preferences.quoteStyle": "single",
|
||||
"eslint.workingDirectories": [
|
||||
{ "pattern": "apps/*/" },
|
||||
{ "pattern": "pkgs/*/" },
|
||||
{ "pattern": "pkgs/*/" }
|
||||
],
|
||||
"tailwindCSS.classFunctions": ["cva", "cn"],
|
||||
"vitest.debugExclude": [
|
||||
"<node_internals>/**",
|
||||
"**/node_modules/**",
|
||||
"**/fixtures/**"
|
||||
],
|
||||
"files.exclude": {
|
||||
"deprecated": true
|
||||
},
|
||||
"search.exclude": {
|
||||
"apps/v4/registry/radix-*": true,
|
||||
"apps/v4/public/r/*": true,
|
||||
"pkgs/shadcn/test/fixtures/*": true
|
||||
}
|
||||
}
|
||||
"editor.gotoLocation.multipleDefinitions": "goto"
|
||||
}
|
||||
-129
@@ -1,129 +0,0 @@
|
||||
# Changelog
|
||||
|
||||
All notable changes to the Hanzo UI component library will be documented in this file.
|
||||
|
||||
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
||||
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
||||
|
||||
## [Unreleased]
|
||||
|
||||
### Added
|
||||
- Slot support for Badge component with `asChild` prop for composition flexibility
|
||||
- Modern focus-visible states for Badge component with ring styling
|
||||
- SVG icon support in Badge component with automatic sizing
|
||||
- Aria-invalid states for Badge form validation
|
||||
- Data-slot attributes for better styling hooks
|
||||
|
||||
### Changed
|
||||
- **THEME**: Restored electric blue primary color (`oklch(0.653 0.269 252.44)`) as default
|
||||
- **THEME**: Updated to use shadcn/ui's neutral gray color scheme for backgrounds and text
|
||||
- **THEME**: Maintained configurable theme system via theme generator
|
||||
- Badge component now uses `<span>` instead of `<div>` for better semantics
|
||||
- Badge font weight changed from `font-semibold` to `font-medium`
|
||||
- Badge padding adjusted from `px-2.5` to `px-2`
|
||||
- Badge hover states now only apply within anchor tags (`[a&]:hover`)
|
||||
- Updated both default (rounded-full) and new-york (rounded-md) badge variants
|
||||
- **Button component updated to shadcn/ui v4 patterns**:
|
||||
- Changed focus ring from `ring-2` to `ring-[3px]` with 50% opacity
|
||||
- Added SVG icon support with `[&_svg]:size-4` default sizing
|
||||
- Added `has-[>svg]:px-*` conditional padding when button contains icons
|
||||
- Added new icon size variants: `icon-sm` (size-8) and `icon-lg` (size-10)
|
||||
- Changed from `forwardRef` to regular function component
|
||||
- Added `data-slot="button"` attribute
|
||||
- Improved accessibility with `aria-invalid` states
|
||||
- Enhanced dark mode support for destructive, outline, and ghost variants
|
||||
- **Input component updated to shadcn/ui v4 patterns**:
|
||||
- Changed height from `h-10` to `h-9` (consistent with Button)
|
||||
- Changed from `forwardRef` to regular function component
|
||||
- Added `data-slot="input"` attribute
|
||||
- Changed background to transparent with `dark:bg-input/30` dark mode variant
|
||||
- Added responsive text sizing: `text-base md:text-sm`
|
||||
- Added `min-w-0` to prevent flex overflow issues
|
||||
- Enhanced file input styling with `file:inline-flex file:h-7`
|
||||
- Added selection highlighting: `selection:bg-primary selection:text-primary-foreground`
|
||||
- Changed focus ring from `ring-2` to `ring-[3px]` with 50% opacity
|
||||
- Added `aria-invalid` states for form validation
|
||||
- Changed transition to `transition-[color,box-shadow]` for smoother animations
|
||||
|
||||
### Fixed
|
||||
- Badge component styling now matches shadcn/ui v4 implementation
|
||||
- Improved accessibility with proper focus-visible states
|
||||
- Better form integration with aria-invalid support
|
||||
- **Security**: Fixed 31 of 32 vulnerabilities (96.9% reduction):
|
||||
- Upgraded happy-dom from 19.0.2 to 20.0.10 (fixes 2 critical RCE vulnerabilities)
|
||||
- Upgraded vite from 5.4.20 to 7.1.12 (fixes moderate vulnerability)
|
||||
- Applied 21 package overrides for next, axios, undici, prismjs, and other dependencies
|
||||
- All critical (5), all but 1 high (3/4), all moderate (19), and all low (5) vulnerabilities resolved
|
||||
- Remaining: 1 high severity in lodash.template (no patch available, abandoned package)
|
||||
|
||||
### Removed
|
||||
- Removed hanzo-compatibility documentation page (redundant with main docs)
|
||||
|
||||
## [5.0.0] - 2025-10-18
|
||||
|
||||
### Added
|
||||
- Next.js 16.0.0 with Turbopack support
|
||||
- React 19.2.0 support
|
||||
- 150+ components in registry
|
||||
- AI-specific components (ai-chat, ai-playground, ai-assistant, ai-vision, etc.)
|
||||
- 3D components (3d-button, 3d-card, 3d-carousel, 3d-grid, etc.)
|
||||
- Animation components (animated-beam, animated-text, animated-cursor, etc.)
|
||||
- Page builder with drag-drop functionality
|
||||
- White-label system for Zoo UI and Lux UI forks
|
||||
- External registry support (35+ sources)
|
||||
- 24+ full-page block templates
|
||||
|
||||
### Changed
|
||||
- Migrated from Contentlayer to Fumadocs for MDX processing
|
||||
- Updated documentation structure
|
||||
- Improved build process with registry-first approach
|
||||
|
||||
### Technical
|
||||
- Tailwind CSS v4.1.14
|
||||
- TypeScript 5.9.3
|
||||
- pnpm 9.0.6+ workspace support
|
||||
- Turborepo build system
|
||||
|
||||
---
|
||||
|
||||
## Version Comparison
|
||||
|
||||
**Hanzo UI vs shadcn/ui:**
|
||||
- **161 components** vs shadcn's 58 components (3x more)
|
||||
- **Unique to Hanzo UI**: AI components (12), 3D components (9), Animation components (13), specialized navigation variants (15+)
|
||||
- **100% compatible** with shadcn/ui CLI and registry system
|
||||
- **Multi-framework support**: React (100%), Vue (~90%), Svelte (~85%), React Native (~70%)
|
||||
|
||||
## Component Status
|
||||
|
||||
- **Implemented**: ~127 fully functional components
|
||||
- **Stub Components**: ~34 need implementation
|
||||
- **Blocks**: 24+ viewport-sized templates
|
||||
- **AI Components**: 12 specialized AI/ML components
|
||||
- **3D Components**: 9 three-dimensional UI elements
|
||||
- **Animation Components**: 13 motion and transition components
|
||||
|
||||
## Migration Notes
|
||||
|
||||
### From shadcn/ui
|
||||
Hanzo UI is a drop-in replacement for shadcn/ui with additional components:
|
||||
```bash
|
||||
# Instead of
|
||||
npx shadcn-ui@latest add button
|
||||
|
||||
# Use
|
||||
npx @hanzo/ui@latest add button
|
||||
```
|
||||
|
||||
All shadcn/ui components work identically in Hanzo UI, plus you get access to 100+ additional components.
|
||||
|
||||
## Links
|
||||
|
||||
- **Website**: https://ui.hanzo.ai
|
||||
- **Documentation**: https://ui.hanzo.ai/docs
|
||||
- **GitHub**: https://github.com/hanzoai/ui
|
||||
- **npm Package**: https://www.npmjs.com/package/@hanzo/ui
|
||||
|
||||
## License
|
||||
|
||||
MIT License - see [LICENSE](LICENSE) for details
|
||||
@@ -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** — `fade-up` (FadeIn), `collapse`/`slide`/`fade` (shell/drawer), `drag`, `skeleton`/`pulse`/`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`; MIT (repo `LICENSE.md`). 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: MIT (repo `LICENSE.md`). `@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).
|
||||
+29
-49
@@ -12,9 +12,7 @@ This repository is a monorepo.
|
||||
|
||||
- We use [pnpm](https://pnpm.io) and [`workspaces`](https://pnpm.io/workspaces) for development.
|
||||
- We use [Turborepo](https://turbo.build/repo) as our build system.
|
||||
- Releases are semver-driven: bump a package's `version` in its `package.json` and
|
||||
merge to `main`. CI (`.github/workflows/publish.yml`) detects the version change
|
||||
and publishes that package to npm. One step, one source of truth.
|
||||
- We use [changesets](https://github.com/changesets/changesets) for managing releases.
|
||||
|
||||
## Structure
|
||||
|
||||
@@ -22,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 `shadcn-ui` package. |
|
||||
|
||||
## Development
|
||||
|
||||
@@ -81,58 +82,37 @@ You can use the `pnpm --filter=[WORKSPACE]` command to start the development pro
|
||||
1. To run the `ui.shadcn.com` website:
|
||||
|
||||
```bash
|
||||
pnpm --filter=v4 dev
|
||||
pnpm --filter=www dev
|
||||
```
|
||||
|
||||
2. To run the `shadcn` package:
|
||||
2. To run the `shadcn-ui` package:
|
||||
|
||||
```bash
|
||||
pnpm --filter=shadcn dev
|
||||
pnpm --filter=shadcn-ui dev
|
||||
```
|
||||
|
||||
## Running the CLI Locally
|
||||
|
||||
To run the CLI locally, you can follow the workflow:
|
||||
|
||||
1. Start by running the dev server:
|
||||
|
||||
```bash
|
||||
pnpm dev
|
||||
```
|
||||
|
||||
2. In another terminal tab, test the CLI by running:
|
||||
|
||||
```bash
|
||||
pnpm shadcn
|
||||
```
|
||||
|
||||
To test the CLI in a specific app, use a command like:
|
||||
|
||||
```bash
|
||||
pnpm shadcn <init | add | ...> -c ~/Desktop/my-app
|
||||
```
|
||||
|
||||
This workflow ensures that you are running the most recent version of the registry and testing the CLI properly in your local environment.
|
||||
|
||||
## Documentation
|
||||
|
||||
The documentation for this project is located in the `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
|
||||
```
|
||||
@@ -141,7 +121,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
|
||||
|
||||
@@ -180,9 +160,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 `shadcn-ui` package is a CLI for adding components to your project. You can find the documentation for the CLI [here](https://ui.shadcn.com/docs/cli).
|
||||
|
||||
Any changes to the CLI should be made in the `packages/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
|
||||
|
||||
|
||||
@@ -1,106 +0,0 @@
|
||||
# Hanzo Design System — Canonical Tokens
|
||||
|
||||
`@hanzo/ui` is the single source of truth for the shared Hanzo product look across
|
||||
the **Tailwind** apps (hanzo.chat, hanzo.app, hanzo console, commerce, hanzo-desktop).
|
||||
Change a value here; apps converge on it. This file is that source of truth for the
|
||||
three things that must read as **one product**: typography, the sidebar/panel system,
|
||||
and the dark-black palette.
|
||||
|
||||
> One library: **`@hanzo/ui@8`** (`pkg/ui`, on **`@hanzo/gui`**) IS the component
|
||||
> library — the cross-platform product/record layer every surface consumes.
|
||||
> **`@hanzo/ui-shadcn`** (`pkgs/ui`) is the legacy shadcn/Tailwind/Radix kit, kept
|
||||
> only for existing v5 consumers (pin `@hanzo/ui-shadcn@^5`; no new adoptions).
|
||||
> This file stays the source of truth for the *token values* (fonts, dark palette,
|
||||
> the sidebar glyph) both render.
|
||||
|
||||
---
|
||||
|
||||
## 1. Typography — Basel Grotesk + Geist Mono
|
||||
|
||||
| Role | Family | Notes |
|
||||
|------|--------|-------|
|
||||
| UI / body / display / heading (`sans`) | **Basel Grotesk** | Self-hosted. Book = weight **400**, Medium = weight **500**. |
|
||||
| code / data / mono (`mono`) | **Geist Mono** | `next/font/google` (`Geist_Mono`) or the geist CDN. |
|
||||
| Arabic / Hebrew (`--font-ar` / `--font-he`) | unchanged | i18n only — keep. |
|
||||
|
||||
**Dropped as defaults:** Geist Sans, DM Sans, Figtree, Inter, PT Sans, Roboto Mono.
|
||||
|
||||
Basel is a **licensed, non-Google** face — **self-host** the woff2/woff, do NOT use
|
||||
`next/font/google` for it. Canonical files (mirror lux.exchange):
|
||||
`Basel-Grotesk-Book.woff2/.woff` (400), `Basel-Grotesk-Medium.woff2/.woff` (500).
|
||||
|
||||
`@font-face` (weights 400/500, `font-display: swap`, `font-style: normal`):
|
||||
|
||||
```css
|
||||
@font-face {
|
||||
font-family: 'Basel';
|
||||
font-style: normal;
|
||||
font-weight: 400; /* Book; 500 = Medium */
|
||||
font-display: swap;
|
||||
src: url('.../Basel-Grotesk-Book.woff2') format('woff2'),
|
||||
url('.../Basel-Grotesk-Book.woff') format('woff');
|
||||
}
|
||||
```
|
||||
|
||||
Per-app adoption (converge the value, keep each app's own mechanism):
|
||||
- **@hanzo/ui / Next apps** → `next/font/local` for Basel (`--font-basel-sans`) +
|
||||
`next/font/google` `Geist_Mono` (`--font-geist-mono`). See `app/lib/fonts.ts`;
|
||||
tailwind `sans → var(--font-basel-sans)`, `mono → var(--font-geist-mono)`.
|
||||
- **Vite + Tailwind apps** (chat, launcher, desktop) → self-host Basel `@font-face`
|
||||
+ geist-mono CDN import; tailwind `fontFamily.sans = ['Basel', …]`,
|
||||
`mono = ['Geist Mono', …]`.
|
||||
- **Tamagui (console)** → Basel `@font-face` in globals + override the Tamagui
|
||||
`body`/`heading` font `family` to Basel; Geist Mono for `code`/`pre`.
|
||||
|
||||
---
|
||||
|
||||
## 2. Sidebar toggle icon — lucide `PanelLeft`
|
||||
|
||||
One glyph everywhere: lucide **`PanelLeft`** (the shadcn `SidebarTrigger` default).
|
||||
Where a directional open/close affordance is wanted, use the pair
|
||||
**`PanelLeftClose`** (expanded) / **`PanelLeft`** (collapsed). Never a hamburger,
|
||||
a magnifier, a directional arrow, or a bespoke panel SVG for the sidebar toggle.
|
||||
|
||||
- Icon: stroke-2, ~16–20px, `currentColor`.
|
||||
- Button: ghost/outline, square (`size-7`/`h-6 w-6`), subtle hover.
|
||||
|
||||
---
|
||||
|
||||
## 3. Sidebar + panels
|
||||
|
||||
| Spec | Value |
|
||||
|------|-------|
|
||||
| Sidebar width (expanded) | **16rem / 256px** (`SIDEBAR_WIDTH`) |
|
||||
| Sidebar width (collapsed / icon rail) | **3rem / 48px** (apps vary 48–70px) |
|
||||
| Sidebar / panel surface | resting **#0a0a0a** over the true-black page |
|
||||
| Border / separation | `border-border` — subtle white-alpha ~10% in dark (`border-r` / `border-l`) |
|
||||
| Item hover | subtle `white/5` |
|
||||
| Item active | monochrome `white/10` — **no colored accent** (the house style is monochrome) |
|
||||
| Right panel rail | `border-l border-border`, same surface, collapsible |
|
||||
|
||||
Prefer the `@hanzo/ui` `Sidebar` primitive (`pkgs/ui/primitives/sidebar.tsx`,
|
||||
`SidebarTrigger` → `PanelLeft`) where the app can consume it; otherwise match these
|
||||
classes/tokens.
|
||||
|
||||
---
|
||||
|
||||
## 4. Dark-black palette (true-black OLED)
|
||||
|
||||
The house dark theme is a **true-black** canvas (matches hanzo.ai marketing +
|
||||
hanzo.chat OLED), with a shallow surface-depth ladder for cards/panels and quiet
|
||||
hairline borders — never harsh pure-white on pure-black.
|
||||
|
||||
| Token | Value | Use |
|
||||
|-------|-------|-----|
|
||||
| Page background | **#000000** (`oklch(0 0 0)`) | body / canvas |
|
||||
| Surface / sidebar / panel (resting) | **#0a0a0a** | sidebars, panels, cards |
|
||||
| Press | **#050505** | pressed surface |
|
||||
| Elevated / hover | **#171717** | hover, raised card |
|
||||
| Border / divider | **rgba(255,255,255,0.10)** (≈ `#171717` opaque on black) | hairlines |
|
||||
| Foreground (primary text) | near-white **#ededf1** (`oklch(0.985)`) — not pure `#fff` | body text |
|
||||
| Muted / secondary text | `white/70` (≈ `#a1a1aa`) | secondary |
|
||||
|
||||
Keep each app's theme-token engine (CSS vars / Tailwind tokens / Tamagui `$color*`);
|
||||
converge the **values/usage** to the table above, don't rip the engine.
|
||||
|
||||
Reference: `pkgs/ui/style/hanzo-default-colors.css` (`.dark` / `.hanzo-ui-dark-theme`).
|
||||
-97
@@ -1,97 +0,0 @@
|
||||
# ui.hanzo.ai — the @hanzo/ui docs + component registry, served by the house
|
||||
# static server (ghcr.io/hanzoai/static, a Go binary), same as every other Hanzo
|
||||
# static site. Built on our own runners, never on a laptop and never by a
|
||||
# third-party builder.
|
||||
#
|
||||
# The app is a Next.js static export: `pnpm build` in app/ writes app/out, which
|
||||
# is the entire site — docs, the registry JSON the CLI reads, and the static
|
||||
# /api/registry/*.json index.
|
||||
FROM node:22 AS builder
|
||||
WORKDIR /src
|
||||
|
||||
ENV NEXT_TELEMETRY_DISABLED=1
|
||||
|
||||
# Publishable ingest key (pk-…), baked in at build because a static export has no
|
||||
# server to read config at runtime. Write-only and HMAC-verified to one org, so it
|
||||
# is safe in a public bundle; the deployment that builds decides which org the
|
||||
# site reports as.
|
||||
#
|
||||
# ONE name, end to end: KMS holds `deploy/PUBLISHABLE_KEY`, hanzo.yml declares it
|
||||
# as this image's build_secret, and the KMS name IS the build-arg name. NEXT_PUBLIC_
|
||||
# is added HERE because that prefix is what makes Next inline it — the app reads
|
||||
# process.env.NEXT_PUBLIC_PUBLISHABLE_KEY.
|
||||
#
|
||||
# Do NOT re-declare `ARG NEXT_PUBLIC_PUBLISHABLE_KEY` after the ENV below. A later
|
||||
# ARG of the same name shadows the ENV with its own (empty) default, and the build
|
||||
# stays green while the bundle ships blank — which is exactly how hanzo.chat 1.0.58
|
||||
# shipped a keyless site from a fully green run.
|
||||
#
|
||||
# Fail CLOSED, on BOTH ways this goes wrong.
|
||||
#
|
||||
# empty — builds, serves and looks correct while cloud answers
|
||||
# `401 ingest_key_required` for every anonymous pageview. The previous
|
||||
# `ARG …=""` default made that the normal outcome of an unattended
|
||||
# build, which is why no automated lane could ever publish a working
|
||||
# image.
|
||||
# `pk_…` — the OLDER key format. v5.7.6 shipped one, passed by hand on a local
|
||||
# `docker build`, and it is now dead: api.hanzo.ai 401s it on both the
|
||||
# fetch and beacon transports. A hand-passed key goes stale in silence,
|
||||
# so requiring the current `pk-` shape refuses the stale one outright.
|
||||
#
|
||||
# Neither failure is visible from outside the artifact, so refuse the artifact.
|
||||
ARG PUBLISHABLE_KEY
|
||||
ENV NEXT_PUBLIC_PUBLISHABLE_KEY=$PUBLISHABLE_KEY
|
||||
RUN case "$PUBLISHABLE_KEY" in \
|
||||
pk-*) : ;; \
|
||||
'') echo "PUBLISHABLE_KEY is empty - pass --build-arg PUBLISHABLE_KEY=<pk-...> (KMS deploy/PUBLISHABLE_KEY, env prod)" >&2; exit 1 ;; \
|
||||
*) echo "PUBLISHABLE_KEY is not a publishable key (expected a pk- prefix)" >&2; exit 1 ;; \
|
||||
esac
|
||||
# 300+ prerendered pages; the default heap is not enough.
|
||||
ENV NODE_OPTIONS=--max-old-space-size=8192
|
||||
|
||||
RUN corepack enable
|
||||
|
||||
COPY . .
|
||||
|
||||
# The lockfile is committed, so the build resolves exactly what was reviewed.
|
||||
RUN pnpm install --frozen-lockfile
|
||||
|
||||
# Workspace packages the app imports must be built first: their package.json
|
||||
# exports point at dist/. Only @hanzo/event qualifies -- .npmrc sets
|
||||
# link-workspace-packages=true and the lockfile resolves it to link:../pkgs/event.
|
||||
#
|
||||
# There used to be a `cd pkgs/ui` line here. pkgs/ui was @hanzo/ui-shadcn, deleted
|
||||
# in 5dbdb2943 when shadcn was consolidated to one home, and that commit did not
|
||||
# touch this file -- so every build since has run `cd` into a directory that does
|
||||
# not exist and died with exit code 2 before compiling anything. The app takes
|
||||
# @hanzo/ui from the registry now (npm:@hanzo/ui-shadcn@^5), not the workspace.
|
||||
RUN cd pkgs/event && pnpm build
|
||||
|
||||
# Builds the component registry, then the site (app/package.json build script).
|
||||
#
|
||||
# ...and then PROVES the key reached the client bundle. The gate above proves a
|
||||
# key was PASSED; only this proves it was INLINED. Those are different failures:
|
||||
# a rename on either side of `process.env.NEXT_PUBLIC_PUBLISHABLE_KEY` leaves the
|
||||
# build-arg intact and the bundle keyless, and a static export cannot report that
|
||||
# at runtime because there is no runtime.
|
||||
#
|
||||
# `&&`, never `;` — a `;` chain returns the LAST command's status, so a failed
|
||||
# build followed by a passing grep exits 0 and the image is published.
|
||||
RUN cd app && pnpm build && \
|
||||
if [ -z "${NEXT_PUBLIC_PUBLISHABLE_KEY}" ]; then \
|
||||
echo "ERROR: NEXT_PUBLIC_PUBLISHABLE_KEY is empty after a successful build." >&2; exit 1; \
|
||||
elif grep -rqF "${NEXT_PUBLIC_PUBLISHABLE_KEY}" out; then \
|
||||
echo "Build OK - ingest key inlined into app/out, verified"; \
|
||||
else \
|
||||
echo "ERROR: key supplied but NOT present in app/out - ui would ship unattributed" >&2; exit 1; \
|
||||
fi
|
||||
|
||||
# 0.5.1 serves a directory's index.html in place. On 0.4.1 every page 301'd to
|
||||
# an explicit /index.html, which leaks that filename into the address bar and
|
||||
# into the URLs Next builds for its route prefetches.
|
||||
FROM ghcr.io/hanzoai/static:0.5.2-amd64
|
||||
COPY --from=builder /src/app/out /public
|
||||
EXPOSE 3000
|
||||
# No -spa: the export writes a real index.html per route (trailingSlash), so a
|
||||
# missing path must 404 rather than silently render the home page.
|
||||
ENTRYPOINT ["/static", "-port", "3000", "-root", "/public"]
|
||||
@@ -1,458 +0,0 @@
|
||||
# UI/UX Improvements - Implementation Summary
|
||||
**Date**: 2025-11-13
|
||||
**Status**: Phase 1 Complete ✅
|
||||
|
||||
## 🎯 Overview
|
||||
|
||||
Following a comprehensive UI/UX review using Playwright automated testing, we've identified and begun implementing **12 high-impact improvements** to elevate Hanzo UI to world-class standards. This document tracks what's been completed and what remains.
|
||||
|
||||
---
|
||||
|
||||
## ✅ Completed Implementations
|
||||
|
||||
### 1. Skip Links for Accessibility (CRITICAL) ✅
|
||||
**File**: `app/components/skip-links.tsx`
|
||||
|
||||
**What it does**:
|
||||
- Provides keyboard navigation shortcuts for screen reader users
|
||||
- Meets WCAG Level A compliance requirements
|
||||
- Hidden by default, visible when focused (Tab key)
|
||||
|
||||
**Implementation**:
|
||||
```tsx
|
||||
<SkipLinks />
|
||||
// Renders:
|
||||
// - Skip to main content
|
||||
// - Skip to navigation
|
||||
```
|
||||
|
||||
**Impact**:
|
||||
- ✅ WCAG Level A compliant
|
||||
- ✅ Screen reader accessible
|
||||
- ✅ Keyboard navigation improved
|
||||
- ✅ Zero visual impact (only shows on focus)
|
||||
|
||||
**Integrated in**: `app/layout.tsx` - Active globally across all pages
|
||||
|
||||
---
|
||||
|
||||
### 2. Skeleton Loading Components ✅
|
||||
**File**: `app/components/skeleton-card.tsx`
|
||||
|
||||
**What it provides**:
|
||||
- `<SkeletonCard />` - Single card skeleton
|
||||
- `<SkeletonCardGrid />` - Grid of skeleton cards
|
||||
- `<SkeletonList />` - List view skeleton
|
||||
- `<SkeletonTable />` - Table skeleton
|
||||
|
||||
**Usage Example**:
|
||||
```tsx
|
||||
import { Suspense } from 'react'
|
||||
import { SkeletonCardGrid } from '@/components/skeleton-card'
|
||||
|
||||
<Suspense fallback={<SkeletonCardGrid count={6} />}>
|
||||
<ComponentGallery />
|
||||
</Suspense>
|
||||
```
|
||||
|
||||
**Impact**:
|
||||
- ⏩ Reduces perceived load time
|
||||
- 🎨 Professional loading states
|
||||
- ♿ Better accessibility (users know content is loading)
|
||||
- 📱 Works on all devices
|
||||
|
||||
**Next Step**: Integrate into:
|
||||
- `/blocks` page (for block gallery)
|
||||
- `/docs/components` page (for component list)
|
||||
- `/builder` page (for block library panel)
|
||||
|
||||
---
|
||||
|
||||
### 3. Enhanced Global Layout ✅
|
||||
**File**: `app/app/layout.tsx`
|
||||
|
||||
**Changes**:
|
||||
- ✅ Skip links added at the top of the component tree
|
||||
- ✅ Proper import order maintained
|
||||
- ✅ Zero breaking changes to existing functionality
|
||||
|
||||
**Before/After**:
|
||||
```tsx
|
||||
// Before
|
||||
<ActiveThemeProvider>
|
||||
<div vaul-drawer-wrapper="">
|
||||
{children}
|
||||
</div>
|
||||
</ActiveThemeProvider>
|
||||
|
||||
// After
|
||||
<ActiveThemeProvider>
|
||||
<SkipLinks />
|
||||
<div vaul-drawer-wrapper="">
|
||||
{children}
|
||||
</div>
|
||||
</ActiveThemeProvider>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 📋 Ready to Integrate (Created, Not Yet Used)
|
||||
|
||||
### Components Available for Use
|
||||
|
||||
1. **Skeleton Components** (`app/components/skeleton-card.tsx`)
|
||||
- Ready to wrap async content
|
||||
- Just import and use in Suspense boundaries
|
||||
|
||||
2. **Skip Links** (`app/components/skip-links.tsx`)
|
||||
- Already active globally
|
||||
- Need to add `id="main-content"` to main content areas
|
||||
|
||||
---
|
||||
|
||||
## 🚧 Next Phase - Ready for Implementation
|
||||
|
||||
### High Priority (Week 1-2)
|
||||
|
||||
#### 1. Add ID Attributes for Skip Links
|
||||
**Files to modify**:
|
||||
- `app/(app)/layout.tsx` - Add `id="main-content"` to main
|
||||
- `app/components/site-header.tsx` - Add `id="navigation"` to nav
|
||||
|
||||
```tsx
|
||||
// In layout
|
||||
<main id="main-content" className="flex-1">
|
||||
{children}
|
||||
</main>
|
||||
|
||||
// In header
|
||||
<nav id="navigation">
|
||||
<MainNav />
|
||||
</nav>
|
||||
```
|
||||
|
||||
**Effort**: 5 minutes
|
||||
**Impact**: High - Completes skip links functionality
|
||||
|
||||
---
|
||||
|
||||
#### 2. Integrate Skeleton Loaders
|
||||
**Target Pages**:
|
||||
|
||||
**A. Blocks Page** (`app/(app)/blocks/page.tsx`)
|
||||
```tsx
|
||||
import { Suspense } from 'react'
|
||||
import { SkeletonCardGrid } from '@/components/skeleton-card'
|
||||
|
||||
export default function BlocksPage() {
|
||||
return (
|
||||
<Suspense fallback={<SkeletonCardGrid count={9} />}>
|
||||
<BlockGallery />
|
||||
</Suspense>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
**B. Components Page** (`app/(app)/docs/components/page.tsx`)
|
||||
```tsx
|
||||
<Suspense fallback={<SkeletonList count={10} />}>
|
||||
<ComponentList />
|
||||
</Suspense>
|
||||
```
|
||||
|
||||
**C. Builder Page** (`app/(app)/builder/page.tsx`)
|
||||
```tsx
|
||||
// In the component library panel
|
||||
<Suspense fallback={<SkeletonCardGrid count={6} />}>
|
||||
<BlockLibrary />
|
||||
</Suspense>
|
||||
```
|
||||
|
||||
**Effort**: 30 minutes
|
||||
**Impact**: High - Dramatically improves perceived performance
|
||||
|
||||
---
|
||||
|
||||
#### 3. Performance Optimizations (CRITICAL)
|
||||
**Current**: 14.7s load time
|
||||
**Target**: <3s load time
|
||||
|
||||
**Action Items**:
|
||||
a. **Image Optimization**
|
||||
```bash
|
||||
# Convert images to WebP
|
||||
pnpm add sharp
|
||||
# Add next.config.js optimization
|
||||
images: {
|
||||
formats: ['image/webp', 'image/avif'],
|
||||
}
|
||||
```
|
||||
|
||||
b. **Code Splitting**
|
||||
```tsx
|
||||
// Dynamic imports for heavy components
|
||||
const HeavyComponent = dynamic(() => import('./heavy-component'), {
|
||||
loading: () => <SkeletonCard />,
|
||||
})
|
||||
```
|
||||
|
||||
c. **Lazy Loading**
|
||||
```tsx
|
||||
// Below-the-fold content
|
||||
import { lazy } from 'react'
|
||||
const Footer = lazy(() => import('./footer'))
|
||||
```
|
||||
|
||||
**Effort**: 4-6 hours
|
||||
**Impact**: CRITICAL - 79% faster page loads
|
||||
|
||||
---
|
||||
|
||||
### Medium Priority (Week 2-3)
|
||||
|
||||
#### 4. Toast Notification System
|
||||
**Package**: Already installed (`sonner`)
|
||||
**File**: `app/registry/default/ui/sonner.tsx` (already exists!)
|
||||
|
||||
**Usage**:
|
||||
```tsx
|
||||
import { toast } from 'sonner'
|
||||
|
||||
// Success
|
||||
toast.success('Component copied!')
|
||||
|
||||
// Error
|
||||
toast.error('Failed to load', {
|
||||
action: {
|
||||
label: 'Retry',
|
||||
onClick: () => retry(),
|
||||
},
|
||||
})
|
||||
```
|
||||
|
||||
**Effort**: 1 hour (just replace alert() calls)
|
||||
**Impact**: Medium - Better UX for notifications
|
||||
|
||||
---
|
||||
|
||||
#### 5. Command Palette (⌘K)
|
||||
**Component**: Use existing `app/components/command-menu.tsx`
|
||||
|
||||
**Enhancement**: Add keyboard shortcut hint
|
||||
```tsx
|
||||
<div className="flex items-center gap-2">
|
||||
<Search className="h-4 w-4" />
|
||||
<span>Search</span>
|
||||
<kbd className="ml-auto">⌘K</kbd>
|
||||
</div>
|
||||
```
|
||||
|
||||
**Effort**: 2 hours
|
||||
**Impact**: Medium - Power user feature
|
||||
|
||||
---
|
||||
|
||||
#### 6. Enhanced Hero Section
|
||||
**File**: `app/(app)/page.tsx`
|
||||
|
||||
**Improvements**:
|
||||
- ✨ Animated gradient background
|
||||
- 🎯 More prominent CTAs
|
||||
- 💫 Subtle animations (fade-in-up)
|
||||
- 🏆 Trust indicators
|
||||
|
||||
**Code Available**: See `docs/UX_ENHANCEMENT_PLAN.md` Section 4
|
||||
|
||||
**Effort**: 3-4 hours
|
||||
**Impact**: High - First impression matters
|
||||
|
||||
---
|
||||
|
||||
### Lower Priority (Week 4+)
|
||||
|
||||
7. **Builder Undo/Redo** (8 hours)
|
||||
8. **Keyboard Shortcuts Panel** (4 hours)
|
||||
9. **Empty State Components** (3 hours)
|
||||
10. **Micro-interactions** (4 hours)
|
||||
11. **Component Card Enhancements** (6 hours)
|
||||
12. **Drag Indicators** (4 hours)
|
||||
|
||||
---
|
||||
|
||||
## 📊 Impact Metrics
|
||||
|
||||
### Current Status
|
||||
| Metric | Before | After Phase 1 | Target | Progress |
|
||||
|--------|--------|---------------|--------|----------|
|
||||
| Accessibility | Good | WCAG A ✅ | WCAG AAA | 33% |
|
||||
| Loading UX | None | Skeletons ✅ | Complete | 50% |
|
||||
| Performance | 14.7s | TBD | <3s | 0% |
|
||||
| Mobile UX | Good | Good ✅ | Excellent | 80% |
|
||||
|
||||
### After Full Implementation
|
||||
| Metric | Target | Improvement |
|
||||
|--------|--------|-------------|
|
||||
| Page Load | <3s | -11.7s (79% faster) |
|
||||
| Accessibility | WCAG AAA | +2 levels |
|
||||
| User Engagement | +50% | Better UX |
|
||||
| Bounce Rate | -20% | Faster loads |
|
||||
|
||||
---
|
||||
|
||||
## 🧪 Testing
|
||||
|
||||
### Automated Tests Available
|
||||
Run comprehensive UX review anytime:
|
||||
```bash
|
||||
pnpm exec playwright test tests/ux-review.spec.ts --headed
|
||||
```
|
||||
|
||||
**What it tests**:
|
||||
- ✅ Skip links presence (now passing!)
|
||||
- ✅ Mobile navigation
|
||||
- ✅ Dark/light mode
|
||||
- ✅ Accessibility features
|
||||
- ⏱️ Performance metrics
|
||||
- 📸 Visual regression (screenshots)
|
||||
|
||||
### Manual Testing Checklist
|
||||
- [ ] Tab through page - skip links should appear
|
||||
- [ ] Press Tab on any page → "Skip to main content" appears
|
||||
- [ ] Click skip link → jumps to main content
|
||||
- [ ] Skeleton loaders appear while content loads
|
||||
- [ ] No layout shift when content replaces skeletons
|
||||
|
||||
---
|
||||
|
||||
## 📚 Documentation Created
|
||||
|
||||
1. **UX Enhancement Plan**: `docs/UX_ENHANCEMENT_PLAN.md`
|
||||
- Complete roadmap with code examples
|
||||
- 12 improvements prioritized
|
||||
- Success metrics defined
|
||||
|
||||
2. **UX Review Summary**: `tests/reports/UX_REVIEW_SUMMARY.md`
|
||||
- Detailed findings from Playwright tests
|
||||
- Current vs target performance
|
||||
- Visual analysis results
|
||||
|
||||
3. **Test Suite**: `tests/ux-review.spec.ts`
|
||||
- 13 comprehensive tests
|
||||
- Reusable for ongoing monitoring
|
||||
- Screenshots for visual tracking
|
||||
|
||||
4. **This Document**: `IMPROVEMENTS_IMPLEMENTED.md`
|
||||
- Track implementation progress
|
||||
- Integration guides
|
||||
- Priority roadmap
|
||||
|
||||
---
|
||||
|
||||
## 🚀 Quick Wins (Do These First!)
|
||||
|
||||
### 5-Minute Tasks
|
||||
1. ✅ Skip links - **DONE**
|
||||
2. Add ID attributes (`id="main-content"`, `id="navigation"`)
|
||||
3. Import skeletons where needed
|
||||
|
||||
### 30-Minute Tasks
|
||||
4. Wrap async content in Suspense with skeleton fallbacks
|
||||
5. Replace alert() with toast notifications
|
||||
6. Add ⌘K hint to search
|
||||
|
||||
### 1-Hour Tasks
|
||||
7. Hero section CTA improvements
|
||||
8. Add loading="lazy" to below-fold images
|
||||
|
||||
---
|
||||
|
||||
## 💡 Tips for Implementation
|
||||
|
||||
### Using Skeleton Loaders
|
||||
```tsx
|
||||
// Pattern 1: Simple async component
|
||||
<Suspense fallback={<SkeletonCard />}>
|
||||
<AsyncComponent />
|
||||
</Suspense>
|
||||
|
||||
// Pattern 2: Grid of items
|
||||
<Suspense fallback={<SkeletonCardGrid count={6} />}>
|
||||
<ItemGrid />
|
||||
</Suspense>
|
||||
|
||||
// Pattern 3: With error boundary
|
||||
<ErrorBoundary fallback={<ErrorState />}>
|
||||
<Suspense fallback={<SkeletonList />}>
|
||||
<DataList />
|
||||
</Suspense>
|
||||
</ErrorBoundary>
|
||||
```
|
||||
|
||||
### Performance Best Practices
|
||||
```tsx
|
||||
// 1. Dynamic imports for heavy components
|
||||
const Chart = dynamic(() => import('./chart'), {
|
||||
ssr: false,
|
||||
loading: () => <SkeletonCard />,
|
||||
})
|
||||
|
||||
// 2. Image optimization
|
||||
import Image from 'next/image'
|
||||
<Image
|
||||
src="/image.jpg"
|
||||
alt="Description"
|
||||
width={800}
|
||||
height={600}
|
||||
loading="lazy"
|
||||
placeholder="blur"
|
||||
/>
|
||||
|
||||
// 3. Code splitting
|
||||
const ModalContent = lazy(() => import('./modal-content'))
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🎯 Next Steps
|
||||
|
||||
**Immediate (This Week)**:
|
||||
1. Add ID attributes to complete skip links
|
||||
2. Integrate skeletons on blocks page
|
||||
3. Test accessibility with screen reader
|
||||
|
||||
**Short-term (Next 2 Weeks)**:
|
||||
4. Performance optimization sprint
|
||||
5. Hero section redesign
|
||||
6. Toast notifications integration
|
||||
|
||||
**Long-term (Month 2)**:
|
||||
7. Builder enhancements (undo/redo, drag indicators)
|
||||
8. Keyboard shortcuts
|
||||
9. Polish and micro-interactions
|
||||
|
||||
---
|
||||
|
||||
## 📞 Support
|
||||
|
||||
**Questions?** See the detailed plans:
|
||||
- `docs/UX_ENHANCEMENT_PLAN.md` - Full implementation guide
|
||||
- `tests/reports/UX_REVIEW_SUMMARY.md` - Test findings
|
||||
- `tests/ux-review.spec.ts` - Test suite source
|
||||
|
||||
**Running Tests**:
|
||||
```bash
|
||||
# Full UX review
|
||||
pnpm test:e2e tests/ux-review.spec.ts
|
||||
|
||||
# Just accessibility tests
|
||||
pnpm test:e2e tests/ux-review.spec.ts -g "Accessibility"
|
||||
|
||||
# With UI (see what's happening)
|
||||
pnpm test:e2e tests/ux-review.spec.ts --headed
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
*Last updated: 2025-11-13*
|
||||
*Phase: 1 of 4 Complete*
|
||||
*Next review: After Week 1 integrations*
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2023 hanzo
|
||||
Copyright (c) 2023 Lux Partners Limited
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
|
||||
@@ -1,580 +0,0 @@
|
||||
# @hanzo/ui — LLM context
|
||||
|
||||
**What this is.** The React component library for AI applications: 161+
|
||||
components, 24+ blocks, two themes, and a single typed import surface, all on ONE
|
||||
substrate (`@hanzo/gui`) so the same import runs on web, native and desktop.
|
||||
Published as `@hanzo/ui` (v8) on npm. Docs at https://ui.hanzo.ai. Dev port: 3003.
|
||||
|
||||
**Canonical role.** This is the canonical impl repo for Hanzo's web UI kit —
|
||||
frontend components, not an SDK. It sits alongside the two SDK lines (full cloud
|
||||
SDK generated from OpenAPI in `hanzo-<lang>/sdk` + wrapper in `hanzoai/<lang>-sdk`;
|
||||
AI/agents lib `hanzo` in `hanzoai/python-sdk` flagship, `@hanzo/ai` in `hanzo-js/ai`).
|
||||
`@hanzo/event` (telemetry, `POST /v1/event`) lives here in `pkgs/event`. DRY: one
|
||||
impl, one place — link out, never duplicate.
|
||||
|
||||
**Brand rules (hard).**
|
||||
- Never call Hanzo an "LLM gateway" or position it against LiteLLM — it is a full
|
||||
AI SDK / AI cloud, not a proxy. Purge that framing on sight.
|
||||
- Paths are `/v1/…` only — never an `/api/` prefix.
|
||||
- Zen models are our own family — never name upstream models.
|
||||
- Voice: "Hanzo — the Open AI Cloud." Developer-first, crisp, no emoji-spam.
|
||||
|
||||
**Install / run.**
|
||||
```bash
|
||||
pnpm add @hanzo/ui # consume
|
||||
# dev:
|
||||
pnpm install && pnpm build:registry && pnpm dev # registry MUST build before app
|
||||
```
|
||||
|
||||
**Key entry points.** `pkg/ui/` (core lib + v8 subpaths: /product /data /canvas
|
||||
/dashboard /usage /gitops) · `app/registry/{default,new-york}/`
|
||||
(component SOURCE OF TRUTH) · `pkgs/*` (auto-published `@hanzo/*` packages) ·
|
||||
`packages/shadcn/` (CLI) · `app/content/docs/` (MDX docs). Publish = bump a
|
||||
package `version` + merge to main (`.github/workflows/publish.yml`).
|
||||
|
||||
**Spec / more context.** Canonical SDK + docs model: `~/work/hanzo/SDK-ARCHITECTURE.md`.
|
||||
Detailed engineering notes (build order, import surface, telemetry, upstream sync,
|
||||
gotchas) follow below.
|
||||
|
||||
---
|
||||
|
||||
## v8 — the canonical `@hanzo/ui` (`pkg/ui`)
|
||||
|
||||
`@hanzo/ui@8` (`pkg/ui`) is THE Hanzo component library, and there is ONE
|
||||
substrate: every component renders through `@hanzo/gui` (Tamagui) primitives on
|
||||
the `@hanzo/tokens` scale, so one import works on web, native (expo) and desktop
|
||||
(Tauri). The Radix + Tailwind surface it used to ship alongside is gone — it
|
||||
lives on as its own package, `@hanzo/shadcn`, and `@hanzo/ui` no longer depends
|
||||
on it, on any `@radix-ui/*` package, on cva, cmdk or sonner.
|
||||
|
||||
`@hanzo/ui-shadcn` (`pkgs/ui`, v5.x) is the legacy standalone package —
|
||||
superseded, being retired.
|
||||
|
||||
### Layout
|
||||
|
||||
```
|
||||
pkg/ui/src/
|
||||
core/ design core: cn.ts (clsx+tailwind-merge),
|
||||
tokens.ts (re-export of @hanzo/tokens), fonts.ts (Geist vars)
|
||||
root.tsx <Hanzo> — the root. Carries the gui config AND the stylesheet.
|
||||
gallery.tsx EVERY component, once, in every variant. The one list.
|
||||
theme.css SELF-CONTAINED token CSS vars + Geist Sans/Mono — the identity
|
||||
backends/gui/ THE component surface on @hanzo/gui. index.ts is its manifest.
|
||||
product/ the product/app layer (charts, PageHeader, ComboBox, …)
|
||||
models/ the unified ModelSelector + catalog helpers
|
||||
primitives/ GENERATED per-member entrypoints (scripts/gen-primitives.mjs)
|
||||
index.ts root barrel = the component surface + cn
|
||||
```
|
||||
|
||||
### Out of the box — the package carries its own config and its own CSS
|
||||
|
||||
```tsx
|
||||
import { Hanzo, Button } from '@hanzo/ui'
|
||||
<Hanzo><Button>Ship</Button></Hanzo>
|
||||
```
|
||||
|
||||
That is the entire setup. No `gui.config.ts`, no CSS import, no generator script.
|
||||
Three things used to be each app's job:
|
||||
|
||||
1. **The stylesheet.** gui compiles a style prop to an atomic class the first
|
||||
time something RENDERS it, so the sheet does not exist until a render has
|
||||
happened — which is why every app ran a `gen-gui-css.mjs` of its own. hanzo.app
|
||||
never did: it shipped 103 `_bg-` classes and 26 `_dsp-` classes against a
|
||||
stylesheet containing ZERO of either, every gui-styled element unstyled in
|
||||
production, green build throughout. The render happens at OUR publish time now
|
||||
(`scripts/gen-css.mjs` renders `src/gallery.tsx` in both themes and writes
|
||||
`dist/styles.css` — 381 KB, 35 KB gzipped, 340 atomic selectors), and
|
||||
`<Hanzo>` imports it. Styles gui generates at RUNTIME for props we could not
|
||||
know at publish time still reach the document through `insertStyleRules`;
|
||||
the shipped sheet is what makes the FIRST paint and every SSR/static render
|
||||
correct.
|
||||
|
||||
2. **The config.** `<Hanzo>` passes `config` from `gui-config.ts` to
|
||||
`GuiProvider` — as a VALUE, never a bare `import './gui-config'`. Vite 8
|
||||
(rolldown) ignores package.json `sideEffects` ARRAYS outright: with any array
|
||||
the registration is dropped and the first render dies on "Missing hanzogui
|
||||
config"; only `sideEffects: true` keeps it, and that costs +63% bundle
|
||||
(404 KB → 661 KB measured). Correctness does not live in bundler metadata.
|
||||
|
||||
3. **The theme.** gui throws `Missing theme.` for any component with no root
|
||||
theme context, so a root is structurally required — there is no version of
|
||||
this with no root at all. Forgetting `<Hanzo>` is therefore a hard crash on
|
||||
first paint, never a silently unstyled page.
|
||||
|
||||
`theme.css` is dark-first at `:root` (it used to claim dark-first while shipping
|
||||
LIGHT at `:root`, so an app that mounted the dark default and read `--background`
|
||||
got white). `.light` retunes, and both answer to gui's own `.t_light`/`.t_dark`
|
||||
that `<Hanzo>` stamps on the body — one theme, named the same by the CSS custom
|
||||
properties and the component tokens.
|
||||
|
||||
### Three tests, one list of components
|
||||
|
||||
`src/gallery.tsx` is the specification of "what this package has to style", and
|
||||
all three layers render THAT — a second copy of the list is how a component gets
|
||||
styled by one and missed by another.
|
||||
|
||||
| Layer | Command | What it catches |
|
||||
|---|---|---|
|
||||
| `src/styles.test.tsx` | `pnpm test:unit` | every atomic class the gallery renders vs every class `dist/styles.css` defines a rule for. Not "the intersection is large" — TOTAL. Catches a stale sheet. |
|
||||
| `src/backends/gui/render.test.tsx` | `pnpm test:unit` | the surface mounts under the real provider; a component that throws on first paint fails. |
|
||||
| `test/consumer.spec.ts` | `pnpm test:consumer` | packs the tarball, installs it into a temp app OUTSIDE the repo (never a workspace link — that hides `files`/`exports`/`workspace:*` defects), builds, serves, and asserts COMPUTED styles + screenshots at 390 and 1280 in both themes. |
|
||||
|
||||
The consumer spec also fails on a solid-white border (the `@hanzo/design`
|
||||
`border-card: var(--white…)` defect — borders are low-alpha hairlines) and on any
|
||||
element that has a text child and a zero-height box.
|
||||
|
||||
### House rules for a component
|
||||
|
||||
- Style through gui props and theme tokens (`$background`, `$color12`,
|
||||
`$borderColor`) — never a utility class string, never a hard-coded font.
|
||||
- Touch targets meet the 44px floor via `hitSlop`, never via padding.
|
||||
- Behaviour (focus, portalling, keyboard, a11y) comes from the matching
|
||||
`@hanzogui/*` primitive; nothing reimplements it.
|
||||
- Free-form text children go through `ink()`; `data-slot` markers through
|
||||
`slot()`. One helper each, one place.
|
||||
- Module scope stays side-effect free — `forwardRef`/`createContext` calls carry
|
||||
`/* @__PURE__ */` and nothing assigns `displayName`, so importing one symbol
|
||||
never drags a neighbour in. A one-symbol import bundles ~4.7KB against ~29KB
|
||||
for the whole barrel.
|
||||
- Utility classes carry `hz-`. This sheet used to claim `.row`, `.skeleton`,
|
||||
`.fade`, `.mono`, `.drag` and `.tnum` at the document level, in a package an
|
||||
app imports once at its root; an app with its own `.row` got no warning, it got
|
||||
whichever rule the cascade preferred. The unprefixed selectors survive as
|
||||
aliases on the same rules for one minor version and are **REMOVED IN 8.1.0**.
|
||||
Nothing here emits them — `styles.test.tsx` scans every `className=` literal in
|
||||
`src/` and fails on an unprefixed one. `glass`/`elevation-N` are the one family
|
||||
still bare: they are an API VALUE (`glass(3).className`), not a typed literal,
|
||||
so they move on their own change.
|
||||
- A prop must actually arrive. gui is not the DOM, and two measured cases prove
|
||||
it in both directions: **`name` is gui's OWN prop** (it names a styled component
|
||||
and a theme) and is consumed before it reaches the element, so a `name` on a
|
||||
field type-checks and renders nothing; and gui **drops `secureTextEntry` on
|
||||
web**, which is why masking needs BOTH spellings via `masked()` in
|
||||
`backends/gui/mask.ts` and why `<Input type="password">` rendered passwords in
|
||||
PLAIN TEXT until 8.0.61 (the wrapper destructured `type` out and never
|
||||
forwarded it, next to an eye offering to reveal what was already visible).
|
||||
Render it and read the markup before you believe a prop works.
|
||||
|
||||
### Subpaths
|
||||
|
||||
| Subpath | What |
|
||||
|---|---|
|
||||
| `@hanzo/ui` | the component API: Button, Badge, Card*, Checkbox, Dialog*, DropdownMenu*, Input, Toaster, Avatar*, Tabs*, Select*, Tooltip*, Popover*, Command*, Collapsible*, Resizable*, ScrollArea, Slider, Switch, Progress, Separator, Label, Textarea, AspectRatio — + `cn` (the product layer is kept off root, at `/product`) |
|
||||
| `@hanzo/ui/components` | alias of the root surface, for hosts that shim the package through a `declare module` |
|
||||
| `@hanzo/ui/product` | the product/app layer: charts, metrics, PageHeader, StatusTag, EmptyState, ComboBox, SlideOver, Toast, Reorder, Field |
|
||||
| `@hanzo/ui/models` | ModelSelector + fetchModelCatalog + catalog helpers |
|
||||
| `@hanzo/ui/core` · `/tokens` | cn, Geist font vars, the @hanzo/tokens color/theme/radii/spacing scale |
|
||||
| `@hanzo/ui/theme.css` | the design tokens alone (custom properties + Geist + touch/elevation) |
|
||||
| `@hanzo/ui/styles.css` | the COMPLETE sheet — tokens + motion + the generated gui atomic/theme CSS. `<Hanzo>` imports it, so an app never has to |
|
||||
| `@hanzo/ui/gallery` | every component, once — what the generator, the unit test and the consumer test all render |
|
||||
| `@hanzo/ui/primitives/<Member>` | per-member entrypoints (for hosts that modularize `@hanzo/ui` imports) |
|
||||
| `@hanzo/ui/data` | `@hanzo/data`: RecordsView, DataTable, typed field editors |
|
||||
| `@hanzo/ui/{canvas,dashboard,usage,gitops}` | the optional-peer kits (each re-exports its home package) |
|
||||
| `@hanzo/ui/product/*` · `/primitives/*` | deep imports — one module without its barrel |
|
||||
| `@hanzo/ui/product/pure` | the product layer's RULES with none of the layer (below) |
|
||||
| `@hanzo/ui/product/theme-toggle-next` | the `@hanzogui/next-theme` binding, off the barrel on purpose (below) |
|
||||
| `@hanzo/ui/css` | `substitute()` — resolve a `var()` chain, for tests jsdom cannot answer (below) |
|
||||
|
||||
Everything ships COMPILED from `dist` — every `exports` target is a real file in
|
||||
the tarball, including `theme.css` and all 90 `primitives/*` entrypoints.
|
||||
`src/dist.test.ts` asserts that against the built output, wildcards included; a
|
||||
subpath pointing at a file tsc never emitted is invisible from source.
|
||||
|
||||
### Three doors that exist because the barrel is not one
|
||||
|
||||
`@hanzo/ui/product` mounts the whole gui runtime to give you one component, and
|
||||
for three kinds of caller that is not a cost — it is a wall.
|
||||
|
||||
**`@hanzo/ui/product/pure` — the rules, without the layer.** `pages()`,
|
||||
`masked()`, `displayName()`, `tone()`, `orgScope`, `filterOptions`,
|
||||
`resolveBrand` and the wordmark geometry. Every module it re-exports imports
|
||||
NOTHING (`src/dist.test.ts` asserts the closure is empty) and is on
|
||||
`postbuild.mjs`'s `DATA` list, so none is stamped `'use client'` — a stamped
|
||||
module is a client REFERENCE on React's server layer, and calling `pages()`
|
||||
through one in a server component throws instead of paging. It loads under a
|
||||
bare `require()` with no transform and no DOM; the test proves it in a child
|
||||
node process rather than under vitest, which has vite's transform already
|
||||
installed and would prove nothing.
|
||||
|
||||
The components import these same modules, so there is one definition and not a
|
||||
testable copy of a shipped one.
|
||||
|
||||
**`@hanzo/ui/product/theme-toggle-next` — Next, quarantined.**
|
||||
`@hanzogui/next-theme`'s provider imports `next/script`, so the product barrel's
|
||||
one `export { ThemeToggleNext }` line put Next in the graph of every Vite,
|
||||
Express and Tauri host — the hosts this layer promises to run on. A barrel
|
||||
re-export is a static edge no bundler can split. It is off the barrel; `<ThemeToggle />`
|
||||
with no props still reaches it by dynamic import and degrades when next-theme is
|
||||
absent, and `dist.test.ts` asserts BOTH — no static edge, and the dynamic one
|
||||
still there, because a test that only asserted the absence would pass on a
|
||||
deleted feature. `next` is now an OPTIONAL peer here: next-theme requires it and
|
||||
nothing in this package admitted that.
|
||||
|
||||
**`@hanzo/ui/css` — `substitute(value, vars?)`.** jsdom does not resolve
|
||||
`var()`; it hands a test the text verbatim. The theme rungs are
|
||||
`var(--border, rgb(255 255 255 / .10))` on purpose (follow the live cascade
|
||||
where design's sheet is mounted, keep the audited literal where it is not), so
|
||||
every consumer trying to assert a border's contrast compared a colour to a
|
||||
function call. With no `vars` map the answer is exact rather than approximate:
|
||||
jsdom mounts no design sheet, so the fallback IS what a browser computes. It is
|
||||
NOT part of `@hanzo/ui/core` — that subpath is ESM-only because @hanzo/design
|
||||
publishes no `require` condition, and a jest consumer is the caller that needs
|
||||
this. Importing nothing is what lets it ship both formats.
|
||||
|
||||
### One DropdownMenu
|
||||
|
||||
There is one `DropdownMenu`, with one API. It is the compound surface (Trigger,
|
||||
Content, Item, CheckboxItem, RadioItem, Label, Separator, Shortcut, Group,
|
||||
Portal, Sub*, RadioGroup) AND it accepts the declarative `trigger` + `items`
|
||||
spec, which it renders through those very same parts. `@hanzo/ui` and
|
||||
`@hanzo/ui/product` export the same component; there is no second shape.
|
||||
|
||||
### modularizeImports support
|
||||
|
||||
`scripts/gen-primitives.mjs` reads the gui backend barrel and emits one
|
||||
`src/primitives/<Member>.tsx` per exported value (re-export from the backend).
|
||||
This makes `@hanzo/ui/primitives/Button` etc. resolve, so a host whose
|
||||
`next.config` rewrites `@hanzo/ui` → `@hanzo/ui/primitives/{{member}}` works
|
||||
unchanged. Re-run `pnpm gen:primitives` after changing the surface.
|
||||
|
||||
### Build — plain `tsc`, one file in, one file out
|
||||
|
||||
There is no bundler. `pnpm build` is two `tsc` passes, `scripts/postbuild.mjs`,
|
||||
then `scripts/gen-css.mjs` (which renders the gallery through vite's SSR pipeline
|
||||
to harvest `config.getCSS()` into `dist/styles.css` — the slow step, ~2 min):
|
||||
|
||||
| Pass | Config | Emits |
|
||||
|---|---|---|
|
||||
| ESM + types | `tsconfig.build.json` | `dist/**/*.js`, `.d.ts`, `.js.map`, `.d.ts.map` |
|
||||
| CJS | `tsconfig.cjs.json` (`--noCheck`) | `dist-cjs/**/*.js`, folded into `dist/**/*.cjs` |
|
||||
|
||||
`postbuild.mjs` does the only two things `tsc` will not: it resolves every
|
||||
relative specifier to a fully-specified path (`./button` → `./button.js`,
|
||||
`./x` → `./x/index.js`; `.cjs` on the CJS half) so Node ESM and strict bundlers
|
||||
resolve, and it prepends `'use client'` to every emitted module — the whole
|
||||
library is client-side @hanzo/gui UI and Next's flight-client loader wants the
|
||||
directive first. It is prepended without a newline so source-map lines hold.
|
||||
|
||||
**`@hanzo/data` runs this same script**, with its package root and its own data
|
||||
modules as arguments (`node ../ui/scripts/postbuild.mjs . theme,table/logic,…`).
|
||||
It has the same two formats and the same two problems, and a second copy of the
|
||||
file would be a second place to get the barrel rule wrong. That package used to
|
||||
point its `exports` at `src/index.ts` and ship raw TSX — every consumer had to
|
||||
transpile it, and `require('@hanzo/data')` could not load at all, which is one of
|
||||
the two edges that broke `require('@hanzo/ui')`. It emits `dist/` now, both
|
||||
formats, from 1.2.2.
|
||||
|
||||
The output is UNBUNDLED and mirrors `src/` one-for-one, so a consumer importing
|
||||
one symbol pulls one module. A bundler here would be actively harmful: tsup's
|
||||
code splitting emitted 11 shared `chunk-*.js`, and importing `Button` alone
|
||||
dragged in `chunk-RCMDRI6V.js` (48K of source). Measured with esbuild, `import
|
||||
{ Button } from '@hanzo/ui'` costs 4717 bytes bundled from tsup output vs 2021
|
||||
from `tsc` output. Dropping `rollup-plugin-dts` (tsup's `dts` worker) is also
|
||||
what makes the package build on TypeScript 7, which it cannot do otherwise.
|
||||
|
||||
Everything under `src/` is emitted, so every `exports` subpath resolves by
|
||||
construction — no hand-maintained entry list to drift.
|
||||
|
||||
NOTE: `pkg/ui` (singular) sits OUTSIDE the `pkgs/*` pnpm workspace, installs
|
||||
standalone, and publishes via the maintainer flow, not `publish.yml`. Because the
|
||||
optional-peer kits (canvas/dashboard/gitops/usage) are not on the public
|
||||
registry, a standalone install must skip auto-installing peers. `.npmrc` and
|
||||
`pnpm-lock.yaml` are gitignored here, so create the `.npmrc` once:
|
||||
|
||||
```bash
|
||||
cd pkg/ui
|
||||
printf 'auto-install-peers=false\nstrict-peer-dependencies=false\n' > .npmrc
|
||||
pnpm install --ignore-workspace # component-surface deps (radix, cmdk, sonner, …) + @hanzo/tokens
|
||||
pnpm gen:primitives # refresh the per-member entrypoints
|
||||
pnpm typecheck:ui # scoped typecheck of the component surface (green)
|
||||
```
|
||||
|
||||
`@hanzo/tokens` (`pkgs/tokens`) must be built first (`pnpm --filter @hanzo/tokens build`)
|
||||
so the `file:` link resolves. The scoped `typecheck:ui` excludes the optional-peer
|
||||
subpaths, whose homes aren't installed standalone.
|
||||
|
||||
The **kits** = canvas, wallet, network, billing, dashboard, usage, gitops, data.
|
||||
Add one by mirroring `src/gitops.ts` (a one-line `export *`) + a `./name` export
|
||||
+ an optional peer/devDep. `pkg/*` is a pnpm workspace member (for `workspace:*`
|
||||
dev links), but `pkg/ui` publishes via the maintainer flow, not `publish.yml`
|
||||
(which auto-publishes only `pkgs/*` on a version bump — see PUBLISH_GUIDE.md). The
|
||||
shared shell lives here too: `AppHeader` + `BrandMark` (@hanzo/logo) +
|
||||
`OrgSwitcher` + `orgScope` (the console org-scope contract, hoisted per #36). Lux
|
||||
surfaces use `@luxfi/web3` for wallet/login; `@hanzo/ui/wallet`+`/network` are the
|
||||
Hanzo-branded equivalents.
|
||||
|
||||
## Repository Structure
|
||||
|
||||
```
|
||||
ui/
|
||||
app/ Hanzo documentation site (Next.js 15.3.1, React 19)
|
||||
registry/ Component registry (SOURCE OF TRUTH)
|
||||
default/ui/ 150+ components
|
||||
default/example/ Usage demos
|
||||
default/blocks/ 24+ full-page sections
|
||||
new-york/ Alternative theme
|
||||
content/docs/ MDX documentation
|
||||
scripts/ Build scripts
|
||||
apps/
|
||||
v4/ Upstream shadcn v4 docs/registry app (port 4000)
|
||||
packages/
|
||||
shadcn/ shadcn CLI v4.1.0 (font system, chart colors, scaffold)
|
||||
tests/ Integration tests for shadcn CLI
|
||||
og/ OG image generation (Hanzo-only)
|
||||
pkg/
|
||||
ui/ Core library (npm)
|
||||
react/ React primitives
|
||||
brand/ Branding system
|
||||
commerce/ E-commerce components
|
||||
checkout/ Checkout flow
|
||||
shop/ Shop components
|
||||
agent-ui/ AI agent UI components
|
||||
tokens/ Design tokens
|
||||
skills/
|
||||
shadcn/ AI skill definitions for shadcn CLI
|
||||
templates/ Project templates (next, vite, astro, react-router, start + monorepo variants)
|
||||
template/next/ Hanzo-customized Next.js template
|
||||
```
|
||||
|
||||
## Critical: Build Order
|
||||
|
||||
**Registry MUST build before app.** The registry generates JSON that the CLI reads.
|
||||
|
||||
```bash
|
||||
pnpm build:registry # MUST run first
|
||||
pnpm build # Then build app
|
||||
```
|
||||
|
||||
## Commands
|
||||
|
||||
```bash
|
||||
pnpm dev # Dev server (:3003)
|
||||
pnpm build:registry # Build component registry
|
||||
pnpm build # Build app
|
||||
pnpm lint # Lint all workspaces
|
||||
pnpm typecheck # Type checking
|
||||
pnpm test # Unit tests
|
||||
pnpm test:e2e # Playwright E2E
|
||||
```
|
||||
|
||||
## How this ships
|
||||
|
||||
One way, and it runs on our own stack:
|
||||
|
||||
push -> github.com/hanzoai/ui (a mirror)
|
||||
.github/workflows/sync.yml carries refs onward
|
||||
-> git.hanzo.ai/hanzoai/ui CANONICAL
|
||||
.hanzo/workflows/ci.yml lint, typecheck, build, test
|
||||
.hanzo/workflows/registries.yml the v4 registry check
|
||||
.hanzo/workflows/publish.yml publishes every pkgs/* package
|
||||
.hanzo/workflows/deploy.yml builds ghcr.io/hanzoai/ui
|
||||
-> hanzoai/universe crs/ui.yaml names the tag that is live
|
||||
-> hanzoai/operator reconciles the App
|
||||
-> hanzoai/static behind hanzoai/ingress serves ui.hanzo.ai
|
||||
|
||||
**git.hanzo.ai is canonical; GitHub is a mirror.** `.github/workflows/` holds
|
||||
exactly one file, `sync.yml`, and its only job is getting refs to the forge. Every
|
||||
build, check, publish and deploy is a workflow under `.hanzo/workflows/`, which the
|
||||
forge reads. `.hanzo/workflows` uses GitHub Actions syntax, so a workflow moves
|
||||
between the two by changing directory and nothing else.
|
||||
|
||||
No Vercel. `ci.yml` used to end in a `deploy-preview` job that ran `vercel deploy`
|
||||
on every PR; previews come from our own stack or not at all, so that job is gone.
|
||||
Its `status` job never depended on it.
|
||||
|
||||
## Publishing
|
||||
|
||||
One way: bump a package's `version` in its `package.json` and merge to `main`.
|
||||
`.hanzo/workflows/publish.yml` detects the changed `@hanzo/*` package and publishes
|
||||
it to npm (needs `NPM_TOKEN` as a forge secret). No changesets, no version-PR bot
|
||||
— the semver bump is the trigger.
|
||||
|
||||
It is the SOLE publisher of every non-private `@hanzo/*` in `pkgs/*`, and it
|
||||
mirrors the same tarball to `api.hanzo.ai/v1/packages/hanzo/npm` when
|
||||
`HANZO_REGISTRY_TOKEN` is present. That mirror is best-effort by construction: no
|
||||
token means a notice, not a failure, and npmjs stays authoritative either way.
|
||||
|
||||
## Deploying the site
|
||||
|
||||
`app/` is a Next.js static export (`output: "export"`, `trailingSlash: true`);
|
||||
`pnpm build` there writes `app/out`, which `Dockerfile` copies into
|
||||
`ghcr.io/hanzoai/static`. ui.hanzo.ai has been served that way since 2026-07-25 —
|
||||
no Cloudflare, no GitHub Pages.
|
||||
|
||||
What was missing until now is the build. `crs/ui.yaml` is live and promoted, but no
|
||||
workflow ever produced the image it pins: every tag up to `v5.7.6` was pushed by
|
||||
hand. `.hanzo/workflows/deploy.yml` is that step. It publishes
|
||||
`ghcr.io/hanzoai/ui:<sha>` and stops there — a build never deploys itself. A human
|
||||
sets `spec.image.tag` in `hanzoai/universe`
|
||||
`infra/k8s/operator/crs/ui.yaml`, which is the one live thing that says which build
|
||||
serves.
|
||||
|
||||
Coverage lives in `ci.yml`'s `test` job, which runs `pnpm test:coverage` so the
|
||||
lcov it uploads to Codecov actually exists. A separate `coverage.yml` used to run
|
||||
the same suite a second time for the same upload plus a PR comment through the
|
||||
GitHub API; one workflow does it now.
|
||||
|
||||
`registries.yml` keeps the `apps/v4` registry honest: reserved namespaces are
|
||||
rejected and `pnpm --filter=v4 validate:registries` must pass. Its other job
|
||||
labelled and commented on pull requests with the `gh` CLI against the GitHub API,
|
||||
which does not exist on the forge, so that job did not come along.
|
||||
|
||||
## Telemetry — `@hanzo/event` is the ONE client (`pkgs/event`)
|
||||
|
||||
`@hanzo/event` is the single canonical telemetry client for every Hanzo surface.
|
||||
ONE API surface over **TWO** planes — the client never sends the org; the server
|
||||
resolves the tenant.
|
||||
|
||||
1. **Event stream** — pageview/event/identify/group (and an error breadcrumb),
|
||||
batched to `POST {host}/v1/event` with `{ batch: [Event, …] }`
|
||||
`-> { accepted, dropped }`. Tenant from the session or a publishable `pk_` key.
|
||||
2. **Error plane** — every captured exception is ALSO framed as a real **Sentry
|
||||
envelope** and POSTed to `POST {dsn.origin}/v1/sentry/{projectId}/envelope/?sentry_key=…`.
|
||||
This is the ONLY thing that reaches the Sentry error dashboard.
|
||||
|
||||
> **There is NO server-side fan-out from `/v1/event` into Sentry.** Versions
|
||||
> ≤ 0.3.1 claimed there was ("lensed server-side into … error tracking"). There
|
||||
> is not: cloud's handler folds the exception into `properties.$exception`,
|
||||
> writes one row to the event warehouse (readable via `GET /v1/errors`), and
|
||||
> stops. Because every property believed that claim, the whole fleet reported
|
||||
> **zero** errors to Sentry until 0.3.2 added the envelope. Do not re-collapse
|
||||
> these planes.
|
||||
|
||||
Configure the error plane with `dsn` (or `NEXT_PUBLIC_HANZO_EVENT_DSN`), minted
|
||||
per property via `POST /v1/sentry/projects`. The DSN key is publishable and
|
||||
write-only — safe in a bundle, same trust class as `pk_`. **No DSN => the error
|
||||
plane is inert** (fail-safe: nothing sent, nothing thrown, event stream
|
||||
unaffected); assert `client.errorPlaneEnabled` if you need to know.
|
||||
|
||||
There is NO third plane. Web analytics used to be one — `analytics.hanzo.ai/hz.js`
|
||||
posting a bare JSON array of `{site, ts, type, …}` to a second collector behind an
|
||||
identical path spelling — and 0.3.7 deleted both. `hz.js` now lives in this package
|
||||
as the script-tag DISTRIBUTION of this client: same `WireEvent`, same
|
||||
`{ batch: [ … ] }`, same `POST {host}/v1/event`. Point everything at the API host.
|
||||
|
||||
It could not authenticate until 0.3.12: it sent no `Authorization` and no
|
||||
`?ingest_key=`, so a keyed static surface's writes were unattributed, the door
|
||||
refused them (`401`), and nothing in the page said so. `data-ingest-key="pk-…"`.
|
||||
|
||||
Entries: `.` (framework-agnostic: `createAnalytics`, `EVENTS`, `GOALS`,
|
||||
attribution + DSN/scrub helpers) and `./react` (`AnalyticsProvider`,
|
||||
`useAnalytics`, `usePageview`, `ErrorBoundary`). Auto error capture
|
||||
(window.onerror / unhandledrejection / React boundary) makes it the drop-in
|
||||
error-tracking replacement. Secrets and PII are scrubbed client-side before an
|
||||
error leaves the device. SSR-safe, fail-soft, beacon-on-unload.
|
||||
|
||||
Build is a tsup dual bundle: **CJS → `.cjs`, ESM → `.mjs`** (required under
|
||||
`"type": "module"` — a CJS `.js` is parsed as ESM and crashes `require()` with
|
||||
"exports is not defined"). Each `exports` condition carries its own types.
|
||||
|
||||
### Interaction analytics — `<Hanzo analytics>` is the one wiring
|
||||
|
||||
An app instruments nothing. `<Hanzo analytics>` (`pkg/ui/src/root.tsx`) is the
|
||||
whole setup, and every click / change / submit / route change inside the tree
|
||||
reaches `POST /v1/event` named by the component it happened on:
|
||||
|
||||
```tsx
|
||||
<Hanzo analytics={{ product: 'console', ingestKey: process.env.NEXT_PUBLIC_PUBLISHABLE_KEY }}>
|
||||
```
|
||||
|
||||
Four packages, one of each concern, no duplication:
|
||||
|
||||
| Concern | Where | Note |
|
||||
|---|---|---|
|
||||
| client + wire | `@hanzo/event` (`pkgs/event`) | one endpoint, one key |
|
||||
| capture engine | `@hanzo/observe` (`pkgs/observe`) | delegated listeners, semantic annotation, redaction |
|
||||
| provider + consent | `@hanzogui/telemetry` (`~/work/hanzo/gui`) | `<TelemetryProvider/>`; owns DNT/GPC + stored choice |
|
||||
| curated events | `@hanzo/ui/product` `instrument.ts` | `emit({component, action})` — what autocapture cannot know |
|
||||
|
||||
**`analytics` is a prop, not a default.** Mounting a component library must not
|
||||
start a network conversation the app did not ask for. Off, no provider renders.
|
||||
|
||||
**Component names are real in production.** Every primitive already carries a
|
||||
`data-slot` (via `slot()`); `componentName()` in `pkgs/observe/src/annotate.ts`
|
||||
reads it, ranked ABOVE the React fiber owner deliberately — the fiber name is
|
||||
dev-only, so grouping on it silently empties the dashboard at deploy. Labels keep
|
||||
the qualifier: `card/button[Save]`.
|
||||
|
||||
**It cannot double-count.** The engine installs *delegated* listeners on a root,
|
||||
so two engines on one root report everything twice — which is what an app got by
|
||||
mounting a library provider AND `<ObserveProvider/>`, both correct instructions.
|
||||
Since observe 0.1.7 the first engine claims its root under a `Symbol.for`
|
||||
registry (page-wide, so duplicate copies of the package still see each other) and
|
||||
any later one stays inert (`engine.capturing === false`). Verified in Chromium,
|
||||
not only in jsdom.
|
||||
|
||||
**Consent is decided in ONE layer.** The engine takes `enabled` as a value; the
|
||||
provider resolves policy (GPC, DNT, stored `hz_consent`, build kill switch) and
|
||||
passes the answer down. Do not add a second, partial copy to the engine — the two
|
||||
then disagree about an explicit opt-in.
|
||||
|
||||
### One way — supersessions (no divergent telemetry client)
|
||||
|
||||
| Package | Status | Note |
|
||||
|---|---|---|
|
||||
| `@hanzo/event` | **canonical** | `pkgs/event`, posts `/v1/event` only |
|
||||
| `@hanzo/capture` (npm) | **deprecated → `@hanzo/event`** | the old name of this package; `@hanzo/event` is a superset |
|
||||
| `pkgs/capture` (`@hanzo/analytics@0.1.0` dup) | **deleted** | stale in-repo duplicate, removed |
|
||||
| `hanzoai/analytics` `packages/event` (`@hanzo/event@0.2.0`) | **deleted** | An unpublished FORK of this package in another repo. It was the only copy that could actually reach Sentry, while the published one here could not — the fleet's error telemetry died in that gap. Its envelope + scrub implementation was merged here in 0.3.2. Never fork this package again; it publishes from `pkgs/event` only. |
|
||||
|
||||
## Three-Layer Architecture
|
||||
|
||||
1. **Components** (`registry/{style}/ui/`) — single primitives (Button, Card, Dialog). CLI-installable.
|
||||
2. **Examples** (`registry/{style}/example/`) — usage demos for docs via `<ComponentPreview />`.
|
||||
3. **Blocks** (`registry/{style}/blocks/`) — full-page sections (Dashboard, Login). NOT CLI-installable, docs only.
|
||||
|
||||
## Import Path Transformation
|
||||
|
||||
Registry files use `@/registry/default/ui/button`. After CLI install, rewritten to `@/components/ui/button`.
|
||||
|
||||
## Package Exports
|
||||
|
||||
```typescript
|
||||
import { Button, Card } from '@hanzo/ui'
|
||||
import { Button } from '@hanzo/ui/components'
|
||||
import * as Dialog from '@hanzo/ui/primitives/dialog'
|
||||
import { cn } from '@hanzo/ui/lib/utils'
|
||||
```
|
||||
|
||||
## Adding a Component
|
||||
|
||||
1. Create in BOTH themes: `app/registry/{default,new-york}/ui/my-component.tsx`
|
||||
2. Create example: `app/registry/default/example/my-component-demo.tsx`
|
||||
3. Create docs: `app/content/docs/components/my-component.mdx`
|
||||
4. Update nav: `app/config/docs.ts`
|
||||
5. Build: `pnpm build:registry`
|
||||
|
||||
## 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 a local clone of shadcn-ui/ui.
|
||||
hanzoai/ui is NOT a GitHub fork — no shared object store, so large merges can fail on push.
|
||||
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
|
||||
- Regenerate: pnpm-lock.yaml after sync
|
||||
|
||||
## Key Features
|
||||
|
||||
- **Page Builder** (`/builder`): drag-drop block assembly with @dnd-kit, export to TSX
|
||||
- **White-Label**: Zoo/Lux forks via `brands/{BRAND}.brand.ts`
|
||||
- **External Registries**: 35+ sources in `app/registries.json`, install via `npx @hanzo/ui add @aceternity/spotlight`
|
||||
|
||||
## Gotchas
|
||||
|
||||
- Registry index is `Index[style][name]`, NOT `Index[name]` — caused silent block render failures
|
||||
- Shiki `getHighlighter` incompatible with static export — replaced with basic pre/code
|
||||
- Some blocks (login-01, login-02, sidebar-02) have Server Component issues with event handlers
|
||||
- `@hanzo/auth` v2.6.0 uses a pluggable provider registry: `registerAuthProvider('firebase', FirebaseAuthService)`
|
||||
|
||||
## Rules
|
||||
|
||||
1. Always build registry before app
|
||||
2. Keep default and new-york themes in sync
|
||||
3. Blocks are docs only, not CLI-installable
|
||||
4. Use pnpm, not npm/yarn
|
||||
5. Never commit symlinked files (AGENTS.md, CLAUDE.md, etc.)
|
||||
6. Documentation goes in `app/content/docs/`, not random root MD files
|
||||
@@ -1,345 +0,0 @@
|
||||
# Hanzo UI Makefile
|
||||
# This Makefile provides convenient commands for building, testing, and managing the Hanzo UI monorepo
|
||||
|
||||
# Variables
|
||||
PNPM := pnpm
|
||||
NODE := node
|
||||
NPM := npm
|
||||
TSX := tsx
|
||||
|
||||
# Colors for output
|
||||
CYAN := \033[0;36m
|
||||
GREEN := \033[0;32m
|
||||
YELLOW := \033[0;33m
|
||||
RED := \033[0;31m
|
||||
NC := \033[0m # No Color
|
||||
|
||||
# Default target - runs build, test, and docs
|
||||
.PHONY: all
|
||||
all: install build test docs
|
||||
@echo "$(GREEN)✓ All tasks completed successfully$(NC)"
|
||||
|
||||
# Help target - shows available commands
|
||||
.PHONY: help
|
||||
help:
|
||||
@echo "$(CYAN)Hanzo UI Makefile$(NC)"
|
||||
@echo ""
|
||||
@echo "$(YELLOW)Available targets:$(NC)"
|
||||
@echo " $(GREEN)make$(NC) - Run default targets (install, build, test, docs)"
|
||||
@echo " $(GREEN)make install$(NC) - Install all dependencies"
|
||||
@echo " $(GREEN)make build$(NC) - Build all packages and the app"
|
||||
@echo " $(GREEN)make test$(NC) - Run all tests"
|
||||
@echo " $(GREEN)make dev$(NC) - Start development server"
|
||||
@echo " $(GREEN)make lint$(NC) - Run linting checks"
|
||||
@echo " $(GREEN)make format$(NC) - Format code with Prettier"
|
||||
@echo " $(GREEN)make typecheck$(NC) - Run TypeScript type checking"
|
||||
@echo " $(GREEN)make docs$(NC) - Generate documentation"
|
||||
@echo " $(GREEN)make clean$(NC) - Clean build artifacts and node_modules"
|
||||
@echo " $(GREEN)make registry$(NC) - Build the component registry"
|
||||
@echo " $(GREEN)make version$(NC) - Show package versions"
|
||||
@echo ""
|
||||
@echo "$(YELLOW)Deployment & Publishing:$(NC)"
|
||||
@echo " $(GREEN)make deploy$(NC) - Deploy to ui.hanzo.ai via GitHub Pages"
|
||||
@echo " $(GREEN)make deploy-pages$(NC) - Manually trigger GitHub Pages deployment"
|
||||
@echo " $(GREEN)make deploy-status$(NC)- Check deployment status"
|
||||
@echo " $(GREEN)make publish$(NC) - Publish packages to npm registry"
|
||||
@echo " $(GREEN)make publish-dry$(NC) - Dry run npm publish"
|
||||
@echo ""
|
||||
@echo "$(YELLOW)Development:$(NC)"
|
||||
@echo " $(GREEN)make dev-app$(NC) - Start app development server"
|
||||
@echo " $(GREEN)make dev-ui$(NC) - Build UI package in watch mode"
|
||||
@echo ""
|
||||
@echo "$(YELLOW)Quality:$(NC)"
|
||||
@echo " $(GREEN)make check$(NC) - Run all quality checks (lint, format, typecheck)"
|
||||
@echo " $(GREEN)make fix$(NC) - Auto-fix linting and formatting issues"
|
||||
@echo ""
|
||||
@echo "$(YELLOW)Utilities:$(NC)"
|
||||
@echo " $(GREEN)make clean-all$(NC) - Deep clean (removes lock file too)"
|
||||
@echo " $(GREEN)make update-deps$(NC) - Update all dependencies"
|
||||
|
||||
# Install dependencies
|
||||
.PHONY: install
|
||||
install:
|
||||
@echo "$(CYAN)Installing dependencies...$(NC)"
|
||||
@$(PNPM) install
|
||||
@echo "$(GREEN)✓ Dependencies installed$(NC)"
|
||||
|
||||
# Build all packages and app
|
||||
.PHONY: build
|
||||
build: install
|
||||
@echo "$(CYAN)Building all packages...$(NC)"
|
||||
@$(PNPM) run build
|
||||
@echo "$(GREEN)✓ Build completed$(NC)"
|
||||
|
||||
# Build individual components
|
||||
.PHONY: build-app
|
||||
build-app:
|
||||
@echo "$(CYAN)Building app...$(NC)"
|
||||
@cd app && $(PNPM) run build
|
||||
@echo "$(GREEN)✓ App build completed$(NC)"
|
||||
|
||||
.PHONY: build-ui
|
||||
build-ui:
|
||||
@echo "$(CYAN)Building UI package...$(NC)"
|
||||
@cd pkg/ui && $(PNPM) run build
|
||||
@echo "$(GREEN)✓ UI package build completed$(NC)"
|
||||
|
||||
.PHONY: build-auth
|
||||
build-auth:
|
||||
@echo "$(CYAN)Building Auth package...$(NC)"
|
||||
@cd pkg/auth && $(PNPM) run build 2>/dev/null || true
|
||||
@echo "$(GREEN)✓ Auth package build completed$(NC)"
|
||||
|
||||
.PHONY: build-commerce
|
||||
build-commerce:
|
||||
@echo "$(CYAN)Building Commerce package...$(NC)"
|
||||
@cd pkg/commerce && $(PNPM) run build 2>/dev/null || true
|
||||
@echo "$(GREEN)✓ Commerce package build completed$(NC)"
|
||||
|
||||
# Run tests
|
||||
.PHONY: test
|
||||
test:
|
||||
@echo "$(CYAN)Running tests...$(NC)"
|
||||
@if [ -f "app/package.json" ] && grep -q '"test"' app/package.json 2>/dev/null; then \
|
||||
cd app && $(PNPM) test 2>/dev/null || echo "$(YELLOW)No tests configured for app$(NC)"; \
|
||||
else \
|
||||
echo "$(YELLOW)No test script found in app$(NC)"; \
|
||||
fi
|
||||
@echo "$(GREEN)✓ Tests completed$(NC)"
|
||||
|
||||
# Development server
|
||||
.PHONY: dev
|
||||
dev:
|
||||
@echo "$(CYAN)Starting development server on port 3003...$(NC)"
|
||||
@cd app && $(PNPM) run dev
|
||||
|
||||
.PHONY: dev-app
|
||||
dev-app: dev
|
||||
|
||||
.PHONY: dev-ui
|
||||
dev-ui:
|
||||
@echo "$(CYAN)Building UI package in watch mode...$(NC)"
|
||||
@cd pkg/ui && $(PNPM) run tc --watch
|
||||
|
||||
# Linting
|
||||
.PHONY: lint
|
||||
lint:
|
||||
@echo "$(CYAN)Running linters...$(NC)"
|
||||
@cd app && $(PNPM) run lint
|
||||
@echo "$(GREEN)✓ Linting completed$(NC)"
|
||||
|
||||
.PHONY: lint-fix
|
||||
lint-fix:
|
||||
@echo "$(CYAN)Fixing linting issues...$(NC)"
|
||||
@cd app && $(PNPM) run lint:fix
|
||||
@echo "$(GREEN)✓ Linting fixes applied$(NC)"
|
||||
|
||||
# Formatting
|
||||
.PHONY: format
|
||||
format:
|
||||
@echo "$(CYAN)Checking code formatting...$(NC)"
|
||||
@cd app && $(PNPM) run format:check
|
||||
@echo "$(GREEN)✓ Format check completed$(NC)"
|
||||
|
||||
.PHONY: format-fix
|
||||
format-fix:
|
||||
@echo "$(CYAN)Formatting code...$(NC)"
|
||||
@cd app && $(PNPM) run format:write
|
||||
@echo "$(GREEN)✓ Code formatted$(NC)"
|
||||
|
||||
# Type checking
|
||||
.PHONY: typecheck
|
||||
typecheck:
|
||||
@echo "$(CYAN)Running TypeScript type checking...$(NC)"
|
||||
@cd app && $(PNPM) run typecheck
|
||||
@cd pkg/ui && $(PNPM) run tc --noEmit
|
||||
@echo "$(GREEN)✓ Type checking completed$(NC)"
|
||||
|
||||
# Documentation generation
|
||||
.PHONY: docs
|
||||
docs:
|
||||
@echo "$(CYAN)Generating documentation...$(NC)"
|
||||
@if [ -f "app/README.md" ]; then \
|
||||
echo "$(GREEN)✓ README.md exists$(NC)"; \
|
||||
fi
|
||||
@if [ -f "LLM.md" ]; then \
|
||||
echo "$(GREEN)✓ LLM.md exists$(NC)"; \
|
||||
fi
|
||||
@echo "$(YELLOW)Note: Full documentation available at https://hanzo.ai/docs$(NC)"
|
||||
|
||||
# Registry build
|
||||
.PHONY: registry
|
||||
registry:
|
||||
@echo "$(CYAN)Building component registry...$(NC)"
|
||||
@cd app && $(PNPM) run registry:build
|
||||
@echo "$(GREEN)✓ Registry built$(NC)"
|
||||
|
||||
.PHONY: registry-capture
|
||||
registry-capture:
|
||||
@echo "$(CYAN)Capturing registry...$(NC)"
|
||||
@cd app && $(PNPM) run registry:capture
|
||||
@echo "$(GREEN)✓ Registry captured$(NC)"
|
||||
|
||||
.PHONY: validate-registries
|
||||
validate-registries:
|
||||
@echo "$(CYAN)Validating registries...$(NC)"
|
||||
@cd app && $(PNPM) run validate:registries
|
||||
@echo "$(GREEN)✓ Registries validated$(NC)"
|
||||
|
||||
# Quality checks
|
||||
.PHONY: check
|
||||
check: lint format typecheck
|
||||
@echo "$(GREEN)✓ All quality checks passed$(NC)"
|
||||
|
||||
.PHONY: fix
|
||||
fix: lint-fix format-fix
|
||||
@echo "$(GREEN)✓ All fixes applied$(NC)"
|
||||
|
||||
# Publishing packages to npm
|
||||
.PHONY: publish
|
||||
publish: build test
|
||||
@echo "$(CYAN)Publishing packages to npm...$(NC)"
|
||||
@echo "$(YELLOW)Publishing @hanzo/ui...$(NC)"
|
||||
@cd pkg/ui && $(NPM) publish --access public
|
||||
@echo "$(YELLOW)Publishing @hanzo/auth...$(NC)"
|
||||
@cd pkg/auth && $(NPM) publish --access public 2>/dev/null || true
|
||||
@echo "$(YELLOW)Publishing @hanzo/commerce...$(NC)"
|
||||
@cd pkg/commerce && $(NPM) publish --access public 2>/dev/null || true
|
||||
@echo "$(GREEN)✓ Packages published to npm$(NC)"
|
||||
|
||||
.PHONY: publish-dry
|
||||
publish-dry: build test
|
||||
@echo "$(CYAN)Dry run: Publishing packages...$(NC)"
|
||||
@cd pkg/ui && $(NPM) publish --dry-run --access public
|
||||
@cd pkg/auth && $(NPM) publish --dry-run --access public 2>/dev/null || true
|
||||
@cd pkg/commerce && $(NPM) publish --dry-run --access public 2>/dev/null || true
|
||||
@echo "$(GREEN)✓ Dry run completed$(NC)"
|
||||
|
||||
# Deployment to ui.hanzo.ai
|
||||
.PHONY: deploy
|
||||
deploy: build-static
|
||||
@echo "$(CYAN)Deploying to ui.hanzo.ai...$(NC)"
|
||||
@echo "$(YELLOW)This will trigger GitHub Actions deployment$(NC)"
|
||||
@git add -A
|
||||
@git diff --cached --quiet || (git commit -m "Deploy to ui.hanzo.ai" && git push origin main)
|
||||
@echo "$(GREEN)✓ Deployment triggered - check GitHub Actions for status$(NC)"
|
||||
@echo "$(YELLOW)View deployment at: https://github.com/hanzoai/react-sdk/actions$(NC)"
|
||||
|
||||
# Build for static export (GitHub Pages)
|
||||
.PHONY: build-static
|
||||
build-static: install
|
||||
@echo "$(CYAN)Building static site for ui.hanzo.ai...$(NC)"
|
||||
@cd app && $(PNPM) run build
|
||||
@if [ ! -f app/next.config.mjs ] || ! grep -q "output.*static" app/next.config.mjs; then \
|
||||
echo "$(YELLOW)Note: For GitHub Pages deployment, consider adding 'output: \"export\"' to next.config.mjs$(NC)"; \
|
||||
fi
|
||||
@echo "$(GREEN)✓ Static build completed$(NC)"
|
||||
|
||||
# Deploy preview (for PRs)
|
||||
.PHONY: deploy-preview
|
||||
deploy-preview: build
|
||||
@echo "$(CYAN)Creating preview deployment...$(NC)"
|
||||
@echo "$(YELLOW)Preview deployments are handled by Vercel/Netlify automatically on PR$(NC)"
|
||||
@echo "$(GREEN)✓ Push your changes to a branch and create a PR for preview$(NC)"
|
||||
|
||||
# Check deployment status
|
||||
.PHONY: deploy-status
|
||||
deploy-status:
|
||||
@echo "$(CYAN)Checking deployment status...$(NC)"
|
||||
@gh workflow view "Deploy to GitHub Pages" --web 2>/dev/null || \
|
||||
echo "$(YELLOW)Install GitHub CLI (gh) to check deployment status$(NC)"
|
||||
@echo ""
|
||||
@echo "$(CYAN)Current site:$(NC) https://ui.hanzo.ai"
|
||||
@echo "$(CYAN)GitHub Pages:$(NC) https://hanzoai.github.io/react-sdk"
|
||||
|
||||
# Manual GitHub Pages deployment trigger
|
||||
.PHONY: deploy-pages
|
||||
deploy-pages:
|
||||
@echo "$(CYAN)Triggering GitHub Pages deployment...$(NC)"
|
||||
@gh workflow run "Deploy to GitHub Pages" 2>/dev/null || \
|
||||
(echo "$(YELLOW)Install GitHub CLI or trigger manually at:$(NC)" && \
|
||||
echo "https://github.com/hanzoai/react-sdk/actions/workflows/deploy-pages.yml")
|
||||
@echo "$(GREEN)✓ Deployment workflow triggered$(NC)"
|
||||
|
||||
# Version management
|
||||
.PHONY: version
|
||||
version:
|
||||
@echo "$(CYAN)Package versions:$(NC)"
|
||||
@echo " @hanzo/react-sdk: $$(cat package.json | grep '"version"' | head -1 | cut -d'"' -f4)"
|
||||
@echo " @hanzo/ui: $$(cat pkg/ui/package.json | grep '"version"' | head -1 | cut -d'"' -f4)"
|
||||
@echo " @hanzo/auth: $$(cat pkg/auth/package.json 2>/dev/null | grep '"version"' | head -1 | cut -d'"' -f4 || echo 'N/A')"
|
||||
@echo " @hanzo/commerce: $$(cat pkg/commerce/package.json 2>/dev/null | grep '"version"' | head -1 | cut -d'"' -f4 || echo 'N/A')"
|
||||
@echo " @hanzo/ui-web (app): $$(cat app/package.json | grep '"version"' | head -1 | cut -d'"' -f4)"
|
||||
|
||||
.PHONY: version-check
|
||||
version-check:
|
||||
@echo "$(CYAN)Latest published versions:$(NC)"
|
||||
@cd pkg/ui && $(PNPM) run lat
|
||||
|
||||
# Cleaning
|
||||
.PHONY: clean
|
||||
clean:
|
||||
@echo "$(CYAN)Cleaning build artifacts and node_modules...$(NC)"
|
||||
@$(PNPM) run clean:nm
|
||||
@rm -rf app/.next app/dist app/build
|
||||
@rm -rf pkg/ui/dist pkg/ui/build
|
||||
@rm -rf pkg/auth/dist pkg/auth/build 2>/dev/null || true
|
||||
@rm -rf pkg/commerce/dist pkg/commerce/build 2>/dev/null || true
|
||||
@echo "$(GREEN)✓ Clean completed$(NC)"
|
||||
|
||||
.PHONY: clean-all
|
||||
clean-all:
|
||||
@echo "$(CYAN)Deep cleaning (including lock file)...$(NC)"
|
||||
@$(PNPM) run clean:all
|
||||
@rm -rf app/.next app/dist app/build
|
||||
@rm -rf pkg/ui/dist pkg/ui/build
|
||||
@rm -rf pkg/auth/dist pkg/auth/build 2>/dev/null || true
|
||||
@rm -rf pkg/commerce/dist pkg/commerce/build 2>/dev/null || true
|
||||
@echo "$(GREEN)✓ Deep clean completed$(NC)"
|
||||
|
||||
# Dependency management
|
||||
.PHONY: update-deps
|
||||
update-deps:
|
||||
@echo "$(CYAN)Updating dependencies...$(NC)"
|
||||
@$(PNPM) update --latest
|
||||
@echo "$(GREEN)✓ Dependencies updated$(NC)"
|
||||
|
||||
.PHONY: outdated
|
||||
outdated:
|
||||
@echo "$(CYAN)Checking for outdated dependencies...$(NC)"
|
||||
@$(PNPM) outdated
|
||||
|
||||
# Production build and start
|
||||
.PHONY: prod
|
||||
prod: build
|
||||
@echo "$(CYAN)Starting production server on port 3001...$(NC)"
|
||||
@cd app && $(PNPM) run start
|
||||
|
||||
# CI/CD helpers
|
||||
.PHONY: ci
|
||||
ci: install lint typecheck test build
|
||||
@echo "$(GREEN)✓ CI checks passed$(NC)"
|
||||
|
||||
# Docker support (if needed in future)
|
||||
.PHONY: docker-build
|
||||
docker-build:
|
||||
@echo "$(CYAN)Building Docker image...$(NC)"
|
||||
@docker build -t hanzo-ui:latest .
|
||||
@echo "$(GREEN)✓ Docker image built$(NC)"
|
||||
|
||||
.PHONY: docker-run
|
||||
docker-run:
|
||||
@echo "$(CYAN)Running Docker container...$(NC)"
|
||||
@docker run -p 3001:3001 hanzo-ui:latest
|
||||
|
||||
# Utility to check if all required tools are installed
|
||||
.PHONY: check-tools
|
||||
check-tools:
|
||||
@echo "$(CYAN)Checking required tools...$(NC)"
|
||||
@command -v $(PNPM) >/dev/null 2>&1 && echo "$(GREEN)✓ pnpm installed$(NC)" || echo "$(RED)✗ pnpm not found$(NC)"
|
||||
@command -v $(NODE) >/dev/null 2>&1 && echo "$(GREEN)✓ node installed$(NC)" || echo "$(RED)✗ node not found$(NC)"
|
||||
@command -v $(NPM) >/dev/null 2>&1 && echo "$(GREEN)✓ npm installed$(NC)" || echo "$(RED)✗ npm not found$(NC)"
|
||||
@command -v git >/dev/null 2>&1 && echo "$(GREEN)✓ git installed$(NC)" || echo "$(RED)✗ git not found$(NC)"
|
||||
|
||||
# Default target explanation
|
||||
.DEFAULT_GOAL := all
|
||||
@@ -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,39 +0,0 @@
|
||||
# Publishing
|
||||
|
||||
Two lanes, one trigger each. No changesets, no version-PR bot.
|
||||
|
||||
## 1. `pkgs/*` — auto-publish on version bump (`publish.yml`)
|
||||
|
||||
Bump a package's `version` in `pkgs/<name>/package.json` and merge to `main`.
|
||||
`.github/workflows/publish.yml` detects the changed public `@hanzo/*` package,
|
||||
builds it, and publishes to npm (repo secret `NPM_TOKEN`). Patch bumps only
|
||||
(`x.y.z` → `x.y.z+1`).
|
||||
|
||||
## 2. `pkg/ui` — `@hanzo/ui@8`, the v8 lane (maintainer flow)
|
||||
|
||||
`pkg/ui` (with `pkg/data`) is the modern cross-platform library on `@hanzo/gui`.
|
||||
It publishes from the package directory (`prepack` builds the `types/`):
|
||||
|
||||
```bash
|
||||
cd pkg/ui
|
||||
pnpm typecheck && pnpm test && pnpm build
|
||||
# bump "version" in package.json (patch), commit to main, then:
|
||||
npm publish --access public
|
||||
```
|
||||
|
||||
`@hanzo/ui-shadcn` (`pkgs/ui`) is the legacy v5 kit — existing consumers pin
|
||||
`@hanzo/ui-shadcn@^5`; it rides lane 1 like any other `pkgs/*` package.
|
||||
|
||||
> The old tag-driven flow (`publish-on-tag.yml`, `npm-publish.yml`, the
|
||||
> `pkg/commerce|brand|react` paths) is gone — do not tag to publish here.
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- `NPM_TOKEN` repo secret (lane 1) / npm auth as a maintainer (lane 2)
|
||||
- Every package carries `"publishConfig": { "access": "public" }`
|
||||
|
||||
## Checklist
|
||||
|
||||
- [ ] `pnpm typecheck` + `pnpm test` green in the package
|
||||
- [ ] Patch version bump (check the last published patch first)
|
||||
- [ ] Commit to `main`; lane 1 publishes on merge, lane 2 via `npm publish`
|
||||
@@ -1,156 +1,17 @@
|
||||
<p align="center"><img src=".github/hero.svg" alt="@hanzo/ui" width="880"></p>
|
||||
# @luxdefi/ui
|
||||
|
||||
# @hanzo/ui
|
||||
Accessible and customizable components that you can copy and paste into your DeFi apps.
|
||||
|
||||
**The React component library for AI applications.** Accessible, customizable primitives for React, Vue, Svelte, and React Native — built on shadcn/ui, extended with AI, 3D, animation, and commerce components, and a single typed import surface.
|
||||
|
||||
<p align="center">
|
||||
<a href="https://www.npmjs.com/package/@hanzo/ui"><img src="https://img.shields.io/npm/v/@hanzo/ui?color=black&label=%40hanzo%2Fui" alt="npm"></a>
|
||||
<a href="./LICENSE.md"><img src="https://img.shields.io/badge/license-MIT-black" alt="MIT"></a>
|
||||
<a href="https://ui.hanzo.ai"><img src="https://img.shields.io/badge/docs-ui.hanzo.ai-black" alt="docs"></a>
|
||||
</p>
|
||||
|
||||

|
||||
|
||||
## Features
|
||||
|
||||
- **161+ components** — 3x the surface of upstream shadcn/ui
|
||||
- **Multi-framework** — React, Vue, Svelte, React Native
|
||||
- **Two themes** — Default & New York variants
|
||||
- **AI components** — chat, assistants, agent UI, playground
|
||||
- **3D components** — interactive 3D elements
|
||||
- **Animations** — advanced motion components
|
||||
- **Page builder** — visual drag-and-drop assembly, export to TSX
|
||||
- **Blocks** — 24+ production-ready full-page templates
|
||||
- **White-label** — fork and rebrand by domain (Zoo, Lux, …)
|
||||
- **Accessible** — built on Radix UI primitives
|
||||
- **Customizable** — Tailwind CSS 4 (OKLCH), fully typed TypeScript
|
||||
|
||||
## Quick start
|
||||
|
||||
### Install
|
||||
|
||||
```bash
|
||||
pnpm add @hanzo/ui
|
||||
# or
|
||||
npm install @hanzo/ui
|
||||
```
|
||||
|
||||
### Use
|
||||
|
||||
```tsx
|
||||
import { Button, Card, Input } from '@hanzo/ui'
|
||||
|
||||
export function App() {
|
||||
return (
|
||||
<Card>
|
||||
<Card.Header>
|
||||
<Card.Title>Welcome</Card.Title>
|
||||
</Card.Header>
|
||||
<Card.Content>
|
||||
<Input placeholder="Enter text..." />
|
||||
</Card.Content>
|
||||
<Card.Footer>
|
||||
<Button>Submit</Button>
|
||||
</Card.Footer>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
## One import surface (v8)
|
||||
|
||||
`@hanzo/ui@8` is the single entry point for the whole kit. Each capability is a
|
||||
thin subpath that re-exports its home package — code lives once, and each home is
|
||||
an optional peer, pulled only when you use its subpath.
|
||||
|
||||
| Import | What you get |
|
||||
|---|---|
|
||||
| `@hanzo/ui` · `/product` | charts, metrics, PageHeader, StatusTag, EmptyState, ComboBox, SlideOver, Toast |
|
||||
| `@hanzo/ui/data` | RecordsView, DataTable, typed field editors |
|
||||
| `@hanzo/ui/canvas` | ProjectCanvas, ServiceNode, DeployTimeline, EnvSwitcher |
|
||||
| `@hanzo/ui/dashboard` | landing + deploy-pipeline + overview kit |
|
||||
| `@hanzo/ui/usage` | UsageMeter, UsageProviderCard, UsageDashboard |
|
||||
| `@hanzo/ui/gitops` | GitopsAppList, tree, diff, sync/rollback, HealthBadge |
|
||||
|
||||
Also available as granular imports:
|
||||
|
||||
```ts
|
||||
import { Button, Card } from '@hanzo/ui/components'
|
||||
import * as Dialog from '@hanzo/ui/primitives/dialog'
|
||||
import { cn } from '@hanzo/ui/lib/utils'
|
||||
```
|
||||
|
||||
## CLI
|
||||
|
||||
Add components straight into your project — the CLI copies source you own:
|
||||
|
||||
```bash
|
||||
npx @hanzo/ui add button
|
||||
npx @hanzo/ui add card dialog
|
||||
```
|
||||
|
||||
Install from 35+ external registries too:
|
||||
|
||||
```bash
|
||||
npx @hanzo/ui add @aceternity/spotlight
|
||||
```
|
||||
|
||||
## Packages
|
||||
|
||||
The workspace publishes a family of scoped packages under `@hanzo/*`:
|
||||
|
||||
| Package | Purpose |
|
||||
|---|---|
|
||||
| `@hanzo/ui` | Core library + the v8 import surface (161+ components) |
|
||||
| `@hanzo/react` | React primitives |
|
||||
| `@hanzo/data` | Records, data tables, typed field editors |
|
||||
| `@hanzo/canvas` | Service/deploy canvas components |
|
||||
| `@hanzo/dashboard` | Dashboard + deploy-pipeline kit |
|
||||
| `@hanzo/commerce` · `@hanzo/checkout` · `@hanzo/shop` | Commerce components |
|
||||
| `@hanzo/agent-ui` | AI agent UI components |
|
||||
| `@hanzo/brand` · `@hanzo/tokens` | Branding system & design tokens |
|
||||
| `@hanzo/event` | Telemetry client (`POST /v1/event`) |
|
||||
|
||||
## Development
|
||||
|
||||
```bash
|
||||
git clone https://github.com/hanzoai/ui.git
|
||||
cd ui
|
||||
pnpm install
|
||||
|
||||
pnpm build:registry # generate the component registry FIRST
|
||||
pnpm dev # docs site + registry (http://localhost:3003)
|
||||
```
|
||||
|
||||
> The registry generates the JSON the CLI reads, so `build:registry` must run
|
||||
> before `build`. Keep the Default and New York themes in sync when adding
|
||||
> components. Use pnpm — not npm or yarn.
|
||||
|
||||
```bash
|
||||
pnpm build # build the docs app
|
||||
pnpm lint # lint all workspaces
|
||||
pnpm typecheck # type check
|
||||
pnpm test # unit tests
|
||||
pnpm test:e2e # Playwright E2E
|
||||
```
|
||||

|
||||
|
||||
## Documentation
|
||||
|
||||
Full docs, live previews, and the component catalog: **[ui.hanzo.ai](https://ui.hanzo.ai)**.
|
||||
Visit http://ui.lux.finance to view the documentation.
|
||||
|
||||
## Contributing
|
||||
|
||||
See the [contributing guide](/CONTRIBUTING.md).
|
||||
Please read the [contributing guide](/CONTRIBUTING.md).
|
||||
|
||||
## License
|
||||
|
||||
MIT — see [LICENSE.md](./LICENSE.md).
|
||||
|
||||
---
|
||||
|
||||
## Hanzo — the Open AI Cloud
|
||||
|
||||
Open source · every language · on-chain settlement. [hanzo.ai](https://hanzo.ai) · [docs.hanzo.ai](https://docs.hanzo.ai)
|
||||
|
||||
**SDKs in every language** — [Python](https://github.com/hanzoai/python-sdk) (flagship) · [TypeScript](https://github.com/hanzo-js/sdk) · [Go](https://github.com/hanzo-go/sdk) · [Rust](https://github.com/hanzo-rs/sdk) · [C++](https://github.com/hanzo-cpp/sdk) · [Swift](https://github.com/hanzo-swift/sdk) · [Kotlin](https://github.com/hanzo-kt/sdk) · [umbrella](https://github.com/hanzoai/sdk)
|
||||
Licensed under the [MIT license](https://github.com/luxdefi/ui/blob/main/LICENSE.md).
|
||||
|
||||
@@ -1,9 +0,0 @@
|
||||
# Security Policy
|
||||
|
||||
If you believe you have found a security vulnerability, we encourage you to let us know right away.
|
||||
|
||||
We will investigate all legitimate reports and do our best to quickly fix the problem.
|
||||
|
||||
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.
|
||||
@@ -1,3 +0,0 @@
|
||||
{
|
||||
"extends": ["@commitlint/config-conventional"]
|
||||
}
|
||||
@@ -1,11 +0,0 @@
|
||||
{
|
||||
"mcpServers": {
|
||||
"hanzo-ui": {
|
||||
"command": "npx",
|
||||
"args": ["@hanzo/ui@latest", "mcp"],
|
||||
"env": {
|
||||
"REGISTRY_URL": "https://ui.hanzo.ai/registry/registry.json"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,434 +0,0 @@
|
||||
// @ts-nocheck
|
||||
import { browser } from "@hanzo/docs-mdx/runtime/browser"
|
||||
|
||||
import type * as Config from "../source.config"
|
||||
|
||||
const create = browser<
|
||||
typeof Config,
|
||||
import("@hanzo/docs-mdx/runtime/types").InternalTypeConfig & {
|
||||
DocData: {}
|
||||
}
|
||||
>()
|
||||
const browserCollections = {
|
||||
docs: create.doc("docs", {
|
||||
"about.mdx": () => import("../content/docs/about.mdx?collection=docs"),
|
||||
"changelog.mdx": () =>
|
||||
import("../content/docs/changelog.mdx?collection=docs"),
|
||||
"cli.mdx": () => import("../content/docs/cli.mdx?collection=docs"),
|
||||
"components-json.mdx": () =>
|
||||
import("../content/docs/components-json.mdx?collection=docs"),
|
||||
"components.mdx": () =>
|
||||
import("../content/docs/components.mdx?collection=docs"),
|
||||
"figma.mdx": () => import("../content/docs/figma.mdx?collection=docs"),
|
||||
"index.mdx": () => import("../content/docs/index.mdx?collection=docs"),
|
||||
"mcp.mdx": () => import("../content/docs/mcp.mdx?collection=docs"),
|
||||
"theming.mdx": () => import("../content/docs/theming.mdx?collection=docs"),
|
||||
"ai/actions.mdx": () =>
|
||||
import("../content/docs/ai/actions.mdx?collection=docs"),
|
||||
"ai/agents.mdx": () =>
|
||||
import("../content/docs/ai/agents.mdx?collection=docs"),
|
||||
"ai/assistant.mdx": () =>
|
||||
import("../content/docs/ai/assistant.mdx?collection=docs"),
|
||||
"ai/chat.mdx": () => import("../content/docs/ai/chat.mdx?collection=docs"),
|
||||
"ai/code.mdx": () => import("../content/docs/ai/code.mdx?collection=docs"),
|
||||
"ai/index.mdx": () =>
|
||||
import("../content/docs/ai/index.mdx?collection=docs"),
|
||||
"ai/models.mdx": () =>
|
||||
import("../content/docs/ai/models.mdx?collection=docs"),
|
||||
"ai/playground.mdx": () =>
|
||||
import("../content/docs/ai/playground.mdx?collection=docs"),
|
||||
"ai/tools.mdx": () =>
|
||||
import("../content/docs/ai/tools.mdx?collection=docs"),
|
||||
"ai/vision.mdx": () =>
|
||||
import("../content/docs/ai/vision.mdx?collection=docs"),
|
||||
"ai/voice-settings.mdx": () =>
|
||||
import("../content/docs/ai/voice-settings.mdx?collection=docs"),
|
||||
"ai/voice.mdx": () =>
|
||||
import("../content/docs/ai/voice.mdx?collection=docs"),
|
||||
"block-examples/billing.mdx": () =>
|
||||
import("../content/docs/block-examples/billing.mdx?collection=docs"),
|
||||
"block-examples/compose-spec.mdx": () =>
|
||||
import("../content/docs/block-examples/compose-spec.mdx?collection=docs"),
|
||||
"block-examples/newsletter.mdx": () =>
|
||||
import("../content/docs/block-examples/newsletter.mdx?collection=docs"),
|
||||
"block-examples/pricing.mdx": () =>
|
||||
import("../content/docs/block-examples/pricing.mdx?collection=docs"),
|
||||
"frameworks/index.mdx": () =>
|
||||
import("../content/docs/frameworks/index.mdx?collection=docs"),
|
||||
"frameworks/react.mdx": () =>
|
||||
import("../content/docs/frameworks/react.mdx?collection=docs"),
|
||||
"frameworks/vue.mdx": () =>
|
||||
import("../content/docs/frameworks/vue.mdx?collection=docs"),
|
||||
"dark-mode/astro.mdx": () =>
|
||||
import("../content/docs/dark-mode/astro.mdx?collection=docs"),
|
||||
"dark-mode/index.mdx": () =>
|
||||
import("../content/docs/dark-mode/index.mdx?collection=docs"),
|
||||
"dark-mode/next.mdx": () =>
|
||||
import("../content/docs/dark-mode/next.mdx?collection=docs"),
|
||||
"dark-mode/remix.mdx": () =>
|
||||
import("../content/docs/dark-mode/remix.mdx?collection=docs"),
|
||||
"dark-mode/vite.mdx": () =>
|
||||
import("../content/docs/dark-mode/vite.mdx?collection=docs"),
|
||||
"guides/page-builder.mdx": () =>
|
||||
import("../content/docs/guides/page-builder.mdx?collection=docs"),
|
||||
"guides/visual-workflows.mdx": () =>
|
||||
import("../content/docs/guides/visual-workflows.mdx?collection=docs"),
|
||||
"hanzo-react/getting-started.mdx": () =>
|
||||
import("../content/docs/hanzo-react/getting-started.mdx?collection=docs"),
|
||||
"hanzo-react/hooks.mdx": () =>
|
||||
import("../content/docs/hanzo-react/hooks.mdx?collection=docs"),
|
||||
"installation/astro.mdx": () =>
|
||||
import("../content/docs/installation/astro.mdx?collection=docs"),
|
||||
"installation/gatsby.mdx": () =>
|
||||
import("../content/docs/installation/gatsby.mdx?collection=docs"),
|
||||
"installation/index.mdx": () =>
|
||||
import("../content/docs/installation/index.mdx?collection=docs"),
|
||||
"installation/laravel.mdx": () =>
|
||||
import("../content/docs/installation/laravel.mdx?collection=docs"),
|
||||
"installation/manual.mdx": () =>
|
||||
import("../content/docs/installation/manual.mdx?collection=docs"),
|
||||
"installation/next.mdx": () =>
|
||||
import("../content/docs/installation/next.mdx?collection=docs"),
|
||||
"installation/remix.mdx": () =>
|
||||
import("../content/docs/installation/remix.mdx?collection=docs"),
|
||||
"installation/vite.mdx": () =>
|
||||
import("../content/docs/installation/vite.mdx?collection=docs"),
|
||||
"packages/index.mdx": () =>
|
||||
import("../content/docs/packages/index.mdx?collection=docs"),
|
||||
"components/3d-card.mdx": () =>
|
||||
import("../content/docs/components/3d-card.mdx?collection=docs"),
|
||||
"components/3d-marquee.mdx": () =>
|
||||
import("../content/docs/components/3d-marquee.mdx?collection=docs"),
|
||||
"components/3d-pin.mdx": () =>
|
||||
import("../content/docs/components/3d-pin.mdx?collection=docs"),
|
||||
"components/accordion.mdx": () =>
|
||||
import("../content/docs/components/accordion.mdx?collection=docs"),
|
||||
"components/advanced-navigation-bar.mdx": () =>
|
||||
import(
|
||||
"../content/docs/components/advanced-navigation-bar.mdx?collection=docs"
|
||||
),
|
||||
"components/ai-model-selector-navigation-bar.mdx": () =>
|
||||
import(
|
||||
"../content/docs/components/ai-model-selector-navigation-bar.mdx?collection=docs"
|
||||
),
|
||||
"components/alert-dialog.mdx": () =>
|
||||
import("../content/docs/components/alert-dialog.mdx?collection=docs"),
|
||||
"components/alert.mdx": () =>
|
||||
import("../content/docs/components/alert.mdx?collection=docs"),
|
||||
"components/android.mdx": () =>
|
||||
import("../content/docs/components/android.mdx?collection=docs"),
|
||||
"components/animated-beam.mdx": () =>
|
||||
import("../content/docs/components/animated-beam.mdx?collection=docs"),
|
||||
"components/animated-cursor.mdx": () =>
|
||||
import("../content/docs/components/animated-cursor.mdx?collection=docs"),
|
||||
"components/animated-testimonials.mdx": () =>
|
||||
import(
|
||||
"../content/docs/components/animated-testimonials.mdx?collection=docs"
|
||||
),
|
||||
"components/animated-tooltip.mdx": () =>
|
||||
import("../content/docs/components/animated-tooltip.mdx?collection=docs"),
|
||||
"components/animation-components.mdx": () =>
|
||||
import(
|
||||
"../content/docs/components/animation-components.mdx?collection=docs"
|
||||
),
|
||||
"components/announcement.mdx": () =>
|
||||
import("../content/docs/components/announcement.mdx?collection=docs"),
|
||||
"components/app-switcher-navigation-bar.mdx": () =>
|
||||
import(
|
||||
"../content/docs/components/app-switcher-navigation-bar.mdx?collection=docs"
|
||||
),
|
||||
"components/apple-cards-carousel.mdx": () =>
|
||||
import(
|
||||
"../content/docs/components/apple-cards-carousel.mdx?collection=docs"
|
||||
),
|
||||
"components/apple-hello-effect.mdx": () =>
|
||||
import(
|
||||
"../content/docs/components/apple-hello-effect.mdx?collection=docs"
|
||||
),
|
||||
"components/aspect-ratio.mdx": () =>
|
||||
import("../content/docs/components/aspect-ratio.mdx?collection=docs"),
|
||||
"components/avatar-group.mdx": () =>
|
||||
import("../content/docs/components/avatar-group.mdx?collection=docs"),
|
||||
"components/avatar.mdx": () =>
|
||||
import("../content/docs/components/avatar.mdx?collection=docs"),
|
||||
"components/badge.mdx": () =>
|
||||
import("../content/docs/components/badge.mdx?collection=docs"),
|
||||
"components/banner.mdx": () =>
|
||||
import("../content/docs/components/banner.mdx?collection=docs"),
|
||||
"components/breadcrumb-and-filters-navigation-bar.mdx": () =>
|
||||
import(
|
||||
"../content/docs/components/breadcrumb-and-filters-navigation-bar.mdx?collection=docs"
|
||||
),
|
||||
"components/breadcrumb-navigation-bar.mdx": () =>
|
||||
import(
|
||||
"../content/docs/components/breadcrumb-navigation-bar.mdx?collection=docs"
|
||||
),
|
||||
"components/breadcrumb.mdx": () =>
|
||||
import("../content/docs/components/breadcrumb.mdx?collection=docs"),
|
||||
"components/button-group.mdx": () =>
|
||||
import("../content/docs/components/button-group.mdx?collection=docs"),
|
||||
"components/button.mdx": () =>
|
||||
import("../content/docs/components/button.mdx?collection=docs"),
|
||||
"components/calendar.mdx": () =>
|
||||
import("../content/docs/components/calendar.mdx?collection=docs"),
|
||||
"components/card.mdx": () =>
|
||||
import("../content/docs/components/card.mdx?collection=docs"),
|
||||
"components/carousel.mdx": () =>
|
||||
import("../content/docs/components/carousel.mdx?collection=docs"),
|
||||
"components/centered-logo-navigation-bar.mdx": () =>
|
||||
import(
|
||||
"../content/docs/components/centered-logo-navigation-bar.mdx?collection=docs"
|
||||
),
|
||||
"components/checkbox.mdx": () =>
|
||||
import("../content/docs/components/checkbox.mdx?collection=docs"),
|
||||
"components/choicebox.mdx": () =>
|
||||
import("../content/docs/components/choicebox.mdx?collection=docs"),
|
||||
"components/code-block.mdx": () =>
|
||||
import("../content/docs/components/code-block.mdx?collection=docs"),
|
||||
"components/code-editor.mdx": () =>
|
||||
import("../content/docs/components/code-editor.mdx?collection=docs"),
|
||||
"components/code-tabs.mdx": () =>
|
||||
import("../content/docs/components/code-tabs.mdx?collection=docs"),
|
||||
"components/collaboration-navigation-bar.mdx": () =>
|
||||
import(
|
||||
"../content/docs/components/collaboration-navigation-bar.mdx?collection=docs"
|
||||
),
|
||||
"components/collapsible.mdx": () =>
|
||||
import("../content/docs/components/collapsible.mdx?collection=docs"),
|
||||
"components/color-picker.mdx": () =>
|
||||
import("../content/docs/components/color-picker.mdx?collection=docs"),
|
||||
"components/combobox.mdx": () =>
|
||||
import("../content/docs/components/combobox.mdx?collection=docs"),
|
||||
"components/command.mdx": () =>
|
||||
import("../content/docs/components/command.mdx?collection=docs"),
|
||||
"components/communication-navigation-bar.mdx": () =>
|
||||
import(
|
||||
"../content/docs/components/communication-navigation-bar.mdx?collection=docs"
|
||||
),
|
||||
"components/comparison.mdx": () =>
|
||||
import("../content/docs/components/comparison.mdx?collection=docs"),
|
||||
"components/context-menu.mdx": () =>
|
||||
import("../content/docs/components/context-menu.mdx?collection=docs"),
|
||||
"components/context-switcher-navigation-bar.mdx": () =>
|
||||
import(
|
||||
"../content/docs/components/context-switcher-navigation-bar.mdx?collection=docs"
|
||||
),
|
||||
"components/credit-card.mdx": () =>
|
||||
import("../content/docs/components/credit-card.mdx?collection=docs"),
|
||||
"components/cursor.mdx": () =>
|
||||
import("../content/docs/components/cursor.mdx?collection=docs"),
|
||||
"components/dashboard-navigation-bar.mdx": () =>
|
||||
import(
|
||||
"../content/docs/components/dashboard-navigation-bar.mdx?collection=docs"
|
||||
),
|
||||
"components/data-table.mdx": () =>
|
||||
import("../content/docs/components/data-table.mdx?collection=docs"),
|
||||
"components/date-picker.mdx": () =>
|
||||
import("../content/docs/components/date-picker.mdx?collection=docs"),
|
||||
"components/desktop-hooks.mdx": () =>
|
||||
import("../content/docs/components/desktop-hooks.mdx?collection=docs"),
|
||||
"components/desktop-spotlight.mdx": () =>
|
||||
import(
|
||||
"../content/docs/components/desktop-spotlight.mdx?collection=docs"
|
||||
),
|
||||
"components/desktop-window.mdx": () =>
|
||||
import("../content/docs/components/desktop-window.mdx?collection=docs"),
|
||||
"components/desktop.mdx": () =>
|
||||
import("../content/docs/components/desktop.mdx?collection=docs"),
|
||||
"components/dialog-stack.mdx": () =>
|
||||
import("../content/docs/components/dialog-stack.mdx?collection=docs"),
|
||||
"components/dialog.mdx": () =>
|
||||
import("../content/docs/components/dialog.mdx?collection=docs"),
|
||||
"components/dock.mdx": () =>
|
||||
import("../content/docs/components/dock.mdx?collection=docs"),
|
||||
"components/drawer.mdx": () =>
|
||||
import("../content/docs/components/drawer.mdx?collection=docs"),
|
||||
"components/dropdown-menu.mdx": () =>
|
||||
import("../content/docs/components/dropdown-menu.mdx?collection=docs"),
|
||||
"components/dropzone.mdx": () =>
|
||||
import("../content/docs/components/dropzone.mdx?collection=docs"),
|
||||
"components/e-commerce-navigation-bar.mdx": () =>
|
||||
import(
|
||||
"../content/docs/components/e-commerce-navigation-bar.mdx?collection=docs"
|
||||
),
|
||||
"components/editor.mdx": () =>
|
||||
import("../content/docs/components/editor.mdx?collection=docs"),
|
||||
"components/empty.mdx": () =>
|
||||
import("../content/docs/components/empty.mdx?collection=docs"),
|
||||
"components/field.mdx": () =>
|
||||
import("../content/docs/components/field.mdx?collection=docs"),
|
||||
"components/form.mdx": () =>
|
||||
import("../content/docs/components/form.mdx?collection=docs"),
|
||||
"components/gantt.mdx": () =>
|
||||
import("../content/docs/components/gantt.mdx?collection=docs"),
|
||||
"components/glimpse.mdx": () =>
|
||||
import("../content/docs/components/glimpse.mdx?collection=docs"),
|
||||
"components/grid-pattern.mdx": () =>
|
||||
import("../content/docs/components/grid-pattern.mdx?collection=docs"),
|
||||
"components/hover-card.mdx": () =>
|
||||
import("../content/docs/components/hover-card.mdx?collection=docs"),
|
||||
"components/icon-navigation-bar.mdx": () =>
|
||||
import(
|
||||
"../content/docs/components/icon-navigation-bar.mdx?collection=docs"
|
||||
),
|
||||
"components/image-crop.mdx": () =>
|
||||
import("../content/docs/components/image-crop.mdx?collection=docs"),
|
||||
"components/image-zoom.mdx": () =>
|
||||
import("../content/docs/components/image-zoom.mdx?collection=docs"),
|
||||
"components/index.mdx": () =>
|
||||
import("../content/docs/components/index.mdx?collection=docs"),
|
||||
"components/input-group.mdx": () =>
|
||||
import("../content/docs/components/input-group.mdx?collection=docs"),
|
||||
"components/input-otp.mdx": () =>
|
||||
import("../content/docs/components/input-otp.mdx?collection=docs"),
|
||||
"components/input.mdx": () =>
|
||||
import("../content/docs/components/input.mdx?collection=docs"),
|
||||
"components/interactive-grid-pattern.mdx": () =>
|
||||
import(
|
||||
"../content/docs/components/interactive-grid-pattern.mdx?collection=docs"
|
||||
),
|
||||
"components/iphone-15-pro.mdx": () =>
|
||||
import("../content/docs/components/iphone-15-pro.mdx?collection=docs"),
|
||||
"components/item.mdx": () =>
|
||||
import("../content/docs/components/item.mdx?collection=docs"),
|
||||
"components/kanban.mdx": () =>
|
||||
import("../content/docs/components/kanban.mdx?collection=docs"),
|
||||
"components/kbd.mdx": () =>
|
||||
import("../content/docs/components/kbd.mdx?collection=docs"),
|
||||
"components/label.mdx": () =>
|
||||
import("../content/docs/components/label.mdx?collection=docs"),
|
||||
"components/limelight-nav.mdx": () =>
|
||||
import("../content/docs/components/limelight-nav.mdx?collection=docs"),
|
||||
"components/list.mdx": () =>
|
||||
import("../content/docs/components/list.mdx?collection=docs"),
|
||||
"components/macos-dock.mdx": () =>
|
||||
import("../content/docs/components/macos-dock.mdx?collection=docs"),
|
||||
"components/magnetic.mdx": () =>
|
||||
import("../content/docs/components/magnetic.mdx?collection=docs"),
|
||||
"components/marquee.mdx": () =>
|
||||
import("../content/docs/components/marquee.mdx?collection=docs"),
|
||||
"components/menu-dock.mdx": () =>
|
||||
import("../content/docs/components/menu-dock.mdx?collection=docs"),
|
||||
"components/menubar.mdx": () =>
|
||||
import("../content/docs/components/menubar.mdx?collection=docs"),
|
||||
"components/message-dock.mdx": () =>
|
||||
import("../content/docs/components/message-dock.mdx?collection=docs"),
|
||||
"components/mini-calendar.mdx": () =>
|
||||
import("../content/docs/components/mini-calendar.mdx?collection=docs"),
|
||||
"components/minimal-tiptap.mdx": () =>
|
||||
import("../content/docs/components/minimal-tiptap.mdx?collection=docs"),
|
||||
"components/motion-highlight.mdx": () =>
|
||||
import("../content/docs/components/motion-highlight.mdx?collection=docs"),
|
||||
"components/navigation-menu.mdx": () =>
|
||||
import("../content/docs/components/navigation-menu.mdx?collection=docs"),
|
||||
"components/pagination.mdx": () =>
|
||||
import("../content/docs/components/pagination.mdx?collection=docs"),
|
||||
"components/particles-background.mdx": () =>
|
||||
import(
|
||||
"../content/docs/components/particles-background.mdx?collection=docs"
|
||||
),
|
||||
"components/particles.mdx": () =>
|
||||
import("../content/docs/components/particles.mdx?collection=docs"),
|
||||
"components/pill.mdx": () =>
|
||||
import("../content/docs/components/pill.mdx?collection=docs"),
|
||||
"components/pin-list.mdx": () =>
|
||||
import("../content/docs/components/pin-list.mdx?collection=docs"),
|
||||
"components/pixel-image.mdx": () =>
|
||||
import("../content/docs/components/pixel-image.mdx?collection=docs"),
|
||||
"components/popover.mdx": () =>
|
||||
import("../content/docs/components/popover.mdx?collection=docs"),
|
||||
"components/progress.mdx": () =>
|
||||
import("../content/docs/components/progress.mdx?collection=docs"),
|
||||
"components/qr-code.mdx": () =>
|
||||
import("../content/docs/components/qr-code.mdx?collection=docs"),
|
||||
"components/radio-group.mdx": () =>
|
||||
import("../content/docs/components/radio-group.mdx?collection=docs"),
|
||||
"components/relative-time.mdx": () =>
|
||||
import("../content/docs/components/relative-time.mdx?collection=docs"),
|
||||
"components/resizable.mdx": () =>
|
||||
import("../content/docs/components/resizable.mdx?collection=docs"),
|
||||
"components/safari.mdx": () =>
|
||||
import("../content/docs/components/safari.mdx?collection=docs"),
|
||||
"components/sandbox.mdx": () =>
|
||||
import("../content/docs/components/sandbox.mdx?collection=docs"),
|
||||
"components/scroll-area.mdx": () =>
|
||||
import("../content/docs/components/scroll-area.mdx?collection=docs"),
|
||||
"components/search-and-toggle-navigation-bar.mdx": () =>
|
||||
import(
|
||||
"../content/docs/components/search-and-toggle-navigation-bar.mdx?collection=docs"
|
||||
),
|
||||
"components/select.mdx": () =>
|
||||
import("../content/docs/components/select.mdx?collection=docs"),
|
||||
"components/separator.mdx": () =>
|
||||
import("../content/docs/components/separator.mdx?collection=docs"),
|
||||
"components/sheet.mdx": () =>
|
||||
import("../content/docs/components/sheet.mdx?collection=docs"),
|
||||
"components/simple-navigation-bar.mdx": () =>
|
||||
import(
|
||||
"../content/docs/components/simple-navigation-bar.mdx?collection=docs"
|
||||
),
|
||||
"components/skeleton.mdx": () =>
|
||||
import("../content/docs/components/skeleton.mdx?collection=docs"),
|
||||
"components/slider.mdx": () =>
|
||||
import("../content/docs/components/slider.mdx?collection=docs"),
|
||||
"components/snippet.mdx": () =>
|
||||
import("../content/docs/components/snippet.mdx?collection=docs"),
|
||||
"components/sonner.mdx": () =>
|
||||
import("../content/docs/components/sonner.mdx?collection=docs"),
|
||||
"components/spinner.mdx": () =>
|
||||
import("../content/docs/components/spinner.mdx?collection=docs"),
|
||||
"components/stars-scrolling-wheel.mdx": () =>
|
||||
import(
|
||||
"../content/docs/components/stars-scrolling-wheel.mdx?collection=docs"
|
||||
),
|
||||
"components/status-dashboard-navigation-bar.mdx": () =>
|
||||
import(
|
||||
"../content/docs/components/status-dashboard-navigation-bar.mdx?collection=docs"
|
||||
),
|
||||
"components/status.mdx": () =>
|
||||
import("../content/docs/components/status.mdx?collection=docs"),
|
||||
"components/switch.mdx": () =>
|
||||
import("../content/docs/components/switch.mdx?collection=docs"),
|
||||
"components/table.mdx": () =>
|
||||
import("../content/docs/components/table.mdx?collection=docs"),
|
||||
"components/tabs.mdx": () =>
|
||||
import("../content/docs/components/tabs.mdx?collection=docs"),
|
||||
"components/tags.mdx": () =>
|
||||
import("../content/docs/components/tags.mdx?collection=docs"),
|
||||
"components/team-switcher-navigation-bar.mdx": () =>
|
||||
import(
|
||||
"../content/docs/components/team-switcher-navigation-bar.mdx?collection=docs"
|
||||
),
|
||||
"components/terminal.mdx": () =>
|
||||
import("../content/docs/components/terminal.mdx?collection=docs"),
|
||||
"components/textarea.mdx": () =>
|
||||
import("../content/docs/components/textarea.mdx?collection=docs"),
|
||||
"components/timeline.mdx": () =>
|
||||
import("../content/docs/components/timeline.mdx?collection=docs"),
|
||||
"components/toast.mdx": () =>
|
||||
import("../content/docs/components/toast.mdx?collection=docs"),
|
||||
"components/toggle-group.mdx": () =>
|
||||
import("../content/docs/components/toggle-group.mdx?collection=docs"),
|
||||
"components/toggle.mdx": () =>
|
||||
import("../content/docs/components/toggle.mdx?collection=docs"),
|
||||
"components/tooltip.mdx": () =>
|
||||
import("../content/docs/components/tooltip.mdx?collection=docs"),
|
||||
"components/two-tier-navigation-bar.mdx": () =>
|
||||
import(
|
||||
"../content/docs/components/two-tier-navigation-bar.mdx?collection=docs"
|
||||
),
|
||||
"components/typography.mdx": () =>
|
||||
import("../content/docs/components/typography.mdx?collection=docs"),
|
||||
"components/underline-navigation-bar.mdx": () =>
|
||||
import(
|
||||
"../content/docs/components/underline-navigation-bar.mdx?collection=docs"
|
||||
),
|
||||
"components/video-player.mdx": () =>
|
||||
import("../content/docs/components/video-player.mdx?collection=docs"),
|
||||
"white-label/index.mdx": () =>
|
||||
import("../content/docs/white-label/index.mdx?collection=docs"),
|
||||
"testing/index.mdx": () =>
|
||||
import("../content/docs/testing/index.mdx?collection=docs"),
|
||||
}),
|
||||
}
|
||||
export default browserCollections
|
||||
@@ -1,15 +0,0 @@
|
||||
// @ts-nocheck
|
||||
import { dynamic } from "@hanzo/docs-mdx/runtime/dynamic"
|
||||
|
||||
import * as Config from "../source.config"
|
||||
|
||||
const create = await dynamic<
|
||||
typeof Config,
|
||||
import("@hanzo/docs-mdx/runtime/types").InternalTypeConfig & {
|
||||
DocData: {}
|
||||
}
|
||||
>(
|
||||
Config,
|
||||
{ configPath: "source.config.ts", environment: "next", outDir: ".docs" },
|
||||
{ doc: { passthroughs: ["extractedReferences"] } }
|
||||
)
|
||||
@@ -1,393 +0,0 @@
|
||||
// @ts-nocheck
|
||||
import { server } from "@hanzo/docs-mdx/runtime/server"
|
||||
|
||||
import * as __fd_glob_0 from "../content/docs/about.mdx?collection=docs"
|
||||
import * as __fd_glob_9 from "../content/docs/ai/actions.mdx?collection=docs"
|
||||
import * as __fd_glob_10 from "../content/docs/ai/agents.mdx?collection=docs"
|
||||
import * as __fd_glob_11 from "../content/docs/ai/assistant.mdx?collection=docs"
|
||||
import * as __fd_glob_12 from "../content/docs/ai/chat.mdx?collection=docs"
|
||||
import * as __fd_glob_13 from "../content/docs/ai/code.mdx?collection=docs"
|
||||
import * as __fd_glob_14 from "../content/docs/ai/index.mdx?collection=docs"
|
||||
import * as __fd_glob_15 from "../content/docs/ai/models.mdx?collection=docs"
|
||||
import * as __fd_glob_16 from "../content/docs/ai/playground.mdx?collection=docs"
|
||||
import * as __fd_glob_17 from "../content/docs/ai/tools.mdx?collection=docs"
|
||||
import * as __fd_glob_18 from "../content/docs/ai/vision.mdx?collection=docs"
|
||||
import * as __fd_glob_19 from "../content/docs/ai/voice-settings.mdx?collection=docs"
|
||||
import * as __fd_glob_20 from "../content/docs/ai/voice.mdx?collection=docs"
|
||||
import * as __fd_glob_21 from "../content/docs/block-examples/billing.mdx?collection=docs"
|
||||
import * as __fd_glob_22 from "../content/docs/block-examples/compose-spec.mdx?collection=docs"
|
||||
import * as __fd_glob_23 from "../content/docs/block-examples/newsletter.mdx?collection=docs"
|
||||
import * as __fd_glob_24 from "../content/docs/block-examples/pricing.mdx?collection=docs"
|
||||
import * as __fd_glob_1 from "../content/docs/changelog.mdx?collection=docs"
|
||||
import * as __fd_glob_2 from "../content/docs/cli.mdx?collection=docs"
|
||||
import * as __fd_glob_3 from "../content/docs/components-json.mdx?collection=docs"
|
||||
import * as __fd_glob_4 from "../content/docs/components.mdx?collection=docs"
|
||||
import * as __fd_glob_46 from "../content/docs/components/3d-card.mdx?collection=docs"
|
||||
import * as __fd_glob_47 from "../content/docs/components/3d-marquee.mdx?collection=docs"
|
||||
import * as __fd_glob_48 from "../content/docs/components/3d-pin.mdx?collection=docs"
|
||||
import * as __fd_glob_49 from "../content/docs/components/accordion.mdx?collection=docs"
|
||||
import * as __fd_glob_50 from "../content/docs/components/advanced-navigation-bar.mdx?collection=docs"
|
||||
import * as __fd_glob_51 from "../content/docs/components/ai-model-selector-navigation-bar.mdx?collection=docs"
|
||||
import * as __fd_glob_52 from "../content/docs/components/alert-dialog.mdx?collection=docs"
|
||||
import * as __fd_glob_53 from "../content/docs/components/alert.mdx?collection=docs"
|
||||
import * as __fd_glob_54 from "../content/docs/components/android.mdx?collection=docs"
|
||||
import * as __fd_glob_55 from "../content/docs/components/animated-beam.mdx?collection=docs"
|
||||
import * as __fd_glob_56 from "../content/docs/components/animated-cursor.mdx?collection=docs"
|
||||
import * as __fd_glob_57 from "../content/docs/components/animated-testimonials.mdx?collection=docs"
|
||||
import * as __fd_glob_58 from "../content/docs/components/animated-tooltip.mdx?collection=docs"
|
||||
import * as __fd_glob_59 from "../content/docs/components/animation-components.mdx?collection=docs"
|
||||
import * as __fd_glob_60 from "../content/docs/components/announcement.mdx?collection=docs"
|
||||
import * as __fd_glob_61 from "../content/docs/components/app-switcher-navigation-bar.mdx?collection=docs"
|
||||
import * as __fd_glob_62 from "../content/docs/components/apple-cards-carousel.mdx?collection=docs"
|
||||
import * as __fd_glob_63 from "../content/docs/components/apple-hello-effect.mdx?collection=docs"
|
||||
import * as __fd_glob_64 from "../content/docs/components/aspect-ratio.mdx?collection=docs"
|
||||
import * as __fd_glob_65 from "../content/docs/components/avatar-group.mdx?collection=docs"
|
||||
import * as __fd_glob_66 from "../content/docs/components/avatar.mdx?collection=docs"
|
||||
import * as __fd_glob_67 from "../content/docs/components/badge.mdx?collection=docs"
|
||||
import * as __fd_glob_68 from "../content/docs/components/banner.mdx?collection=docs"
|
||||
import * as __fd_glob_69 from "../content/docs/components/breadcrumb-and-filters-navigation-bar.mdx?collection=docs"
|
||||
import * as __fd_glob_70 from "../content/docs/components/breadcrumb-navigation-bar.mdx?collection=docs"
|
||||
import * as __fd_glob_71 from "../content/docs/components/breadcrumb.mdx?collection=docs"
|
||||
import * as __fd_glob_72 from "../content/docs/components/button-group.mdx?collection=docs"
|
||||
import * as __fd_glob_73 from "../content/docs/components/button.mdx?collection=docs"
|
||||
import * as __fd_glob_74 from "../content/docs/components/calendar.mdx?collection=docs"
|
||||
import * as __fd_glob_75 from "../content/docs/components/card.mdx?collection=docs"
|
||||
import * as __fd_glob_76 from "../content/docs/components/carousel.mdx?collection=docs"
|
||||
import * as __fd_glob_77 from "../content/docs/components/centered-logo-navigation-bar.mdx?collection=docs"
|
||||
import * as __fd_glob_78 from "../content/docs/components/checkbox.mdx?collection=docs"
|
||||
import * as __fd_glob_79 from "../content/docs/components/choicebox.mdx?collection=docs"
|
||||
import * as __fd_glob_80 from "../content/docs/components/code-block.mdx?collection=docs"
|
||||
import * as __fd_glob_81 from "../content/docs/components/code-editor.mdx?collection=docs"
|
||||
import * as __fd_glob_82 from "../content/docs/components/code-tabs.mdx?collection=docs"
|
||||
import * as __fd_glob_83 from "../content/docs/components/collaboration-navigation-bar.mdx?collection=docs"
|
||||
import * as __fd_glob_84 from "../content/docs/components/collapsible.mdx?collection=docs"
|
||||
import * as __fd_glob_85 from "../content/docs/components/color-picker.mdx?collection=docs"
|
||||
import * as __fd_glob_86 from "../content/docs/components/combobox.mdx?collection=docs"
|
||||
import * as __fd_glob_87 from "../content/docs/components/command.mdx?collection=docs"
|
||||
import * as __fd_glob_88 from "../content/docs/components/communication-navigation-bar.mdx?collection=docs"
|
||||
import * as __fd_glob_89 from "../content/docs/components/comparison.mdx?collection=docs"
|
||||
import * as __fd_glob_90 from "../content/docs/components/context-menu.mdx?collection=docs"
|
||||
import * as __fd_glob_91 from "../content/docs/components/context-switcher-navigation-bar.mdx?collection=docs"
|
||||
import * as __fd_glob_92 from "../content/docs/components/credit-card.mdx?collection=docs"
|
||||
import * as __fd_glob_93 from "../content/docs/components/cursor.mdx?collection=docs"
|
||||
import * as __fd_glob_94 from "../content/docs/components/dashboard-navigation-bar.mdx?collection=docs"
|
||||
import * as __fd_glob_95 from "../content/docs/components/data-table.mdx?collection=docs"
|
||||
import * as __fd_glob_96 from "../content/docs/components/date-picker.mdx?collection=docs"
|
||||
import * as __fd_glob_97 from "../content/docs/components/desktop-hooks.mdx?collection=docs"
|
||||
import * as __fd_glob_98 from "../content/docs/components/desktop-spotlight.mdx?collection=docs"
|
||||
import * as __fd_glob_99 from "../content/docs/components/desktop-window.mdx?collection=docs"
|
||||
import * as __fd_glob_100 from "../content/docs/components/desktop.mdx?collection=docs"
|
||||
import * as __fd_glob_101 from "../content/docs/components/dialog-stack.mdx?collection=docs"
|
||||
import * as __fd_glob_102 from "../content/docs/components/dialog.mdx?collection=docs"
|
||||
import * as __fd_glob_103 from "../content/docs/components/dock.mdx?collection=docs"
|
||||
import * as __fd_glob_104 from "../content/docs/components/drawer.mdx?collection=docs"
|
||||
import * as __fd_glob_105 from "../content/docs/components/dropdown-menu.mdx?collection=docs"
|
||||
import * as __fd_glob_106 from "../content/docs/components/dropzone.mdx?collection=docs"
|
||||
import * as __fd_glob_107 from "../content/docs/components/e-commerce-navigation-bar.mdx?collection=docs"
|
||||
import * as __fd_glob_108 from "../content/docs/components/editor.mdx?collection=docs"
|
||||
import * as __fd_glob_109 from "../content/docs/components/empty.mdx?collection=docs"
|
||||
import * as __fd_glob_110 from "../content/docs/components/field.mdx?collection=docs"
|
||||
import * as __fd_glob_111 from "../content/docs/components/form.mdx?collection=docs"
|
||||
import * as __fd_glob_112 from "../content/docs/components/gantt.mdx?collection=docs"
|
||||
import * as __fd_glob_113 from "../content/docs/components/glimpse.mdx?collection=docs"
|
||||
import * as __fd_glob_114 from "../content/docs/components/grid-pattern.mdx?collection=docs"
|
||||
import * as __fd_glob_115 from "../content/docs/components/hover-card.mdx?collection=docs"
|
||||
import * as __fd_glob_116 from "../content/docs/components/icon-navigation-bar.mdx?collection=docs"
|
||||
import * as __fd_glob_117 from "../content/docs/components/image-crop.mdx?collection=docs"
|
||||
import * as __fd_glob_118 from "../content/docs/components/image-zoom.mdx?collection=docs"
|
||||
import * as __fd_glob_119 from "../content/docs/components/index.mdx?collection=docs"
|
||||
import * as __fd_glob_120 from "../content/docs/components/input-group.mdx?collection=docs"
|
||||
import * as __fd_glob_121 from "../content/docs/components/input-otp.mdx?collection=docs"
|
||||
import * as __fd_glob_122 from "../content/docs/components/input.mdx?collection=docs"
|
||||
import * as __fd_glob_123 from "../content/docs/components/interactive-grid-pattern.mdx?collection=docs"
|
||||
import * as __fd_glob_124 from "../content/docs/components/iphone-15-pro.mdx?collection=docs"
|
||||
import * as __fd_glob_125 from "../content/docs/components/item.mdx?collection=docs"
|
||||
import * as __fd_glob_126 from "../content/docs/components/kanban.mdx?collection=docs"
|
||||
import * as __fd_glob_127 from "../content/docs/components/kbd.mdx?collection=docs"
|
||||
import * as __fd_glob_128 from "../content/docs/components/label.mdx?collection=docs"
|
||||
import * as __fd_glob_129 from "../content/docs/components/limelight-nav.mdx?collection=docs"
|
||||
import * as __fd_glob_130 from "../content/docs/components/list.mdx?collection=docs"
|
||||
import * as __fd_glob_131 from "../content/docs/components/macos-dock.mdx?collection=docs"
|
||||
import * as __fd_glob_132 from "../content/docs/components/magnetic.mdx?collection=docs"
|
||||
import * as __fd_glob_133 from "../content/docs/components/marquee.mdx?collection=docs"
|
||||
import * as __fd_glob_134 from "../content/docs/components/menu-dock.mdx?collection=docs"
|
||||
import * as __fd_glob_135 from "../content/docs/components/menubar.mdx?collection=docs"
|
||||
import * as __fd_glob_136 from "../content/docs/components/message-dock.mdx?collection=docs"
|
||||
import * as __fd_glob_137 from "../content/docs/components/mini-calendar.mdx?collection=docs"
|
||||
import * as __fd_glob_138 from "../content/docs/components/minimal-tiptap.mdx?collection=docs"
|
||||
import * as __fd_glob_139 from "../content/docs/components/motion-highlight.mdx?collection=docs"
|
||||
import * as __fd_glob_140 from "../content/docs/components/navigation-menu.mdx?collection=docs"
|
||||
import * as __fd_glob_141 from "../content/docs/components/pagination.mdx?collection=docs"
|
||||
import * as __fd_glob_142 from "../content/docs/components/particles-background.mdx?collection=docs"
|
||||
import * as __fd_glob_143 from "../content/docs/components/particles.mdx?collection=docs"
|
||||
import * as __fd_glob_144 from "../content/docs/components/pill.mdx?collection=docs"
|
||||
import * as __fd_glob_145 from "../content/docs/components/pin-list.mdx?collection=docs"
|
||||
import * as __fd_glob_146 from "../content/docs/components/pixel-image.mdx?collection=docs"
|
||||
import * as __fd_glob_147 from "../content/docs/components/popover.mdx?collection=docs"
|
||||
import * as __fd_glob_148 from "../content/docs/components/progress.mdx?collection=docs"
|
||||
import * as __fd_glob_149 from "../content/docs/components/qr-code.mdx?collection=docs"
|
||||
import * as __fd_glob_150 from "../content/docs/components/radio-group.mdx?collection=docs"
|
||||
import * as __fd_glob_151 from "../content/docs/components/relative-time.mdx?collection=docs"
|
||||
import * as __fd_glob_152 from "../content/docs/components/resizable.mdx?collection=docs"
|
||||
import * as __fd_glob_153 from "../content/docs/components/safari.mdx?collection=docs"
|
||||
import * as __fd_glob_154 from "../content/docs/components/sandbox.mdx?collection=docs"
|
||||
import * as __fd_glob_155 from "../content/docs/components/scroll-area.mdx?collection=docs"
|
||||
import * as __fd_glob_156 from "../content/docs/components/search-and-toggle-navigation-bar.mdx?collection=docs"
|
||||
import * as __fd_glob_157 from "../content/docs/components/select.mdx?collection=docs"
|
||||
import * as __fd_glob_158 from "../content/docs/components/separator.mdx?collection=docs"
|
||||
import * as __fd_glob_159 from "../content/docs/components/sheet.mdx?collection=docs"
|
||||
import * as __fd_glob_160 from "../content/docs/components/simple-navigation-bar.mdx?collection=docs"
|
||||
import * as __fd_glob_161 from "../content/docs/components/skeleton.mdx?collection=docs"
|
||||
import * as __fd_glob_162 from "../content/docs/components/slider.mdx?collection=docs"
|
||||
import * as __fd_glob_163 from "../content/docs/components/snippet.mdx?collection=docs"
|
||||
import * as __fd_glob_164 from "../content/docs/components/sonner.mdx?collection=docs"
|
||||
import * as __fd_glob_165 from "../content/docs/components/spinner.mdx?collection=docs"
|
||||
import * as __fd_glob_166 from "../content/docs/components/stars-scrolling-wheel.mdx?collection=docs"
|
||||
import * as __fd_glob_167 from "../content/docs/components/status-dashboard-navigation-bar.mdx?collection=docs"
|
||||
import * as __fd_glob_168 from "../content/docs/components/status.mdx?collection=docs"
|
||||
import * as __fd_glob_169 from "../content/docs/components/switch.mdx?collection=docs"
|
||||
import * as __fd_glob_170 from "../content/docs/components/table.mdx?collection=docs"
|
||||
import * as __fd_glob_171 from "../content/docs/components/tabs.mdx?collection=docs"
|
||||
import * as __fd_glob_172 from "../content/docs/components/tags.mdx?collection=docs"
|
||||
import * as __fd_glob_173 from "../content/docs/components/team-switcher-navigation-bar.mdx?collection=docs"
|
||||
import * as __fd_glob_174 from "../content/docs/components/terminal.mdx?collection=docs"
|
||||
import * as __fd_glob_175 from "../content/docs/components/textarea.mdx?collection=docs"
|
||||
import * as __fd_glob_176 from "../content/docs/components/timeline.mdx?collection=docs"
|
||||
import * as __fd_glob_177 from "../content/docs/components/toast.mdx?collection=docs"
|
||||
import * as __fd_glob_178 from "../content/docs/components/toggle-group.mdx?collection=docs"
|
||||
import * as __fd_glob_179 from "../content/docs/components/toggle.mdx?collection=docs"
|
||||
import * as __fd_glob_180 from "../content/docs/components/tooltip.mdx?collection=docs"
|
||||
import * as __fd_glob_181 from "../content/docs/components/two-tier-navigation-bar.mdx?collection=docs"
|
||||
import * as __fd_glob_182 from "../content/docs/components/typography.mdx?collection=docs"
|
||||
import * as __fd_glob_183 from "../content/docs/components/underline-navigation-bar.mdx?collection=docs"
|
||||
import * as __fd_glob_184 from "../content/docs/components/video-player.mdx?collection=docs"
|
||||
import * as __fd_glob_28 from "../content/docs/dark-mode/astro.mdx?collection=docs"
|
||||
import * as __fd_glob_29 from "../content/docs/dark-mode/index.mdx?collection=docs"
|
||||
import * as __fd_glob_30 from "../content/docs/dark-mode/next.mdx?collection=docs"
|
||||
import * as __fd_glob_31 from "../content/docs/dark-mode/remix.mdx?collection=docs"
|
||||
import * as __fd_glob_32 from "../content/docs/dark-mode/vite.mdx?collection=docs"
|
||||
import * as __fd_glob_5 from "../content/docs/figma.mdx?collection=docs"
|
||||
import * as __fd_glob_25 from "../content/docs/frameworks/index.mdx?collection=docs"
|
||||
import * as __fd_glob_26 from "../content/docs/frameworks/react.mdx?collection=docs"
|
||||
import * as __fd_glob_27 from "../content/docs/frameworks/vue.mdx?collection=docs"
|
||||
import * as __fd_glob_33 from "../content/docs/guides/page-builder.mdx?collection=docs"
|
||||
import * as __fd_glob_34 from "../content/docs/guides/visual-workflows.mdx?collection=docs"
|
||||
import * as __fd_glob_35 from "../content/docs/hanzo-react/getting-started.mdx?collection=docs"
|
||||
import * as __fd_glob_36 from "../content/docs/hanzo-react/hooks.mdx?collection=docs"
|
||||
import * as __fd_glob_6 from "../content/docs/index.mdx?collection=docs"
|
||||
import * as __fd_glob_37 from "../content/docs/installation/astro.mdx?collection=docs"
|
||||
import * as __fd_glob_38 from "../content/docs/installation/gatsby.mdx?collection=docs"
|
||||
import * as __fd_glob_39 from "../content/docs/installation/index.mdx?collection=docs"
|
||||
import * as __fd_glob_40 from "../content/docs/installation/laravel.mdx?collection=docs"
|
||||
import * as __fd_glob_41 from "../content/docs/installation/manual.mdx?collection=docs"
|
||||
import * as __fd_glob_42 from "../content/docs/installation/next.mdx?collection=docs"
|
||||
import * as __fd_glob_43 from "../content/docs/installation/remix.mdx?collection=docs"
|
||||
import * as __fd_glob_44 from "../content/docs/installation/vite.mdx?collection=docs"
|
||||
import * as __fd_glob_7 from "../content/docs/mcp.mdx?collection=docs"
|
||||
import * as __fd_glob_45 from "../content/docs/packages/index.mdx?collection=docs"
|
||||
import * as __fd_glob_186 from "../content/docs/testing/index.mdx?collection=docs"
|
||||
import * as __fd_glob_8 from "../content/docs/theming.mdx?collection=docs"
|
||||
import * as __fd_glob_185 from "../content/docs/white-label/index.mdx?collection=docs"
|
||||
import type * as Config from "../source.config"
|
||||
|
||||
const create = server<
|
||||
typeof Config,
|
||||
import("@hanzo/docs-mdx/runtime/types").InternalTypeConfig & {
|
||||
DocData: {}
|
||||
}
|
||||
>({ doc: { passthroughs: ["extractedReferences"] } })
|
||||
|
||||
export const docs = await create.docs(
|
||||
"docs",
|
||||
"content/docs",
|
||||
{},
|
||||
{
|
||||
"about.mdx": __fd_glob_0,
|
||||
"changelog.mdx": __fd_glob_1,
|
||||
"cli.mdx": __fd_glob_2,
|
||||
"components-json.mdx": __fd_glob_3,
|
||||
"components.mdx": __fd_glob_4,
|
||||
"figma.mdx": __fd_glob_5,
|
||||
"index.mdx": __fd_glob_6,
|
||||
"mcp.mdx": __fd_glob_7,
|
||||
"theming.mdx": __fd_glob_8,
|
||||
"ai/actions.mdx": __fd_glob_9,
|
||||
"ai/agents.mdx": __fd_glob_10,
|
||||
"ai/assistant.mdx": __fd_glob_11,
|
||||
"ai/chat.mdx": __fd_glob_12,
|
||||
"ai/code.mdx": __fd_glob_13,
|
||||
"ai/index.mdx": __fd_glob_14,
|
||||
"ai/models.mdx": __fd_glob_15,
|
||||
"ai/playground.mdx": __fd_glob_16,
|
||||
"ai/tools.mdx": __fd_glob_17,
|
||||
"ai/vision.mdx": __fd_glob_18,
|
||||
"ai/voice-settings.mdx": __fd_glob_19,
|
||||
"ai/voice.mdx": __fd_glob_20,
|
||||
"block-examples/billing.mdx": __fd_glob_21,
|
||||
"block-examples/compose-spec.mdx": __fd_glob_22,
|
||||
"block-examples/newsletter.mdx": __fd_glob_23,
|
||||
"block-examples/pricing.mdx": __fd_glob_24,
|
||||
"frameworks/index.mdx": __fd_glob_25,
|
||||
"frameworks/react.mdx": __fd_glob_26,
|
||||
"frameworks/vue.mdx": __fd_glob_27,
|
||||
"dark-mode/astro.mdx": __fd_glob_28,
|
||||
"dark-mode/index.mdx": __fd_glob_29,
|
||||
"dark-mode/next.mdx": __fd_glob_30,
|
||||
"dark-mode/remix.mdx": __fd_glob_31,
|
||||
"dark-mode/vite.mdx": __fd_glob_32,
|
||||
"guides/page-builder.mdx": __fd_glob_33,
|
||||
"guides/visual-workflows.mdx": __fd_glob_34,
|
||||
"hanzo-react/getting-started.mdx": __fd_glob_35,
|
||||
"hanzo-react/hooks.mdx": __fd_glob_36,
|
||||
"installation/astro.mdx": __fd_glob_37,
|
||||
"installation/gatsby.mdx": __fd_glob_38,
|
||||
"installation/index.mdx": __fd_glob_39,
|
||||
"installation/laravel.mdx": __fd_glob_40,
|
||||
"installation/manual.mdx": __fd_glob_41,
|
||||
"installation/next.mdx": __fd_glob_42,
|
||||
"installation/remix.mdx": __fd_glob_43,
|
||||
"installation/vite.mdx": __fd_glob_44,
|
||||
"packages/index.mdx": __fd_glob_45,
|
||||
"components/3d-card.mdx": __fd_glob_46,
|
||||
"components/3d-marquee.mdx": __fd_glob_47,
|
||||
"components/3d-pin.mdx": __fd_glob_48,
|
||||
"components/accordion.mdx": __fd_glob_49,
|
||||
"components/advanced-navigation-bar.mdx": __fd_glob_50,
|
||||
"components/ai-model-selector-navigation-bar.mdx": __fd_glob_51,
|
||||
"components/alert-dialog.mdx": __fd_glob_52,
|
||||
"components/alert.mdx": __fd_glob_53,
|
||||
"components/android.mdx": __fd_glob_54,
|
||||
"components/animated-beam.mdx": __fd_glob_55,
|
||||
"components/animated-cursor.mdx": __fd_glob_56,
|
||||
"components/animated-testimonials.mdx": __fd_glob_57,
|
||||
"components/animated-tooltip.mdx": __fd_glob_58,
|
||||
"components/animation-components.mdx": __fd_glob_59,
|
||||
"components/announcement.mdx": __fd_glob_60,
|
||||
"components/app-switcher-navigation-bar.mdx": __fd_glob_61,
|
||||
"components/apple-cards-carousel.mdx": __fd_glob_62,
|
||||
"components/apple-hello-effect.mdx": __fd_glob_63,
|
||||
"components/aspect-ratio.mdx": __fd_glob_64,
|
||||
"components/avatar-group.mdx": __fd_glob_65,
|
||||
"components/avatar.mdx": __fd_glob_66,
|
||||
"components/badge.mdx": __fd_glob_67,
|
||||
"components/banner.mdx": __fd_glob_68,
|
||||
"components/breadcrumb-and-filters-navigation-bar.mdx": __fd_glob_69,
|
||||
"components/breadcrumb-navigation-bar.mdx": __fd_glob_70,
|
||||
"components/breadcrumb.mdx": __fd_glob_71,
|
||||
"components/button-group.mdx": __fd_glob_72,
|
||||
"components/button.mdx": __fd_glob_73,
|
||||
"components/calendar.mdx": __fd_glob_74,
|
||||
"components/card.mdx": __fd_glob_75,
|
||||
"components/carousel.mdx": __fd_glob_76,
|
||||
"components/centered-logo-navigation-bar.mdx": __fd_glob_77,
|
||||
"components/checkbox.mdx": __fd_glob_78,
|
||||
"components/choicebox.mdx": __fd_glob_79,
|
||||
"components/code-block.mdx": __fd_glob_80,
|
||||
"components/code-editor.mdx": __fd_glob_81,
|
||||
"components/code-tabs.mdx": __fd_glob_82,
|
||||
"components/collaboration-navigation-bar.mdx": __fd_glob_83,
|
||||
"components/collapsible.mdx": __fd_glob_84,
|
||||
"components/color-picker.mdx": __fd_glob_85,
|
||||
"components/combobox.mdx": __fd_glob_86,
|
||||
"components/command.mdx": __fd_glob_87,
|
||||
"components/communication-navigation-bar.mdx": __fd_glob_88,
|
||||
"components/comparison.mdx": __fd_glob_89,
|
||||
"components/context-menu.mdx": __fd_glob_90,
|
||||
"components/context-switcher-navigation-bar.mdx": __fd_glob_91,
|
||||
"components/credit-card.mdx": __fd_glob_92,
|
||||
"components/cursor.mdx": __fd_glob_93,
|
||||
"components/dashboard-navigation-bar.mdx": __fd_glob_94,
|
||||
"components/data-table.mdx": __fd_glob_95,
|
||||
"components/date-picker.mdx": __fd_glob_96,
|
||||
"components/desktop-hooks.mdx": __fd_glob_97,
|
||||
"components/desktop-spotlight.mdx": __fd_glob_98,
|
||||
"components/desktop-window.mdx": __fd_glob_99,
|
||||
"components/desktop.mdx": __fd_glob_100,
|
||||
"components/dialog-stack.mdx": __fd_glob_101,
|
||||
"components/dialog.mdx": __fd_glob_102,
|
||||
"components/dock.mdx": __fd_glob_103,
|
||||
"components/drawer.mdx": __fd_glob_104,
|
||||
"components/dropdown-menu.mdx": __fd_glob_105,
|
||||
"components/dropzone.mdx": __fd_glob_106,
|
||||
"components/e-commerce-navigation-bar.mdx": __fd_glob_107,
|
||||
"components/editor.mdx": __fd_glob_108,
|
||||
"components/empty.mdx": __fd_glob_109,
|
||||
"components/field.mdx": __fd_glob_110,
|
||||
"components/form.mdx": __fd_glob_111,
|
||||
"components/gantt.mdx": __fd_glob_112,
|
||||
"components/glimpse.mdx": __fd_glob_113,
|
||||
"components/grid-pattern.mdx": __fd_glob_114,
|
||||
"components/hover-card.mdx": __fd_glob_115,
|
||||
"components/icon-navigation-bar.mdx": __fd_glob_116,
|
||||
"components/image-crop.mdx": __fd_glob_117,
|
||||
"components/image-zoom.mdx": __fd_glob_118,
|
||||
"components/index.mdx": __fd_glob_119,
|
||||
"components/input-group.mdx": __fd_glob_120,
|
||||
"components/input-otp.mdx": __fd_glob_121,
|
||||
"components/input.mdx": __fd_glob_122,
|
||||
"components/interactive-grid-pattern.mdx": __fd_glob_123,
|
||||
"components/iphone-15-pro.mdx": __fd_glob_124,
|
||||
"components/item.mdx": __fd_glob_125,
|
||||
"components/kanban.mdx": __fd_glob_126,
|
||||
"components/kbd.mdx": __fd_glob_127,
|
||||
"components/label.mdx": __fd_glob_128,
|
||||
"components/limelight-nav.mdx": __fd_glob_129,
|
||||
"components/list.mdx": __fd_glob_130,
|
||||
"components/macos-dock.mdx": __fd_glob_131,
|
||||
"components/magnetic.mdx": __fd_glob_132,
|
||||
"components/marquee.mdx": __fd_glob_133,
|
||||
"components/menu-dock.mdx": __fd_glob_134,
|
||||
"components/menubar.mdx": __fd_glob_135,
|
||||
"components/message-dock.mdx": __fd_glob_136,
|
||||
"components/mini-calendar.mdx": __fd_glob_137,
|
||||
"components/minimal-tiptap.mdx": __fd_glob_138,
|
||||
"components/motion-highlight.mdx": __fd_glob_139,
|
||||
"components/navigation-menu.mdx": __fd_glob_140,
|
||||
"components/pagination.mdx": __fd_glob_141,
|
||||
"components/particles-background.mdx": __fd_glob_142,
|
||||
"components/particles.mdx": __fd_glob_143,
|
||||
"components/pill.mdx": __fd_glob_144,
|
||||
"components/pin-list.mdx": __fd_glob_145,
|
||||
"components/pixel-image.mdx": __fd_glob_146,
|
||||
"components/popover.mdx": __fd_glob_147,
|
||||
"components/progress.mdx": __fd_glob_148,
|
||||
"components/qr-code.mdx": __fd_glob_149,
|
||||
"components/radio-group.mdx": __fd_glob_150,
|
||||
"components/relative-time.mdx": __fd_glob_151,
|
||||
"components/resizable.mdx": __fd_glob_152,
|
||||
"components/safari.mdx": __fd_glob_153,
|
||||
"components/sandbox.mdx": __fd_glob_154,
|
||||
"components/scroll-area.mdx": __fd_glob_155,
|
||||
"components/search-and-toggle-navigation-bar.mdx": __fd_glob_156,
|
||||
"components/select.mdx": __fd_glob_157,
|
||||
"components/separator.mdx": __fd_glob_158,
|
||||
"components/sheet.mdx": __fd_glob_159,
|
||||
"components/simple-navigation-bar.mdx": __fd_glob_160,
|
||||
"components/skeleton.mdx": __fd_glob_161,
|
||||
"components/slider.mdx": __fd_glob_162,
|
||||
"components/snippet.mdx": __fd_glob_163,
|
||||
"components/sonner.mdx": __fd_glob_164,
|
||||
"components/spinner.mdx": __fd_glob_165,
|
||||
"components/stars-scrolling-wheel.mdx": __fd_glob_166,
|
||||
"components/status-dashboard-navigation-bar.mdx": __fd_glob_167,
|
||||
"components/status.mdx": __fd_glob_168,
|
||||
"components/switch.mdx": __fd_glob_169,
|
||||
"components/table.mdx": __fd_glob_170,
|
||||
"components/tabs.mdx": __fd_glob_171,
|
||||
"components/tags.mdx": __fd_glob_172,
|
||||
"components/team-switcher-navigation-bar.mdx": __fd_glob_173,
|
||||
"components/terminal.mdx": __fd_glob_174,
|
||||
"components/textarea.mdx": __fd_glob_175,
|
||||
"components/timeline.mdx": __fd_glob_176,
|
||||
"components/toast.mdx": __fd_glob_177,
|
||||
"components/toggle-group.mdx": __fd_glob_178,
|
||||
"components/toggle.mdx": __fd_glob_179,
|
||||
"components/tooltip.mdx": __fd_glob_180,
|
||||
"components/two-tier-navigation-bar.mdx": __fd_glob_181,
|
||||
"components/typography.mdx": __fd_glob_182,
|
||||
"components/underline-navigation-bar.mdx": __fd_glob_183,
|
||||
"components/video-player.mdx": __fd_glob_184,
|
||||
"white-label/index.mdx": __fd_glob_185,
|
||||
"testing/index.mdx": __fd_glob_186,
|
||||
}
|
||||
)
|
||||
@@ -1,40 +0,0 @@
|
||||
// source.config.ts
|
||||
import {
|
||||
defineConfig,
|
||||
defineDocs,
|
||||
frontmatterSchema
|
||||
} from "@hanzo/docs-mdx/config";
|
||||
import rehypePrettyCode from "rehype-pretty-code";
|
||||
import { z } from "zod";
|
||||
var source_config_default = defineConfig({
|
||||
mdxOptions: {
|
||||
rehypePlugins: [
|
||||
[
|
||||
rehypePrettyCode,
|
||||
{
|
||||
theme: {
|
||||
dark: "github-dark",
|
||||
light: "github-light-default"
|
||||
},
|
||||
keepBackground: false
|
||||
}
|
||||
]
|
||||
]
|
||||
}
|
||||
});
|
||||
var docs = defineDocs({
|
||||
dir: "content/docs",
|
||||
docs: {
|
||||
schema: frontmatterSchema.extend({
|
||||
links: z.object({
|
||||
doc: z.string().optional(),
|
||||
api: z.string().optional()
|
||||
}).optional(),
|
||||
toc: z.boolean().optional()
|
||||
})
|
||||
}
|
||||
});
|
||||
export {
|
||||
source_config_default as default,
|
||||
docs
|
||||
};
|
||||
@@ -1,18 +0,0 @@
|
||||
# .kodiak.toml
|
||||
version = 1
|
||||
|
||||
[merge]
|
||||
automerge_label = "automerge"
|
||||
require_automerge_label = true
|
||||
method = "squash"
|
||||
delete_branch_on_merge = true
|
||||
optimistic_updates = false
|
||||
prioritize_ready_to_merge = true
|
||||
notify_on_conflict = true
|
||||
|
||||
[merge.message]
|
||||
title = "pull_request_title"
|
||||
body = "pull_request_body"
|
||||
include_pr_number = true
|
||||
body_type = "markdown"
|
||||
strip_html_comments = true
|
||||
@@ -1,11 +0,0 @@
|
||||
{
|
||||
"mcpServers": {
|
||||
"hanzo-ui": {
|
||||
"command": "npx",
|
||||
"args": ["@hanzo/ui@latest", "mcp"],
|
||||
"env": {
|
||||
"REGISTRY_URL": "https://ui.hanzo.ai/registry/registry.json"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,8 +0,0 @@
|
||||
dist
|
||||
node_modules
|
||||
.next
|
||||
build
|
||||
.contentlayer
|
||||
apps/web/pages/api/registry.json
|
||||
**/fixtures
|
||||
deprecated
|
||||
@@ -1,114 +0,0 @@
|
||||
# Changelog
|
||||
|
||||
All notable changes to the Hanzo UI project will be documented in this file.
|
||||
|
||||
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
||||
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
||||
|
||||
## [0.2.0] - 2025-11-04
|
||||
|
||||
### Added
|
||||
|
||||
#### Page Builder
|
||||
- **Visual Page Builder** (`/builder`) - Drag-and-drop interface for assembling pages from blocks
|
||||
- Filterable block library with 1/4 scale previews
|
||||
- Drag-to-reorder blocks in canvas
|
||||
- Viewport controls (Desktop/Tablet/Mobile) for responsive preview
|
||||
- Export to React TSX code
|
||||
- Copy code to clipboard
|
||||
- Download as .tsx file
|
||||
- Deploy with Hanzo integration
|
||||
- Real-time block rendering with dynamic imports
|
||||
- Error handling and loading states for blocks
|
||||
|
||||
#### Syntax Highlighting
|
||||
- **Shiki Integration** - Build-time syntax highlighting via Fumadocs
|
||||
- GitHub Light/Dark themes for automatic light/dark mode support
|
||||
- Multi-color syntax highlighting for all code blocks
|
||||
- Copy-to-clipboard button on code blocks
|
||||
- Line numbers and line highlighting support
|
||||
- File name labels via meta strings
|
||||
- Zero client-side JavaScript (all highlighting is static HTML)
|
||||
|
||||
#### New Blocks
|
||||
- **Newsletter Blocks** (5 variants)
|
||||
- `newsletter-01` - Simple newsletter signup
|
||||
- `newsletter-02` - Newsletter with description
|
||||
- `newsletter-03` - Newsletter with image
|
||||
- `newsletter-04` - Newsletter with testimonial
|
||||
- `newsletter-05` - Full-width newsletter section
|
||||
- **Blog Cards** - Modern blog post card layout
|
||||
- **Photo Grid** - Responsive photo gallery grid
|
||||
- **Showcase Blocks**
|
||||
- `showcase-marketing-01` - Marketing website showcase
|
||||
- `showcase-ecommerce-01` - E-commerce site showcase
|
||||
- `showcase-app-ui-01` - App UI showcase
|
||||
|
||||
#### New AI Components
|
||||
- **AI Voice Settings** - Voice configuration interface for AI assistants
|
||||
- Voice model selection
|
||||
- Speed, pitch, and volume controls
|
||||
- Preview functionality
|
||||
- Settings persistence
|
||||
|
||||
#### New Documentation
|
||||
- **AI Components Documentation**
|
||||
- AI Actions guide
|
||||
- AI Agents guide
|
||||
- AI Assistant guide
|
||||
- AI Code editor guide
|
||||
- AI Models selector guide
|
||||
- AI Vision guide
|
||||
- AI Voice Settings guide
|
||||
- AI Voice guide
|
||||
- **Blocks Documentation**
|
||||
- Newsletter blocks guide with all variants
|
||||
|
||||
### Fixed
|
||||
|
||||
- **Block Display Component** - Fixed registry access pattern for proper block rendering
|
||||
- Now uses `getBlock()` function with style parameter
|
||||
- Proper fallback to `BlockWrapper` for blocks
|
||||
- Correct handling of component vs block types
|
||||
- **Stub Components** - Fixed three blocks with missing `@hanzo/ui/billing` dependency
|
||||
- `invoice-manager-01` - Added placeholder with installation instructions
|
||||
- `payment-settings-01` - Added placeholder with installation instructions
|
||||
- `subscription-portal-01` - Added placeholder with installation instructions
|
||||
- **Dynamic Block Loading** - Replaced iframe-based previews with direct component rendering
|
||||
- Eliminates 404 errors in block previews
|
||||
- Better error handling and loading states
|
||||
- Proper scaling with transform CSS
|
||||
|
||||
### Changed
|
||||
|
||||
- **Page Builder Layout**
|
||||
- Reduced sidebar width from w-80 to w-64 for more canvas space
|
||||
- Removed right sidebar to maximize preview area
|
||||
- Changed block preview scale from 1/2 to 1/4 in sidebar
|
||||
- Removed gaps between stacked blocks using border-b pattern
|
||||
- Added viewport width controls for responsive testing
|
||||
|
||||
### Technical Improvements
|
||||
|
||||
- **MDX Configuration** - Enhanced with Shiki rehype plugin options
|
||||
- **MDX Components** - Integrated Fumadocs CodeBlock wrapper for code blocks
|
||||
- **Build Process** - Improved registry build with proper error handling
|
||||
- **Type Safety** - Updated component props to use React.ComponentPropsWithoutRef
|
||||
|
||||
## [0.1.0] - 2025-10-05
|
||||
|
||||
### Initial Release
|
||||
|
||||
- Base component library with 150+ components
|
||||
- Two theme variants (default, new-york)
|
||||
- 24+ viewport-sized blocks
|
||||
- Documentation site with MDX support
|
||||
- CLI for component installation
|
||||
- External registry support
|
||||
- Responsive design system
|
||||
- Dark mode support
|
||||
|
||||
---
|
||||
|
||||
[0.2.0]: https://github.com/hanzoai/ui/releases/tag/v0.2.0
|
||||
[0.1.0]: https://github.com/hanzoai/ui/releases/tag/v0.1.0
|
||||
@@ -1,254 +0,0 @@
|
||||
# Hanzo Identity Registration System
|
||||
|
||||
A decentralized identity registration system for Hanzo, Lux, and Zoo networks.
|
||||
|
||||
## Overview
|
||||
|
||||
The identity system allows users to claim unique identities across multiple blockchain networks with a tiered pricing structure based on name length.
|
||||
|
||||
## Features
|
||||
|
||||
- **Multi-Network Support**: Register identities on Hanzo, Lux, and Zoo (mainnet and testnet)
|
||||
- **Tiered Pricing**:
|
||||
- Single character (1): 100,000 AI
|
||||
- Two characters (2): 10,000 AI
|
||||
- Three characters (3): 1,000 AI
|
||||
- Four characters (4): 100 AI
|
||||
- Five or more (5+): 10 AI
|
||||
- **Referral System**: 50% discount with valid referrer
|
||||
- **Web3 Wallet Integration**: RainbowKit for seamless wallet connection
|
||||
- **Real-time Validation**: Check name availability and format validation
|
||||
- **Responsive UI**: Modern, accessible interface built with Radix UI
|
||||
|
||||
## Project Structure
|
||||
|
||||
```
|
||||
/Users/z/work/hanzo/ui/app/
|
||||
├── app/(app)/identity/page.tsx # Identity registration page
|
||||
├── components/web3-provider.tsx # Web3/Wagmi provider wrapper
|
||||
├── lib/
|
||||
│ ├── wagmi.ts # Wagmi config with custom networks
|
||||
│ └── contracts.ts # Contract ABIs and addresses
|
||||
└── .env.example # Environment variable template
|
||||
```
|
||||
|
||||
## Smart Contracts
|
||||
|
||||
Located in `/Users/z/work/lux/standard/src/`:
|
||||
|
||||
- **HanzoRegistry.sol**: Core identity registration contract
|
||||
- Manages identity claims
|
||||
- Handles staking requirements
|
||||
- Supports delegation and referrals
|
||||
- Implements reward distribution
|
||||
|
||||
- **AIToken.sol**: ERC20 governance token
|
||||
- Staking and vesting functionality
|
||||
- Voting power for governance
|
||||
- Deflationary burn mechanism
|
||||
|
||||
- **AIFaucet.sol**: Token distribution faucet
|
||||
- 100 AI tokens per authenticated user
|
||||
- 24-hour cooldown period
|
||||
- Daily limits and rate limiting
|
||||
|
||||
## Setup
|
||||
|
||||
### 1. Install Dependencies
|
||||
|
||||
```bash
|
||||
cd /Users/z/work/hanzo/ui/app
|
||||
pnpm install
|
||||
```
|
||||
|
||||
### 2. Configure Environment
|
||||
|
||||
Copy `.env.example` to `.env.local`:
|
||||
|
||||
```bash
|
||||
cp .env.example .env.local
|
||||
```
|
||||
|
||||
Get a WalletConnect Project ID from https://cloud.walletconnect.com/ and update:
|
||||
|
||||
```
|
||||
NEXT_PUBLIC_WALLETCONNECT_PROJECT_ID=your_project_id_here
|
||||
```
|
||||
|
||||
### 3. Deploy Contracts
|
||||
|
||||
Deploy the smart contracts to your target networks:
|
||||
|
||||
```bash
|
||||
cd /Users/z/work/lux/standard
|
||||
|
||||
# For Hanzo Mainnet
|
||||
forge script script/DeployIdentitySystem.s.sol:DeployIdentitySystem \
|
||||
--rpc-url https://rpc.hanzo.ai \
|
||||
--broadcast
|
||||
|
||||
# For Hanzo Testnet
|
||||
forge script script/DeployIdentitySystem.s.sol:DeployIdentitySystem \
|
||||
--rpc-url https://testnet-rpc.hanzo.ai \
|
||||
--broadcast
|
||||
|
||||
# For Lux Mainnet
|
||||
forge script script/DeployIdentitySystem.s.sol:DeployIdentitySystem \
|
||||
--rpc-url https://api.lux.network/v1/bc/C/rpc \
|
||||
--broadcast
|
||||
|
||||
# For Zoo Mainnet
|
||||
forge script script/DeployIdentitySystem.s.sol:DeployIdentitySystem \
|
||||
--rpc-url https://rpc.zoo.network \
|
||||
--broadcast
|
||||
```
|
||||
|
||||
### 4. Update Contract Addresses
|
||||
|
||||
After deployment, update the contract addresses in `lib/contracts.ts`:
|
||||
|
||||
```typescript
|
||||
export const CONTRACT_ADDRESSES: Record<number, { registry: `0x${string}`; token: `0x${string}` }> = {
|
||||
36963: { // Hanzo Mainnet
|
||||
registry: '0xYourRegistryAddress',
|
||||
token: '0xYourTokenAddress',
|
||||
},
|
||||
// ... update other networks
|
||||
}
|
||||
```
|
||||
|
||||
### 5. Run Development Server
|
||||
|
||||
```bash
|
||||
pnpm dev
|
||||
```
|
||||
|
||||
Visit `http://localhost:3333/identity` to access the identity registration page.
|
||||
|
||||
## Networks
|
||||
|
||||
| Network | Chain ID | RPC URL | Explorer |
|
||||
|---------|----------|---------|----------|
|
||||
| Hanzo Mainnet | 36963 | https://rpc.hanzo.ai | https://explorer.hanzo.ai |
|
||||
| Hanzo Testnet | 36962 | https://testnet-rpc.hanzo.ai | https://testnet-explorer.hanzo.ai |
|
||||
| Lux Mainnet | 96369 | https://api.lux.network/v1/bc/C/rpc | https://explorer.lux.network |
|
||||
| Lux Testnet | 96368 | https://testnet-api.lux.network/v1/bc/C/rpc | https://testnet-explorer.lux.network |
|
||||
| Zoo Mainnet | 200200 | https://rpc.zoo.network | https://explorer.zoo.network |
|
||||
| Zoo Testnet | 200201 | https://testnet-rpc.zoo.network | https://testnet-explorer.zoo.network |
|
||||
|
||||
## Identity Format
|
||||
|
||||
Identities follow the format: `@username.network/profile/type/name`
|
||||
|
||||
- **Username**: Letters, numbers, dots, and underscores
|
||||
- **Network**: Automatically determined by connected chain
|
||||
- `.hanzo` or `.ai` for Hanzo mainnet
|
||||
- `.hanzo-testnet` for Hanzo testnet
|
||||
- `.lux` for Lux mainnet
|
||||
- `.lux-testnet` for Lux testnet
|
||||
- `.zoo` for Zoo mainnet
|
||||
- `.zoo-testnet` for Zoo testnet
|
||||
- **Profile**: Optional profile path (default: `/profile`)
|
||||
|
||||
## Faucet Integration
|
||||
|
||||
Users can get AI tokens from the faucet to register identities:
|
||||
|
||||
- **Location**: `/faucet` page
|
||||
- **Amount**: 100 AI tokens per authenticated user
|
||||
- **Cooldown**: 24 hours between drips
|
||||
- **Networks**: Hanzo mainnet and testnet only
|
||||
|
||||
Configuration in `/Users/z/work/hanzo/faucet/config.json`
|
||||
|
||||
## Tech Stack
|
||||
|
||||
- **Framework**: Next.js 16 (App Router)
|
||||
- **Styling**: Tailwind CSS 4
|
||||
- **UI Components**: Radix UI primitives
|
||||
- **Web3**: Wagmi, Viem, RainbowKit
|
||||
- **State Management**: TanStack Query
|
||||
- **Smart Contracts**: Solidity, Foundry
|
||||
- **Networks**: Hanzo, Lux, Zoo (mainnet + testnet)
|
||||
|
||||
## Development
|
||||
|
||||
### Testing Locally
|
||||
|
||||
1. Use a testnet for development (Hanzo Testnet recommended)
|
||||
2. Connect your wallet to the testnet
|
||||
3. Get test AI tokens from the faucet
|
||||
4. Test identity registration flow
|
||||
|
||||
### Building for Production
|
||||
|
||||
```bash
|
||||
pnpm build
|
||||
```
|
||||
|
||||
### Type Checking
|
||||
|
||||
```bash
|
||||
pnpm typecheck
|
||||
```
|
||||
|
||||
### Linting
|
||||
|
||||
```bash
|
||||
pnpm lint
|
||||
pnpm lint:fix
|
||||
```
|
||||
|
||||
## Contract Deployment Scripts
|
||||
|
||||
Located in `/Users/z/work/lux/standard/script/`:
|
||||
|
||||
- `DeployIdentitySystem.s.sol`: Deploys complete identity system
|
||||
- Deploys AIToken with treasury allocation
|
||||
- Deploys HanzoNft for identity binding
|
||||
- Deploys HanzoRegistry with UUPS proxy
|
||||
- Deploys AIFaucet with 100 AI drip amount
|
||||
- Configures initial permissions and transfers
|
||||
|
||||
## Security Considerations
|
||||
|
||||
- All contracts use OpenZeppelin's upgradeable patterns
|
||||
- Registry uses UUPS proxy for upgradeability
|
||||
- Access control with Ownable2Step for safe ownership transfer
|
||||
- Pausable functionality for emergency stops
|
||||
- Rate limiting on faucet to prevent abuse
|
||||
- Input validation for identity names
|
||||
- Proper allowance checks before token transfers
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### "Insufficient balance" error
|
||||
- Get AI tokens from the faucet at `/faucet`
|
||||
- Ensure you're connected to the correct network
|
||||
|
||||
### "Name already taken" error
|
||||
- Try a different identity name
|
||||
- Check if the name is available on your current network
|
||||
|
||||
### "Invalid format" error
|
||||
- Use only letters, numbers, dots, and underscores
|
||||
- No spaces or special characters allowed
|
||||
|
||||
### Contract not deployed
|
||||
- Ensure contracts are deployed to your connected network
|
||||
- Update contract addresses in `lib/contracts.ts`
|
||||
- Check RPC URL is correct for your network
|
||||
|
||||
## Contributing
|
||||
|
||||
When adding new features:
|
||||
|
||||
1. Update contracts in `/Users/z/work/lux/standard/src/`
|
||||
2. Update ABIs in `lib/contracts.ts`
|
||||
3. Update UI in `app/(app)/identity/page.tsx`
|
||||
4. Add tests for new functionality
|
||||
5. Update this README with new features
|
||||
|
||||
## License
|
||||
|
||||
MIT
|
||||
-551
@@ -1,551 +0,0 @@
|
||||
# CLAUDE.md
|
||||
|
||||
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
|
||||
|
||||
## Repository Overview
|
||||
|
||||
This is the **Hanzo UI App** - the documentation and showcase site for the @hanzo/ui component library. It's built with Next.js 15.3.1, React 19, and Fumadocs for documentation. The site serves as both documentation and a living demo of 149+ components.
|
||||
|
||||
**Key URLs:**
|
||||
- Production: https://ui.hanzo.ai
|
||||
- Local Dev: http://localhost:3003
|
||||
- Repo: github.com/hanzoai/ui (monorepo root is parent directory)
|
||||
|
||||
## Essential Commands
|
||||
|
||||
### Development
|
||||
```bash
|
||||
# Start dev server (port 3003)
|
||||
pnpm dev
|
||||
|
||||
# Build site (includes registry build + Next.js build)
|
||||
pnpm build
|
||||
|
||||
# Build registry only (generates JSON files for CLI)
|
||||
pnpm registry:build
|
||||
|
||||
# Start production server (port 3001)
|
||||
pnpm start
|
||||
```
|
||||
|
||||
### Code Quality
|
||||
```bash
|
||||
# Lint
|
||||
pnpm lint
|
||||
pnpm lint:fix
|
||||
|
||||
# Type check
|
||||
pnpm typecheck
|
||||
|
||||
# Format
|
||||
pnpm format:write
|
||||
pnpm format:check
|
||||
```
|
||||
|
||||
### Registry Management
|
||||
```bash
|
||||
# Validate external registries
|
||||
pnpm validate:registries
|
||||
|
||||
# Capture screenshots (for component previews)
|
||||
pnpm registry:capture
|
||||
```
|
||||
|
||||
## Architecture Overview
|
||||
|
||||
### Three-Layer System
|
||||
|
||||
**1. Components** (`/registry/{default,new-york}/ui/`)
|
||||
- Single UI elements (Button, Input, Card, Dialog)
|
||||
- Reusable primitives that compose into larger UIs
|
||||
- Two theme variants: default and new-york
|
||||
- 149 components total (115 implemented, 34 stubs)
|
||||
|
||||
**2. Examples** (`/registry/{default,new-york}/example/`)
|
||||
- Usage demonstrations for each component
|
||||
- Shows component in isolation with typical props
|
||||
- Used in documentation via `<ComponentPreview name="..." />`
|
||||
|
||||
**3. Blocks** (`/registry/{default,new-york}/blocks/`)
|
||||
- Viewport-sized sections (Dashboard, Hero, Login, Pricing)
|
||||
- Compose multiple components into full-page layouts
|
||||
- Production-ready templates users can copy
|
||||
- 24+ blocks including dashboard-01, sidebar-07, login-03
|
||||
|
||||
### Registry Build System
|
||||
|
||||
The registry is the **core of the component distribution system**:
|
||||
|
||||
1. **Source Files**: Components in `/registry/{style}/ui/`
|
||||
2. **Build Script**: `scripts/build-registry.mts` reads components and generates:
|
||||
- JSON metadata files in `/public/registry/styles/{style}/{name}.json`
|
||||
- Includes dependencies, files, type info
|
||||
3. **CLI Consumption**: The `@hanzo/ui` CLI reads these JSON files to install components
|
||||
4. **Documentation**: MDX files in `/content/docs/components/` reference the registry
|
||||
|
||||
**CRITICAL**: Always run `pnpm registry:build` before building the app if you modify components.
|
||||
|
||||
### Documentation System
|
||||
|
||||
- **Framework**: Fumadocs (replaced Contentlayer in v4 migration)
|
||||
- **MDX Files**: `/content/docs/` contains all documentation
|
||||
- **Components in MDX**: Special components available in MDX via `mdx-components.tsx`:
|
||||
- `<ComponentPreview name="..." />` - Live component demo
|
||||
- `<ComponentSource name="..." />` - Component source code
|
||||
- `<CodeTabs>` - Installation method tabs (CLI/Manual)
|
||||
- `<Steps>` - Step-by-step instructions
|
||||
- **Navigation**: Configured in `/config/docs.ts`
|
||||
|
||||
### Page Builder (`/builder`)
|
||||
|
||||
New drag-drop visual builder for assembling pages from blocks:
|
||||
- **Left Sidebar**: Filterable block library
|
||||
- **Canvas**: Drag-drop assembly area with reordering
|
||||
- **Export**: Generates React TSX code
|
||||
- **Tech**: @dnd-kit for drag-drop, SortableContext for reordering
|
||||
|
||||
Access at `/builder` route.
|
||||
|
||||
## Key File Locations
|
||||
|
||||
### Component Development
|
||||
```
|
||||
registry/default/ui/{component}.tsx # Component implementation
|
||||
registry/default/example/{component}-demo.tsx # Usage example
|
||||
content/docs/components/{component}.mdx # Documentation
|
||||
```
|
||||
|
||||
### Configuration
|
||||
```
|
||||
config/site.ts # Site metadata, links
|
||||
config/docs.ts # Documentation navigation structure
|
||||
tailwind.config.cjs # Tailwind configuration
|
||||
components.json # hanzo CLI configuration
|
||||
```
|
||||
|
||||
### Build & Scripts
|
||||
```
|
||||
scripts/build-registry.mts # Registry JSON generator
|
||||
scripts/validate-registries.mts # External registry validator
|
||||
__registry__/index.tsx # Auto-generated registry index
|
||||
```
|
||||
|
||||
## Component Creation Workflow
|
||||
|
||||
### 1. Create Component Files
|
||||
```bash
|
||||
# Create in both variants
|
||||
touch registry/default/ui/my-component.tsx
|
||||
touch registry/new-york/ui/my-component.tsx
|
||||
|
||||
# Create demo
|
||||
touch registry/default/example/my-component-demo.tsx
|
||||
```
|
||||
|
||||
### 2. Implement Component
|
||||
Follow the standard pattern:
|
||||
```tsx
|
||||
"use client"
|
||||
|
||||
import * as React from "react"
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
interface MyComponentProps extends React.HTMLAttributes<HTMLDivElement> {
|
||||
variant?: "default" | "outline"
|
||||
}
|
||||
|
||||
const MyComponent = React.forwardRef<HTMLDivElement, MyComponentProps>(
|
||||
({ className, variant = "default", ...props }, ref) => {
|
||||
return (
|
||||
<div
|
||||
ref={ref}
|
||||
className={cn("base-classes", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
)
|
||||
MyComponent.displayName = "MyComponent"
|
||||
|
||||
export { MyComponent }
|
||||
```
|
||||
|
||||
### 3. Create Documentation
|
||||
```bash
|
||||
# Create MDX file
|
||||
touch content/docs/components/my-component.mdx
|
||||
```
|
||||
|
||||
Include:
|
||||
- `<ComponentPreview name="my-component-demo" />`
|
||||
- Installation instructions with `<CodeTabs>`
|
||||
- Usage examples
|
||||
- API table with props
|
||||
|
||||
### 4. Add to Navigation
|
||||
Edit `config/docs.ts` to add component to sidebar navigation.
|
||||
|
||||
### 5. Build Registry
|
||||
```bash
|
||||
pnpm registry:build
|
||||
```
|
||||
|
||||
### 6. Test
|
||||
```bash
|
||||
pnpm dev
|
||||
# Visit http://localhost:3003/docs/components/my-component
|
||||
```
|
||||
|
||||
## Registry Architecture
|
||||
|
||||
The registry system enables the `npx hanzo-ui@latest add {component}` CLI workflow:
|
||||
|
||||
**Registry Entry Structure:**
|
||||
```json
|
||||
{
|
||||
"name": "button",
|
||||
"type": "components:ui",
|
||||
"files": ["registry/default/ui/button.tsx"],
|
||||
"dependencies": ["class-variance-authority"],
|
||||
"registryDependencies": ["utils"]
|
||||
}
|
||||
```
|
||||
|
||||
**External Registries** (`registries.json`):
|
||||
- 35+ external component sources
|
||||
- Examples: @aceternity, @magicui, @hanzo-editor
|
||||
- Validated via `pnpm validate:registries`
|
||||
|
||||
## Theme System
|
||||
|
||||
**CSS Variables** (`/styles/globals.css`):
|
||||
- Base theme colors defined in HSL
|
||||
- Primary color: `210 100% 50%` (electric blue)
|
||||
- Dark mode via `.dark` class
|
||||
- All components use `hsl(var(--primary))` pattern
|
||||
|
||||
**Two Visual Styles:**
|
||||
- **default**: More rounded, softer shadows
|
||||
- **new-york**: Flatter, sharper, minimal
|
||||
|
||||
Both styles share the same component API, only visual styling differs.
|
||||
|
||||
## Build System
|
||||
|
||||
### Production Build Process
|
||||
1. `pnpm registry:build` - Generate registry JSON files
|
||||
2. `next build` - Build Next.js app
|
||||
3. Static export to `/out` directory for GitHub Pages
|
||||
|
||||
### Local Development
|
||||
- Uses Turbopack for fast HMR
|
||||
- Port 3003 (configurable)
|
||||
- MDX hot reload via Fumadocs
|
||||
|
||||
## Component vs Block Distinction
|
||||
|
||||
**Components** (149 total):
|
||||
- Single-purpose UI elements
|
||||
- Examples: Button, Input, Card, Dialog, Dropdown
|
||||
- Used as building blocks
|
||||
- Installed individually via CLI
|
||||
|
||||
**Blocks** (24+ available):
|
||||
- Full viewport sections
|
||||
- Examples: dashboard-01 (admin layout), login-03 (auth page), sidebar-07
|
||||
- Compose multiple components
|
||||
- Ready-to-use page sections
|
||||
- Users copy entire blocks, not install via CLI
|
||||
|
||||
## Import Patterns
|
||||
|
||||
Components can be imported three ways:
|
||||
|
||||
**1. Package Import** (if using @hanzo/ui package):
|
||||
```tsx
|
||||
import { Button, Card } from "@hanzo/ui"
|
||||
```
|
||||
|
||||
**2. Registry Import** (in documentation/examples):
|
||||
```tsx
|
||||
import { Button } from "@/registry/default/ui/button"
|
||||
```
|
||||
|
||||
**3. Local Import** (after installing via CLI):
|
||||
```tsx
|
||||
import { Button } from "@/components/ui/button"
|
||||
```
|
||||
|
||||
The registry uses `@/registry/{style}/` which gets rewritten to `@/components/` during installation.
|
||||
|
||||
## MDX Component System
|
||||
|
||||
Components available in all MDX files:
|
||||
|
||||
- `<ComponentPreview name="button-demo" />` - Live demo
|
||||
- `<ComponentSource name="button" />` - Source code display
|
||||
- `<ComponentExample>` - Example wrapper
|
||||
- `<CodeTabs>` - Installation tabs (requires useConfig hook)
|
||||
- `<Steps>` / `<Step>` - Step-by-step instructions
|
||||
- `<Tabs>` / `<TabsList>` / `<TabsTrigger>` / `<TabsContent>` - Content tabs
|
||||
- `<Callout>` - Info/warning callouts
|
||||
|
||||
## Testing
|
||||
|
||||
No formal test suite currently. Testing is done via:
|
||||
1. Local dev server visual testing
|
||||
2. Build process catches type errors
|
||||
3. Registry validation ensures component metadata is correct
|
||||
|
||||
## Deployment
|
||||
|
||||
**GitHub Pages** (automatic):
|
||||
- Workflow: `.github/workflows/deploy-gh-pages.yml`
|
||||
- Triggers on push to main
|
||||
- Builds app + pkg/ui package
|
||||
- Deploys to ui.hanzo.ai
|
||||
- Requires: npm-run-all, del-cli for dependency builds
|
||||
|
||||
**Important**: The GitHub Actions workflow builds the `pkg/ui` package first, then the app, to ensure registry has latest component code.
|
||||
|
||||
## Common Patterns
|
||||
|
||||
### Stub Components
|
||||
Components marked for future implementation:
|
||||
```tsx
|
||||
export default function Component() {
|
||||
return (
|
||||
<div className="flex items-center justify-center p-8">
|
||||
<p className="text-muted-foreground">Component coming soon</p>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
Currently 34 stubs remaining (documented but not implemented).
|
||||
|
||||
### Component Dependencies
|
||||
- Radix UI primitives for accessible base components
|
||||
- `cn()` utility from `@/lib/utils` for className merging
|
||||
- `cva` (class-variance-authority) for variant management
|
||||
- Lucide React for icons
|
||||
- Framer Motion for animations (some components)
|
||||
|
||||
### Registry Dependencies
|
||||
Components can depend on other registry components:
|
||||
```json
|
||||
{
|
||||
"registryDependencies": ["button", "dialog", "utils"]
|
||||
}
|
||||
```
|
||||
|
||||
These are auto-installed when user adds the component.
|
||||
|
||||
## Recent Architectural Changes (2025-10-05)
|
||||
|
||||
1. **Migrated from Contentlayer to Fumadocs** for MDX processing
|
||||
2. **Upgraded to Next.js 15.3.1** with Turbopack
|
||||
3. **React 19** with new JSX transform
|
||||
4. **Added @dnd-kit** for page builder drag-drop
|
||||
5. **Synced with hanzo/ui v3.4.0** (7 new components: button-group, empty, field, input-group, item, kbd, spinner)
|
||||
6. **Electric blue primary color** (210 100% 50%)
|
||||
7. **Page builder feature** at `/builder` route
|
||||
|
||||
## Notes for AI Assistants
|
||||
|
||||
- **Registry must rebuild** after component changes: `pnpm registry:build`
|
||||
- **Two theme variants** must be kept in sync: default and new-york
|
||||
- **Import paths** in components use `@/registry/{style}/` which becomes `@/components/` after CLI install
|
||||
- **MDX components** are defined in `mdx-components.tsx`, not auto-imported
|
||||
- **Blocks are different** from components - they're full-page sections, not installable via CLI
|
||||
- **GitHub Pages deployment** requires building pkg/ui package first (workflow handles this)
|
||||
|
||||
## LLM API Route (Added 2025-11-05)
|
||||
|
||||
The `/llm` API route serves MDX documentation optimized for LLM consumption by replacing `<ComponentPreview>` tags with actual component source code.
|
||||
|
||||
### Files Created
|
||||
|
||||
**`/lib/llm.ts`**:
|
||||
- `processMdxForLLMs(content, style)` - Transforms MDX for LLMs
|
||||
- Finds `<ComponentPreview name="..." />` tags in MDX
|
||||
- Replaces with actual component source code in markdown code blocks
|
||||
- Rewrites import paths from `@/registry/${style}/` to `@/components/`
|
||||
- Removes `export default` in favor of named exports
|
||||
|
||||
**`/app/(app)/llm/[[...slug]]/route.ts`**:
|
||||
- API route handler using Next.js catch-all routing
|
||||
- `GET` handler that processes MDX files via `processMdxForLLMs()`
|
||||
- Returns Content-Type: `text/markdown; charset=utf-8`
|
||||
- Supports static generation via `generateStaticParams()`
|
||||
- Uses `revalidate = false` for static export
|
||||
|
||||
### Key Adaptations from shadcn/ui
|
||||
|
||||
1. **Dynamic Style Support**: Changed from hardcoded `@/registry/new-york-v4/` to dynamic `@/registry/${style}/`
|
||||
2. **Multi-Style System**: Works with both "default" and "new-york" themes
|
||||
3. **Active Style Detection**: Uses `getActiveStyle()` to determine current theme
|
||||
|
||||
### Usage
|
||||
|
||||
Access documentation via:
|
||||
```bash
|
||||
# Get LLM-optimized docs for a component
|
||||
curl http://localhost:3003/llm/docs/components/button
|
||||
|
||||
# Returns MDX with inline source code instead of component previews
|
||||
```
|
||||
|
||||
## Deviation Report: shadcn/ui v4 vs Hanzo UI
|
||||
|
||||
### Directory Structure Comparison
|
||||
|
||||
**Copied from shadcn/ui v4**:
|
||||
- ✅ `/blocks` - Block showcase page with filtering and search
|
||||
- ✅ `/charts` - Chart component demonstrations
|
||||
- ✅ `/colors` - Tailwind color palette reference
|
||||
- ✅ `/docs` - Component documentation
|
||||
- ✅ `/examples` - Component usage examples
|
||||
- ✅ `/llm` - LLM-optimized API endpoint
|
||||
- ✅ `/themes` - Theme customization page
|
||||
|
||||
**Hanzo-Specific Features** (not in shadcn):
|
||||
- 🆕 `/ai` - AI assistant integration page
|
||||
- 🆕 `/builder` - Visual page builder with drag-drop
|
||||
- 🆕 `/components` - Component category pages
|
||||
- 🆕 `/compose` - Composition editor
|
||||
- 🆕 `/health` - System health check
|
||||
- 🆕 `/mcp` - Model Context Protocol integration
|
||||
- 🆕 `/sink` - Kitchen sink / testing page
|
||||
- 🆕 `/theme-generator` - Interactive theme generator
|
||||
|
||||
### Key Implementation Differences
|
||||
|
||||
#### 1. Path Handling
|
||||
- **shadcn**: Hardcoded `@/registry/new-york-v4/` paths
|
||||
- **Hanzo**: Dynamic `@/registry/${style}/` supporting multiple themes
|
||||
|
||||
#### 2. Style System
|
||||
- **shadcn**: Primary style is "new-york-v4"
|
||||
- **Hanzo**: Primary style is "default", with "new-york" as alternative
|
||||
- **Impact**: All copied files had paths adapted: `new-york-v4` → `default`
|
||||
|
||||
#### 3. Branding
|
||||
- **shadcn**: GitHub logo, shadcn branding
|
||||
- **Hanzo**: Hanzo logo (OpenInHButton), Hanzo branding
|
||||
- **Files Modified**:
|
||||
- `components/open-in-hanzo-button.tsx` → `components/open-in-h-button.tsx`
|
||||
- Hanzo logo (H icon) replaces GitHub logo
|
||||
|
||||
#### 4. Registry Structure
|
||||
- **shadcn**: `Index[name]` - flat structure
|
||||
- **Hanzo**: `Index[style][name]` - nested by style
|
||||
- **Impact**: Required updates to registry accessor functions
|
||||
|
||||
#### 5. Component Count
|
||||
- **shadcn v4**: ~58 core components
|
||||
- **Hanzo**: 149 components (115 implemented, 34 stubs)
|
||||
- **Unique to Hanzo**: 3D components, AI components, animation components, specialized navigation
|
||||
|
||||
### Files Requiring Ongoing Sync
|
||||
|
||||
When shadcn/ui v4 updates these, Hanzo should review for updates:
|
||||
|
||||
1. **Layout Files**:
|
||||
- `app/(app)/blocks/layout.tsx`
|
||||
- `app/(app)/colors/layout.tsx`
|
||||
- `app/(app)/themes/layout.tsx`
|
||||
|
||||
2. **Page Files**:
|
||||
- `app/(app)/blocks/page.tsx`
|
||||
- `app/(app)/colors/page.tsx`
|
||||
- `app/(app)/themes/page.tsx`
|
||||
- `app/(app)/llm/[[...slug]]/route.ts`
|
||||
|
||||
3. **Supporting Components**:
|
||||
- `components/blocks-nav.tsx`
|
||||
- `components/colors-nav.tsx`
|
||||
- `components/theme-customizer.tsx`
|
||||
- `lib/llm.ts`
|
||||
|
||||
4. **Registry Functions**:
|
||||
- `lib/registry.ts` - Any changes to registry access patterns
|
||||
- `lib/blocks.ts` - Block metadata handling
|
||||
|
||||
### Known Issues Requiring Fixes
|
||||
|
||||
Per user feedback: "don't suppress errors just do shit right or cover up things"
|
||||
|
||||
**1. getActiveStyle Export Error** ✅ FIXED (2025-11-05):
|
||||
- **Was**: Server logs showed "Export getActiveStyle doesn't exist in target module"
|
||||
- **Root Cause**: Turbopack had issues with `async function getActiveStyle()` in `force-static` pages. The async nature conflicted with Turbopack's static analysis in force-static mode.
|
||||
- **Fix**: Made `getActiveStyle()` synchronous by removing `async` keyword
|
||||
- Updated `registry/styles.ts` line 14: removed `async` from function declaration
|
||||
- Updated `app/(app)/blocks/page.tsx`: removed `async` from function and `await` from call
|
||||
- Updated `app/(app)/llm/[[...slug]]/route.ts`: split Promise.all, called getActiveStyle() synchronously
|
||||
- **Result**: ✅ NO MORE EXPORT ERRORS - Pages load cleanly with HTTP 200, no server errors
|
||||
- **Note**: Function doesn't actually need to be async since it just returns styles[0]
|
||||
|
||||
**2. Client Component Boundary Warnings** ⚠️ LOW PRIORITY:
|
||||
- **Symptom**: "Functions cannot be passed directly to Client Components unless you explicitly expose it by marking it with 'use server'"
|
||||
- **Locations**: Multiple components in blocks, themes, colors pages (7 warnings with digest `638247047`)
|
||||
- **Status**: WARNINGS ONLY - Not breaking, pages render correctly with HTTP 200
|
||||
- **Impact**: No user-facing issues, functionality fully intact
|
||||
- **Note**: These warnings also appear in shadcn/ui v4, suggesting they may be framework-level issues
|
||||
- **Action**: Can investigate if desired, but not critical since pages work perfectly
|
||||
|
||||
**3. Tabs Hydration Issues** ✅ FIXED:
|
||||
- **Was**: Tabs component had hydration errors due to SSR/client mismatch
|
||||
- **Fix**: Updated `registry/default/ui/tabs.tsx` with proper refs and display names
|
||||
- **Status**: ✅ RESOLVED
|
||||
|
||||
**4. macOS Dock Demo SSR Error** ✅ FIXED (2025-11-06):
|
||||
- **Was**: Build error "Event handlers cannot be passed to Client Component props" on `/view/new-york/macos-dock-demo`
|
||||
- **Root Cause**: `macos-dock-demo` component has onClick event handlers on DockItem components. During static prerendering with `force-static`, Next.js tries to serialize props but can't serialize functions.
|
||||
- **Fix**: Added component to skip list in `generateStaticParams()` in `app/(view)/view/[name]/page.tsx`
|
||||
- Components with event handlers that can't be serialized are excluded from static generation
|
||||
- They can still be accessed dynamically at runtime
|
||||
- **Result**: ✅ Build completes successfully, no SSR errors
|
||||
- **Note**: Component works perfectly in dev mode and at runtime, just skips static prerendering
|
||||
|
||||
### Testing Coverage
|
||||
|
||||
**Pages Tested**:
|
||||
- ✅ `/blocks` - All 5 featured blocks display correctly
|
||||
- ✅ `/colors` - Full Tailwind palette with copy functionality
|
||||
- ✅ `/themes` - Theme customizer with live preview
|
||||
- ✅ `/llm` - API endpoint compiles and serves MDX
|
||||
|
||||
**Components Tested**:
|
||||
- ✅ `<BlockDisplay>` - Renders blocks correctly
|
||||
- ✅ `<BlocksNav>` - Navigation with search/filter
|
||||
- ✅ `<ColorsNav>` - Color palette navigation
|
||||
- ✅ `<OpenInHButton>` - Hanzo-branded button with logo
|
||||
- ✅ `<Tabs>` - No hydration errors
|
||||
|
||||
### Maintenance Strategy
|
||||
|
||||
**When to Sync with shadcn/ui v4**:
|
||||
1. Major version updates (v4.x → v4.y)
|
||||
2. New features in /blocks, /themes, /colors pages
|
||||
3. Security patches or bug fixes
|
||||
4. New component additions to showcase pages
|
||||
|
||||
**How to Sync**:
|
||||
1. Compare shadcn v4 file with Hanzo version
|
||||
2. Identify changes in logic (not just branding)
|
||||
3. Apply logic changes while preserving:
|
||||
- Hanzo branding (OpenInHButton, logos)
|
||||
- Path adaptations (new-york-v4 → default)
|
||||
- Registry structure differences (Index[style][name])
|
||||
4. Test thoroughly before deploying
|
||||
5. Document changes in CLAUDE.md
|
||||
|
||||
### Future Work
|
||||
|
||||
**Improvements Needed**:
|
||||
1. Fix getActiveStyle export error properly (not suppress)
|
||||
2. Resolve Client Component boundary warnings
|
||||
3. Add comprehensive test suite for copied pages
|
||||
4. Set up automated diff checking against shadcn v4
|
||||
5. Document all customizations in component headers
|
||||
@@ -1,45 +0,0 @@
|
||||
import { Button } from "@/registry/default/ui/button"
|
||||
import {
|
||||
Card,
|
||||
CardContent,
|
||||
CardDescription,
|
||||
CardFooter,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
} from "@/registry/default/ui/card"
|
||||
import { Input } from "@/registry/default/ui/input"
|
||||
import { Label } from "@/registry/default/ui/label"
|
||||
|
||||
export const description =
|
||||
"A simple login form with email and password. The submit button says 'Sign in'."
|
||||
|
||||
export const iframeHeight = "600px"
|
||||
|
||||
export const containerClassName =
|
||||
"w-full h-screen flex items-center justify-center px-4"
|
||||
|
||||
export default function LoginForm() {
|
||||
return (
|
||||
<Card className="w-full max-w-sm">
|
||||
<CardHeader>
|
||||
<CardTitle className="text-2xl">Login</CardTitle>
|
||||
<CardDescription>
|
||||
Enter your email below to login to your account.
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="grid gap-4">
|
||||
<div className="grid gap-2">
|
||||
<Label htmlFor="email">Email</Label>
|
||||
<Input id="email" type="email" placeholder="m@example.com" required />
|
||||
</div>
|
||||
<div className="grid gap-2">
|
||||
<Label htmlFor="password">Password</Label>
|
||||
<Input id="password" type="password" required />
|
||||
</div>
|
||||
</CardContent>
|
||||
<CardFooter>
|
||||
<Button className="w-full">Sign in</Button>
|
||||
</CardFooter>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
@@ -1,67 +0,0 @@
|
||||
import Link from "next/link"
|
||||
|
||||
import { Button } from "@/registry/default/ui/button"
|
||||
import {
|
||||
Card,
|
||||
CardContent,
|
||||
CardDescription,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
} from "@/registry/default/ui/card"
|
||||
import { Input } from "@/registry/default/ui/input"
|
||||
import { Label } from "@/registry/default/ui/label"
|
||||
|
||||
export const description =
|
||||
"A login form with email and password. There's an option to login with Google and a link to sign up if you don't have an account."
|
||||
|
||||
export const iframeHeight = "600px"
|
||||
|
||||
export const containerClassName =
|
||||
"w-full h-screen flex items-center justify-center px-4"
|
||||
|
||||
export default function LoginForm() {
|
||||
return (
|
||||
<Card className="mx-auto max-w-sm">
|
||||
<CardHeader>
|
||||
<CardTitle className="text-2xl">Login</CardTitle>
|
||||
<CardDescription>
|
||||
Enter your email below to login to your account
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="grid gap-4">
|
||||
<div className="grid gap-2">
|
||||
<Label htmlFor="email">Email</Label>
|
||||
<Input
|
||||
id="email"
|
||||
type="email"
|
||||
placeholder="m@example.com"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
<div className="grid gap-2">
|
||||
<div className="flex items-center">
|
||||
<Label htmlFor="password">Password</Label>
|
||||
<Link href="#" className="ml-auto inline-block text-sm underline">
|
||||
Forgot your password?
|
||||
</Link>
|
||||
</div>
|
||||
<Input id="password" type="password" required />
|
||||
</div>
|
||||
<Button type="submit" className="w-full">
|
||||
Login
|
||||
</Button>
|
||||
<Button variant="outline" className="w-full">
|
||||
Login with Google
|
||||
</Button>
|
||||
</div>
|
||||
<div className="mt-4 text-center text-sm">
|
||||
Don't have an account?{" "}
|
||||
<Link href="#" className="underline">
|
||||
Sign up
|
||||
</Link>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
@@ -1,72 +0,0 @@
|
||||
import Link from "next/link"
|
||||
|
||||
import { Button } from "@/registry/default/ui/button"
|
||||
import {
|
||||
Card,
|
||||
CardContent,
|
||||
CardDescription,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
} from "@/registry/default/ui/card"
|
||||
import { Input } from "@/registry/default/ui/input"
|
||||
import { Label } from "@/registry/default/ui/label"
|
||||
|
||||
export const description =
|
||||
"A sign up form with first name, last name, email and password inside a card. There's an option to sign up with GitHub and a link to login if you already have an account"
|
||||
|
||||
export const iframeHeight = "600px"
|
||||
|
||||
export const containerClassName =
|
||||
"w-full h-screen flex items-center justify-center px-4"
|
||||
|
||||
export default function LoginForm() {
|
||||
return (
|
||||
<Card className="mx-auto max-w-sm">
|
||||
<CardHeader>
|
||||
<CardTitle className="text-xl">Sign Up</CardTitle>
|
||||
<CardDescription>
|
||||
Enter your information to create an account
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="grid gap-4">
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div className="grid gap-2">
|
||||
<Label htmlFor="first-name">First name</Label>
|
||||
<Input id="first-name" placeholder="Max" required />
|
||||
</div>
|
||||
<div className="grid gap-2">
|
||||
<Label htmlFor="last-name">Last name</Label>
|
||||
<Input id="last-name" placeholder="Robinson" required />
|
||||
</div>
|
||||
</div>
|
||||
<div className="grid gap-2">
|
||||
<Label htmlFor="email">Email</Label>
|
||||
<Input
|
||||
id="email"
|
||||
type="email"
|
||||
placeholder="m@example.com"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
<div className="grid gap-2">
|
||||
<Label htmlFor="password">Password</Label>
|
||||
<Input id="password" type="password" />
|
||||
</div>
|
||||
<Button type="submit" className="w-full">
|
||||
Create an account
|
||||
</Button>
|
||||
<Button variant="outline" className="w-full">
|
||||
Sign up with GitHub
|
||||
</Button>
|
||||
</div>
|
||||
<div className="mt-4 text-center text-sm">
|
||||
Already have an account?{" "}
|
||||
<Link href="#" className="underline">
|
||||
Sign in
|
||||
</Link>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
@@ -1,75 +0,0 @@
|
||||
import Image from "next/image"
|
||||
import Link from "next/link"
|
||||
|
||||
import { Button } from "@/registry/default/ui/button"
|
||||
import { Input } from "@/registry/default/ui/input"
|
||||
import { Label } from "@/registry/default/ui/label"
|
||||
|
||||
export const description =
|
||||
"A login page with two columns. The first column has the login form with email and password. There's a Forgot your passwork link and a link to sign up if you do not have an account. The second column has a cover image."
|
||||
|
||||
export const iframeHeight = "800px"
|
||||
|
||||
export const containerClassName = "w-full h-full p-4 lg:p-0"
|
||||
|
||||
export default function Dashboard() {
|
||||
return (
|
||||
<div className="w-full lg:grid lg:min-h-[600px] lg:grid-cols-2 xl:min-h-[800px]">
|
||||
<div className="flex items-center justify-center py-12">
|
||||
<div className="mx-auto grid w-[350px] gap-6">
|
||||
<div className="grid gap-2 text-center">
|
||||
<h1 className="text-3xl font-bold">Login</h1>
|
||||
<p className="text-balance text-muted-foreground">
|
||||
Enter your email below to login to your account
|
||||
</p>
|
||||
</div>
|
||||
<div className="grid gap-4">
|
||||
<div className="grid gap-2">
|
||||
<Label htmlFor="email">Email</Label>
|
||||
<Input
|
||||
id="email"
|
||||
type="email"
|
||||
placeholder="m@example.com"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
<div className="grid gap-2">
|
||||
<div className="flex items-center">
|
||||
<Label htmlFor="password">Password</Label>
|
||||
<Link
|
||||
href="/forgot-password"
|
||||
className="ml-auto inline-block text-sm underline"
|
||||
>
|
||||
Forgot your password?
|
||||
</Link>
|
||||
</div>
|
||||
<Input id="password" type="password" required />
|
||||
</div>
|
||||
<Button type="submit" className="w-full">
|
||||
Login
|
||||
</Button>
|
||||
<Button variant="outline" className="w-full">
|
||||
Login with Google
|
||||
</Button>
|
||||
</div>
|
||||
<div className="mt-4 text-center text-sm">
|
||||
Don't have an account?{" "}
|
||||
<Link href="#" className="underline">
|
||||
Sign up
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="hidden bg-muted lg:block">
|
||||
<Image
|
||||
src="/placeholder.svg"
|
||||
alt="Authentication page cover image"
|
||||
width="1920"
|
||||
height="1080"
|
||||
className="h-full w-full object-cover dark:brightness-[0.2] dark:grayscale"
|
||||
loading="lazy"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -1,21 +0,0 @@
|
||||
"use client"
|
||||
|
||||
import * as React from "react"
|
||||
|
||||
import { Calendar } from "@/registry/default/ui/calendar"
|
||||
|
||||
export default function Calendar01() {
|
||||
const [date, setDate] = React.useState<Date | undefined>(
|
||||
new Date(2025, 5, 12)
|
||||
)
|
||||
|
||||
return (
|
||||
<Calendar
|
||||
mode="single"
|
||||
defaultMonth={date}
|
||||
selected={date}
|
||||
onSelect={setDate}
|
||||
className="rounded-lg border shadow-sm"
|
||||
/>
|
||||
)
|
||||
}
|
||||
@@ -1,22 +0,0 @@
|
||||
"use client"
|
||||
|
||||
import * as React from "react"
|
||||
|
||||
import { Calendar } from "@/registry/default/ui/calendar"
|
||||
|
||||
export default function Calendar02() {
|
||||
const [date, setDate] = React.useState<Date | undefined>(
|
||||
new Date(2025, 5, 12)
|
||||
)
|
||||
|
||||
return (
|
||||
<Calendar
|
||||
mode="single"
|
||||
defaultMonth={date}
|
||||
numberOfMonths={2}
|
||||
selected={date}
|
||||
onSelect={setDate}
|
||||
className="rounded-lg border shadow-sm"
|
||||
/>
|
||||
)
|
||||
}
|
||||
@@ -1,25 +0,0 @@
|
||||
"use client"
|
||||
|
||||
import * as React from "react"
|
||||
|
||||
import { Calendar } from "@/registry/default/ui/calendar"
|
||||
|
||||
export default function Calendar03() {
|
||||
const [dates, setDates] = React.useState<Date[]>([
|
||||
new Date(2025, 5, 12),
|
||||
new Date(2025, 6, 24),
|
||||
])
|
||||
|
||||
return (
|
||||
<Calendar
|
||||
mode="multiple"
|
||||
numberOfMonths={2}
|
||||
defaultMonth={dates[0]}
|
||||
required
|
||||
selected={dates}
|
||||
onSelect={setDates}
|
||||
max={5}
|
||||
className="rounded-lg border shadow-sm"
|
||||
/>
|
||||
)
|
||||
}
|
||||
@@ -1,23 +0,0 @@
|
||||
"use client"
|
||||
|
||||
import * as React from "react"
|
||||
import { type DateRange } from "react-day-picker"
|
||||
|
||||
import { Calendar } from "@/registry/default/ui/calendar"
|
||||
|
||||
export default function Calendar04() {
|
||||
const [dateRange, setDateRange] = React.useState<DateRange | undefined>({
|
||||
from: new Date(2025, 5, 9),
|
||||
to: new Date(2025, 5, 26),
|
||||
})
|
||||
|
||||
return (
|
||||
<Calendar
|
||||
mode="range"
|
||||
defaultMonth={dateRange?.from}
|
||||
selected={dateRange}
|
||||
onSelect={setDateRange}
|
||||
className="rounded-lg border shadow-sm"
|
||||
/>
|
||||
)
|
||||
}
|
||||
@@ -1,24 +0,0 @@
|
||||
"use client"
|
||||
|
||||
import * as React from "react"
|
||||
import { type DateRange } from "react-day-picker"
|
||||
|
||||
import { Calendar } from "@/registry/default/ui/calendar"
|
||||
|
||||
export default function Calendar05() {
|
||||
const [dateRange, setDateRange] = React.useState<DateRange | undefined>({
|
||||
from: new Date(2025, 5, 12),
|
||||
to: new Date(2025, 6, 15),
|
||||
})
|
||||
|
||||
return (
|
||||
<Calendar
|
||||
mode="range"
|
||||
defaultMonth={dateRange?.from}
|
||||
selected={dateRange}
|
||||
onSelect={setDateRange}
|
||||
numberOfMonths={2}
|
||||
className="rounded-lg border shadow-sm"
|
||||
/>
|
||||
)
|
||||
}
|
||||
@@ -1,30 +0,0 @@
|
||||
"use client"
|
||||
|
||||
import * as React from "react"
|
||||
import { type DateRange } from "react-day-picker"
|
||||
|
||||
import { Calendar } from "@/registry/default/ui/calendar"
|
||||
|
||||
export default function Calendar06() {
|
||||
const [dateRange, setDateRange] = React.useState<DateRange | undefined>({
|
||||
from: new Date(2025, 5, 12),
|
||||
to: new Date(2025, 5, 26),
|
||||
})
|
||||
|
||||
return (
|
||||
<div className="flex min-w-0 flex-col gap-2">
|
||||
<Calendar
|
||||
mode="range"
|
||||
defaultMonth={dateRange?.from}
|
||||
selected={dateRange}
|
||||
onSelect={setDateRange}
|
||||
numberOfMonths={1}
|
||||
min={5}
|
||||
className="rounded-lg border shadow-sm"
|
||||
/>
|
||||
<div className="text-muted-foreground text-center text-xs">
|
||||
A minimum of 5 days is required
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -1,31 +0,0 @@
|
||||
"use client"
|
||||
|
||||
import * as React from "react"
|
||||
import { type DateRange } from "react-day-picker"
|
||||
|
||||
import { Calendar } from "@/registry/default/ui/calendar"
|
||||
|
||||
export default function Calendar07() {
|
||||
const [dateRange, setDateRange] = React.useState<DateRange | undefined>({
|
||||
from: new Date(2025, 5, 18),
|
||||
to: new Date(2025, 6, 7),
|
||||
})
|
||||
|
||||
return (
|
||||
<div className="flex min-w-0 flex-col gap-2">
|
||||
<Calendar
|
||||
mode="range"
|
||||
defaultMonth={dateRange?.from}
|
||||
selected={dateRange}
|
||||
onSelect={setDateRange}
|
||||
numberOfMonths={2}
|
||||
min={2}
|
||||
max={20}
|
||||
className="rounded-lg border shadow-sm"
|
||||
/>
|
||||
<div className="text-muted-foreground text-center text-xs">
|
||||
Your stay must be between 2 and 20 nights
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -1,24 +0,0 @@
|
||||
"use client"
|
||||
|
||||
import * as React from "react"
|
||||
|
||||
import { Calendar } from "@/registry/default/ui/calendar"
|
||||
|
||||
export default function Calendar08() {
|
||||
const [date, setDate] = React.useState<Date | undefined>(
|
||||
new Date(2025, 5, 12)
|
||||
)
|
||||
|
||||
return (
|
||||
<Calendar
|
||||
mode="single"
|
||||
defaultMonth={date}
|
||||
selected={date}
|
||||
onSelect={setDate}
|
||||
disabled={{
|
||||
before: new Date(2025, 5, 12),
|
||||
}}
|
||||
className="rounded-lg border shadow-sm"
|
||||
/>
|
||||
)
|
||||
}
|
||||
@@ -1,26 +0,0 @@
|
||||
"use client"
|
||||
|
||||
import * as React from "react"
|
||||
import { type DateRange } from "react-day-picker"
|
||||
|
||||
import { Calendar } from "@/registry/default/ui/calendar"
|
||||
|
||||
export default function Calendar09() {
|
||||
const [dateRange, setDateRange] = React.useState<DateRange | undefined>({
|
||||
from: new Date(2025, 5, 17),
|
||||
to: new Date(2025, 5, 20),
|
||||
})
|
||||
|
||||
return (
|
||||
<Calendar
|
||||
mode="range"
|
||||
defaultMonth={dateRange?.from}
|
||||
selected={dateRange}
|
||||
onSelect={setDateRange}
|
||||
numberOfMonths={2}
|
||||
disabled={{ dayOfWeek: [0, 6] }}
|
||||
className="rounded-lg border shadow-sm"
|
||||
excludeDisabled
|
||||
/>
|
||||
)
|
||||
}
|
||||
@@ -1,50 +0,0 @@
|
||||
"use client"
|
||||
|
||||
import * as React from "react"
|
||||
|
||||
import { Button } from "@/registry/default/ui/button"
|
||||
import { Calendar } from "@/registry/default/ui/calendar"
|
||||
import {
|
||||
Card,
|
||||
CardContent,
|
||||
CardDescription,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
} from "@/registry/default/ui/card"
|
||||
|
||||
export default function Calendar10() {
|
||||
const [date, setDate] = React.useState<Date | undefined>(
|
||||
new Date(2025, 5, 12)
|
||||
)
|
||||
const [month, setMonth] = React.useState<Date | undefined>(new Date())
|
||||
|
||||
return (
|
||||
<Card>
|
||||
<CardHeader className="relative">
|
||||
<CardTitle>Appointment</CardTitle>
|
||||
<CardDescription>Find a date</CardDescription>
|
||||
<Button
|
||||
size="sm"
|
||||
variant="outline"
|
||||
className="absolute right-4 top-4"
|
||||
onClick={() => {
|
||||
setMonth(new Date())
|
||||
setDate(new Date())
|
||||
}}
|
||||
>
|
||||
Today
|
||||
</Button>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<Calendar
|
||||
mode="single"
|
||||
month={month}
|
||||
onMonthChange={setMonth}
|
||||
selected={date}
|
||||
onSelect={setDate}
|
||||
className="bg-transparent p-0"
|
||||
/>
|
||||
</CardContent>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
@@ -1,31 +0,0 @@
|
||||
"use client"
|
||||
|
||||
import * as React from "react"
|
||||
import { type DateRange } from "react-day-picker"
|
||||
|
||||
import { Calendar } from "@/registry/default/ui/calendar"
|
||||
|
||||
export default function Calendar11() {
|
||||
const [dateRange, setDateRange] = React.useState<DateRange | undefined>({
|
||||
from: new Date(2025, 5, 17),
|
||||
to: new Date(2025, 5, 20),
|
||||
})
|
||||
|
||||
return (
|
||||
<div className="flex min-w-0 flex-col gap-2">
|
||||
<Calendar
|
||||
mode="range"
|
||||
selected={dateRange}
|
||||
onSelect={setDateRange}
|
||||
numberOfMonths={2}
|
||||
startMonth={new Date(2025, 5, 1)}
|
||||
endMonth={new Date(2025, 6, 31)}
|
||||
disableNavigation
|
||||
className="rounded-lg border shadow-sm"
|
||||
/>
|
||||
<div className="text-muted-foreground text-center text-xs">
|
||||
We are open in June and July only.
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -1,77 +0,0 @@
|
||||
"use client"
|
||||
|
||||
import * as React from "react"
|
||||
import { type DateRange } from "react-day-picker"
|
||||
import { enUS, es } from "react-day-picker/locale"
|
||||
|
||||
import { Calendar } from "@/registry/default/ui/calendar"
|
||||
import {
|
||||
Card,
|
||||
CardContent,
|
||||
CardDescription,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
} from "@/registry/default/ui/card"
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from "@/registry/default/ui/select"
|
||||
|
||||
const localizedStrings = {
|
||||
en: {
|
||||
title: "Book an appointment",
|
||||
description: "Select the dates for your appointment",
|
||||
},
|
||||
es: {
|
||||
title: "Reserva una cita",
|
||||
description: "Selecciona las fechas para tu cita",
|
||||
},
|
||||
} as const
|
||||
|
||||
export default function Calendar12() {
|
||||
const [locale, setLocale] =
|
||||
React.useState<keyof typeof localizedStrings>("es")
|
||||
const [dateRange, setDateRange] = React.useState<DateRange | undefined>({
|
||||
from: new Date(2025, 8, 9),
|
||||
to: new Date(2025, 8, 17),
|
||||
})
|
||||
|
||||
return (
|
||||
<Card>
|
||||
<CardHeader className="relative border-b">
|
||||
<CardTitle>{localizedStrings[locale].title}</CardTitle>
|
||||
<CardDescription>
|
||||
{localizedStrings[locale].description}
|
||||
</CardDescription>
|
||||
<Select
|
||||
value={locale}
|
||||
onValueChange={(value) =>
|
||||
setLocale(value as keyof typeof localizedStrings)
|
||||
}
|
||||
>
|
||||
<SelectTrigger className="absolute right-4 top-4 w-[100px]">
|
||||
<SelectValue placeholder="Language" />
|
||||
</SelectTrigger>
|
||||
<SelectContent align="end">
|
||||
<SelectItem value="es">Español</SelectItem>
|
||||
<SelectItem value="en">English</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</CardHeader>
|
||||
<CardContent className="pt-4">
|
||||
<Calendar
|
||||
mode="range"
|
||||
selected={dateRange}
|
||||
onSelect={setDateRange}
|
||||
defaultMonth={dateRange?.from}
|
||||
numberOfMonths={2}
|
||||
locale={locale === "es" ? es : enUS}
|
||||
className="bg-transparent p-0"
|
||||
/>
|
||||
</CardContent>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
@@ -1,58 +0,0 @@
|
||||
"use client"
|
||||
|
||||
import * as React from "react"
|
||||
|
||||
import { Calendar } from "@/registry/default/ui/calendar"
|
||||
import { Label } from "@/registry/default/ui/label"
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from "@/registry/default/ui/select"
|
||||
|
||||
export default function Calendar13() {
|
||||
const [dropdown, setDropdown] =
|
||||
React.useState<React.ComponentProps<typeof Calendar>["captionLayout"]>(
|
||||
"dropdown"
|
||||
)
|
||||
const [date, setDate] = React.useState<Date | undefined>(
|
||||
new Date(2025, 5, 12)
|
||||
)
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-4">
|
||||
<Calendar
|
||||
mode="single"
|
||||
defaultMonth={date}
|
||||
selected={date}
|
||||
onSelect={setDate}
|
||||
captionLayout={dropdown}
|
||||
className="rounded-lg border shadow-sm"
|
||||
/>
|
||||
<div className="flex flex-col gap-3">
|
||||
<Label htmlFor="dropdown" className="px-1">
|
||||
Dropdown
|
||||
</Label>
|
||||
<Select
|
||||
value={dropdown}
|
||||
onValueChange={(value) =>
|
||||
setDropdown(
|
||||
value as React.ComponentProps<typeof Calendar>["captionLayout"]
|
||||
)
|
||||
}
|
||||
>
|
||||
<SelectTrigger id="dropdown" className="bg-background w-full">
|
||||
<SelectValue placeholder="Dropdown" />
|
||||
</SelectTrigger>
|
||||
<SelectContent align="center">
|
||||
<SelectItem value="dropdown">Month and Year</SelectItem>
|
||||
<SelectItem value="dropdown-months">Month Only</SelectItem>
|
||||
<SelectItem value="dropdown-years">Year Only</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -1,32 +0,0 @@
|
||||
"use client"
|
||||
|
||||
import * as React from "react"
|
||||
|
||||
import { Calendar } from "@/registry/default/ui/calendar"
|
||||
|
||||
export default function Calendar14() {
|
||||
const [date, setDate] = React.useState<Date | undefined>(
|
||||
new Date(2025, 5, 12)
|
||||
)
|
||||
const bookedDates = Array.from(
|
||||
{ length: 12 },
|
||||
(_, i) => new Date(2025, 5, 15 + i)
|
||||
)
|
||||
|
||||
return (
|
||||
<Calendar
|
||||
mode="single"
|
||||
defaultMonth={date}
|
||||
selected={date}
|
||||
onSelect={setDate}
|
||||
disabled={bookedDates}
|
||||
modifiers={{
|
||||
booked: bookedDates,
|
||||
}}
|
||||
modifiersClassNames={{
|
||||
booked: "[&>button]:line-through opacity-100",
|
||||
}}
|
||||
className="rounded-lg border shadow-sm"
|
||||
/>
|
||||
)
|
||||
}
|
||||
@@ -1,22 +0,0 @@
|
||||
"use client"
|
||||
|
||||
import * as React from "react"
|
||||
|
||||
import { Calendar } from "@/registry/default/ui/calendar"
|
||||
|
||||
export default function Calendar15() {
|
||||
const [date, setDate] = React.useState<Date | undefined>(
|
||||
new Date(2025, 5, 12)
|
||||
)
|
||||
|
||||
return (
|
||||
<Calendar
|
||||
mode="single"
|
||||
defaultMonth={date}
|
||||
selected={date}
|
||||
onSelect={setDate}
|
||||
className="rounded-lg border shadow-sm"
|
||||
showWeekNumber
|
||||
/>
|
||||
)
|
||||
}
|
||||
@@ -1,56 +0,0 @@
|
||||
"use client"
|
||||
|
||||
import * as React from "react"
|
||||
import { Clock2Icon } from "lucide-react"
|
||||
|
||||
import { Calendar } from "@/registry/default/ui/calendar"
|
||||
import { Card, CardContent, CardFooter } from "@/registry/default/ui/card"
|
||||
import { Input } from "@/registry/default/ui/input"
|
||||
import { Label } from "@/registry/default/ui/label"
|
||||
|
||||
export default function Calendar16() {
|
||||
const [date, setDate] = React.useState<Date | undefined>(
|
||||
new Date(2025, 5, 12)
|
||||
)
|
||||
|
||||
return (
|
||||
<Card className="w-fit py-4">
|
||||
<CardContent className="px-4">
|
||||
<Calendar
|
||||
mode="single"
|
||||
selected={date}
|
||||
onSelect={setDate}
|
||||
className="bg-transparent p-0"
|
||||
/>
|
||||
</CardContent>
|
||||
<CardFooter className="flex flex-col gap-6 border-t px-4 pb-0 pt-4">
|
||||
<div className="flex w-full flex-col gap-3">
|
||||
<Label htmlFor="time-from">Start Time</Label>
|
||||
<div className="relative flex w-full items-center gap-2">
|
||||
<Clock2Icon className="text-muted-foreground pointer-events-none absolute left-2.5 size-4 select-none" />
|
||||
<Input
|
||||
id="time-from"
|
||||
type="time"
|
||||
step="1"
|
||||
defaultValue="10:30:00"
|
||||
className="appearance-none pl-8 [&::-webkit-calendar-picker-indicator]:hidden [&::-webkit-calendar-picker-indicator]:appearance-none"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex w-full flex-col gap-3">
|
||||
<Label htmlFor="time-to">End Time</Label>
|
||||
<div className="relative flex w-full items-center gap-2">
|
||||
<Clock2Icon className="text-muted-foreground pointer-events-none absolute left-2.5 size-4 select-none" />
|
||||
<Input
|
||||
id="time-to"
|
||||
type="time"
|
||||
step="1"
|
||||
defaultValue="12:30:00"
|
||||
className="appearance-none pl-8 [&::-webkit-calendar-picker-indicator]:hidden [&::-webkit-calendar-picker-indicator]:appearance-none"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</CardFooter>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
@@ -1,54 +0,0 @@
|
||||
"use client"
|
||||
|
||||
import * as React from "react"
|
||||
|
||||
import { Calendar } from "@/registry/default/ui/calendar"
|
||||
import { Card, CardContent, CardFooter } from "@/registry/default/ui/card"
|
||||
import { Input } from "@/registry/default/ui/input"
|
||||
import { Label } from "@/registry/default/ui/label"
|
||||
|
||||
export default function Calendar17() {
|
||||
const [date, setDate] = React.useState<Date | undefined>(
|
||||
new Date(2025, 5, 12)
|
||||
)
|
||||
|
||||
return (
|
||||
<Card className="w-fit py-4">
|
||||
<CardContent className="px-4">
|
||||
<Calendar
|
||||
mode="single"
|
||||
selected={date}
|
||||
onSelect={setDate}
|
||||
className="bg-transparent p-0 [--cell-size:2.8rem]"
|
||||
/>
|
||||
</CardContent>
|
||||
<CardFooter className="*:[div]:w-full flex gap-2 border-t px-4 pb-0 pt-4">
|
||||
<div className="flex-1">
|
||||
<Label htmlFor="time-from" className="sr-only">
|
||||
Start Time
|
||||
</Label>
|
||||
<Input
|
||||
id="time-from"
|
||||
type="time"
|
||||
step="1"
|
||||
defaultValue="10:30:00"
|
||||
className="appearance-none [&::-webkit-calendar-picker-indicator]:hidden [&::-webkit-calendar-picker-indicator]:appearance-none"
|
||||
/>
|
||||
</div>
|
||||
<span>-</span>
|
||||
<div className="flex-1">
|
||||
<Label htmlFor="time-to" className="sr-only">
|
||||
End Time
|
||||
</Label>
|
||||
<Input
|
||||
id="time-to"
|
||||
type="time"
|
||||
step="1"
|
||||
defaultValue="12:30:00"
|
||||
className="appearance-none [&::-webkit-calendar-picker-indicator]:hidden [&::-webkit-calendar-picker-indicator]:appearance-none"
|
||||
/>
|
||||
</div>
|
||||
</CardFooter>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
@@ -1,20 +0,0 @@
|
||||
"use client"
|
||||
|
||||
import * as React from "react"
|
||||
|
||||
import { Calendar } from "@/registry/default/ui/calendar"
|
||||
|
||||
export default function Calendar18() {
|
||||
const [date, setDate] = React.useState<Date | undefined>(
|
||||
new Date(2025, 5, 12)
|
||||
)
|
||||
|
||||
return (
|
||||
<Calendar
|
||||
mode="single"
|
||||
selected={date}
|
||||
onSelect={setDate}
|
||||
className="rounded-lg border [--cell-size:2.75rem] md:[--cell-size:3rem]"
|
||||
/>
|
||||
)
|
||||
}
|
||||
@@ -1,50 +0,0 @@
|
||||
"use client"
|
||||
|
||||
import * as React from "react"
|
||||
import { addDays } from "date-fns"
|
||||
|
||||
import { Button } from "@/registry/default/ui/button"
|
||||
import { Calendar } from "@/registry/default/ui/calendar"
|
||||
import { Card, CardContent, CardFooter } from "@/registry/default/ui/card"
|
||||
|
||||
export default function Calendar19() {
|
||||
const [date, setDate] = React.useState<Date | undefined>(
|
||||
new Date(2025, 5, 12)
|
||||
)
|
||||
|
||||
return (
|
||||
<Card className="max-w-[300px] py-4">
|
||||
<CardContent className="px-4">
|
||||
<Calendar
|
||||
mode="single"
|
||||
selected={date}
|
||||
onSelect={setDate}
|
||||
defaultMonth={date}
|
||||
className="bg-transparent p-0 [--cell-size:2.375rem]"
|
||||
/>
|
||||
</CardContent>
|
||||
<CardFooter className="flex flex-wrap gap-2 border-t px-4 pb-0 pt-4">
|
||||
{[
|
||||
{ label: "Today", value: 0 },
|
||||
{ label: "Tomorrow", value: 1 },
|
||||
{ label: "In 3 days", value: 3 },
|
||||
{ label: "In a week", value: 7 },
|
||||
{ label: "In 2 weeks", value: 14 },
|
||||
].map((preset) => (
|
||||
<Button
|
||||
key={preset.value}
|
||||
variant="outline"
|
||||
size="sm"
|
||||
className="flex-1"
|
||||
onClick={() => {
|
||||
const newDate = addDays(new Date(), preset.value)
|
||||
setDate(newDate)
|
||||
}}
|
||||
>
|
||||
{preset.label}
|
||||
</Button>
|
||||
))}
|
||||
</CardFooter>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
@@ -1,97 +0,0 @@
|
||||
"use client"
|
||||
|
||||
import * as React from "react"
|
||||
|
||||
import { Button } from "@/registry/default/ui/button"
|
||||
import { Calendar } from "@/registry/default/ui/calendar"
|
||||
import { Card, CardContent, CardFooter } from "@/registry/default/ui/card"
|
||||
|
||||
export default function Calendar20() {
|
||||
const [date, setDate] = React.useState<Date | undefined>(
|
||||
new Date(2025, 5, 12)
|
||||
)
|
||||
const [selectedTime, setSelectedTime] = React.useState<string | null>("10:00")
|
||||
const timeSlots = Array.from({ length: 37 }, (_, i) => {
|
||||
const totalMinutes = i * 15
|
||||
const hour = Math.floor(totalMinutes / 60) + 9
|
||||
const minute = totalMinutes % 60
|
||||
return `${hour.toString().padStart(2, "0")}:${minute
|
||||
.toString()
|
||||
.padStart(2, "0")}`
|
||||
})
|
||||
|
||||
const bookedDates = Array.from(
|
||||
{ length: 3 },
|
||||
(_, i) => new Date(2025, 5, 17 + i)
|
||||
)
|
||||
|
||||
return (
|
||||
<Card className="gap-0 p-0">
|
||||
<CardContent className="relative p-0 md:pr-48">
|
||||
<div className="p-6">
|
||||
<Calendar
|
||||
mode="single"
|
||||
selected={date}
|
||||
onSelect={setDate}
|
||||
defaultMonth={date}
|
||||
disabled={bookedDates}
|
||||
showOutsideDays={false}
|
||||
modifiers={{
|
||||
booked: bookedDates,
|
||||
}}
|
||||
modifiersClassNames={{
|
||||
booked: "[&>button]:line-through opacity-100",
|
||||
}}
|
||||
className="bg-transparent p-0 [--cell-size:2.5rem] md:[--cell-size:3rem]"
|
||||
formatters={{
|
||||
formatWeekdayName: (date) => {
|
||||
return date.toLocaleString("en-US", { weekday: "short" })
|
||||
},
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<div className="no-scrollbar inset-y-0 right-0 flex max-h-72 w-full scroll-pb-6 flex-col gap-4 overflow-y-auto border-t p-6 md:absolute md:max-h-none md:w-48 md:border-l md:border-t-0">
|
||||
<div className="grid gap-2">
|
||||
{timeSlots.map((time) => (
|
||||
<Button
|
||||
key={time}
|
||||
variant={selectedTime === time ? "default" : "outline"}
|
||||
onClick={() => setSelectedTime(time)}
|
||||
className="w-full shadow-none"
|
||||
>
|
||||
{time}
|
||||
</Button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
<CardFooter className="flex flex-col gap-4 border-t !py-5 px-6 md:flex-row">
|
||||
<div className="text-sm">
|
||||
{date && selectedTime ? (
|
||||
<>
|
||||
Your meeting is booked for{" "}
|
||||
<span className="font-medium">
|
||||
{" "}
|
||||
{date?.toLocaleDateString("en-US", {
|
||||
weekday: "long",
|
||||
day: "numeric",
|
||||
month: "long",
|
||||
})}{" "}
|
||||
</span>
|
||||
at <span className="font-medium">{selectedTime}</span>.
|
||||
</>
|
||||
) : (
|
||||
<>Select a date and time for your meeting.</>
|
||||
)}
|
||||
</div>
|
||||
<Button
|
||||
disabled={!date || !selectedTime}
|
||||
className="w-full md:ml-auto md:w-auto"
|
||||
variant="outline"
|
||||
>
|
||||
Continue
|
||||
</Button>
|
||||
</CardFooter>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
@@ -1,52 +0,0 @@
|
||||
"use client"
|
||||
|
||||
import * as React from "react"
|
||||
import { DateRange } from "react-day-picker"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
import { buttonVariants } from "@/registry/default/ui/button"
|
||||
import { Calendar } from "@/registry/default/ui/calendar"
|
||||
|
||||
export default function Calendar21() {
|
||||
const [range, setRange] = React.useState<DateRange | undefined>({
|
||||
from: new Date(2025, 5, 12),
|
||||
to: new Date(2025, 5, 17),
|
||||
})
|
||||
|
||||
return (
|
||||
<Calendar
|
||||
mode="range"
|
||||
defaultMonth={range?.from}
|
||||
selected={range}
|
||||
onSelect={setRange}
|
||||
numberOfMonths={1}
|
||||
captionLayout="dropdown"
|
||||
className="rounded-lg border shadow-sm [--cell-size:2.75rem] md:[--cell-size:3rem]"
|
||||
formatters={{
|
||||
formatMonthDropdown: (date) => {
|
||||
return date.toLocaleString("default", { month: "long" })
|
||||
},
|
||||
}}
|
||||
components={{
|
||||
DayButton: ({ children, modifiers, day, ...props }) => {
|
||||
const isWeekend = day.date.getDay() === 0 || day.date.getDay() === 6
|
||||
|
||||
return (
|
||||
<button
|
||||
className={cn(
|
||||
buttonVariants({ variant: "ghost" }),
|
||||
"relative flex flex-col items-center justify-center h-9 w-9 p-0 font-normal aria-selected:opacity-100"
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<div>{children}</div>
|
||||
{!modifiers.outside && (
|
||||
<span className="text-xs">{isWeekend ? "$220" : "$100"}</span>
|
||||
)}
|
||||
</button>
|
||||
)
|
||||
},
|
||||
}}
|
||||
/>
|
||||
)
|
||||
}
|
||||
@@ -1,49 +0,0 @@
|
||||
"use client"
|
||||
|
||||
import * as React from "react"
|
||||
import { ChevronDownIcon } from "lucide-react"
|
||||
|
||||
import { Button } from "@/registry/default/ui/button"
|
||||
import { Calendar } from "@/registry/default/ui/calendar"
|
||||
import { Label } from "@/registry/default/ui/label"
|
||||
import {
|
||||
Popover,
|
||||
PopoverContent,
|
||||
PopoverTrigger,
|
||||
} from "@/registry/default/ui/popover"
|
||||
|
||||
export default function Calendar22() {
|
||||
const [open, setOpen] = React.useState(false)
|
||||
const [date, setDate] = React.useState<Date | undefined>(undefined)
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-3">
|
||||
<Label htmlFor="date" className="px-1">
|
||||
Date of birth
|
||||
</Label>
|
||||
<Popover open={open} onOpenChange={setOpen}>
|
||||
<PopoverTrigger asChild>
|
||||
<Button
|
||||
variant="outline"
|
||||
id="date"
|
||||
className="w-48 justify-between font-normal"
|
||||
>
|
||||
{date ? date.toLocaleDateString() : "Select date"}
|
||||
<ChevronDownIcon />
|
||||
</Button>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent className="w-auto overflow-hidden p-0" align="start">
|
||||
<Calendar
|
||||
mode="single"
|
||||
selected={date}
|
||||
captionLayout="dropdown"
|
||||
onSelect={(date) => {
|
||||
setDate(date)
|
||||
setOpen(false)
|
||||
}}
|
||||
/>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -1,50 +0,0 @@
|
||||
"use client"
|
||||
|
||||
import * as React from "react"
|
||||
import { ChevronDownIcon } from "lucide-react"
|
||||
import { type DateRange } from "react-day-picker"
|
||||
|
||||
import { Button } from "@/registry/default/ui/button"
|
||||
import { Calendar } from "@/registry/default/ui/calendar"
|
||||
import { Label } from "@/registry/default/ui/label"
|
||||
import {
|
||||
Popover,
|
||||
PopoverContent,
|
||||
PopoverTrigger,
|
||||
} from "@/registry/default/ui/popover"
|
||||
|
||||
export default function Calendar23() {
|
||||
const [range, setRange] = React.useState<DateRange | undefined>(undefined)
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-3">
|
||||
<Label htmlFor="dates" className="px-1">
|
||||
Select your stay
|
||||
</Label>
|
||||
<Popover>
|
||||
<PopoverTrigger asChild>
|
||||
<Button
|
||||
variant="outline"
|
||||
id="dates"
|
||||
className="w-56 justify-between font-normal"
|
||||
>
|
||||
{range?.from && range?.to
|
||||
? `${range.from.toLocaleDateString()} - ${range.to.toLocaleDateString()}`
|
||||
: "Select date"}
|
||||
<ChevronDownIcon />
|
||||
</Button>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent className="w-auto overflow-hidden p-0" align="start">
|
||||
<Calendar
|
||||
mode="range"
|
||||
selected={range}
|
||||
captionLayout="dropdown"
|
||||
onSelect={(range) => {
|
||||
setRange(range)
|
||||
}}
|
||||
/>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -1,64 +0,0 @@
|
||||
"use client"
|
||||
|
||||
import * as React from "react"
|
||||
import { ChevronDownIcon } from "lucide-react"
|
||||
|
||||
import { Button } from "@/registry/default/ui/button"
|
||||
import { Calendar } from "@/registry/default/ui/calendar"
|
||||
import { Input } from "@/registry/default/ui/input"
|
||||
import { Label } from "@/registry/default/ui/label"
|
||||
import {
|
||||
Popover,
|
||||
PopoverContent,
|
||||
PopoverTrigger,
|
||||
} from "@/registry/default/ui/popover"
|
||||
|
||||
export default function Calendar24() {
|
||||
const [open, setOpen] = React.useState(false)
|
||||
const [date, setDate] = React.useState<Date | undefined>(undefined)
|
||||
|
||||
return (
|
||||
<div className="flex gap-4">
|
||||
<div className="flex flex-col gap-3">
|
||||
<Label htmlFor="date" className="px-1">
|
||||
Date
|
||||
</Label>
|
||||
<Popover open={open} onOpenChange={setOpen}>
|
||||
<PopoverTrigger asChild>
|
||||
<Button
|
||||
variant="outline"
|
||||
id="date"
|
||||
className="w-32 justify-between font-normal"
|
||||
>
|
||||
{date ? date.toLocaleDateString() : "Select date"}
|
||||
<ChevronDownIcon />
|
||||
</Button>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent className="w-auto overflow-hidden p-0" align="start">
|
||||
<Calendar
|
||||
mode="single"
|
||||
selected={date}
|
||||
captionLayout="dropdown"
|
||||
onSelect={(date) => {
|
||||
setDate(date)
|
||||
setOpen(false)
|
||||
}}
|
||||
/>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
</div>
|
||||
<div className="flex flex-col gap-3">
|
||||
<Label htmlFor="time" className="px-1">
|
||||
Time
|
||||
</Label>
|
||||
<Input
|
||||
type="time"
|
||||
id="time"
|
||||
step="1"
|
||||
defaultValue="10:30:00"
|
||||
className="bg-background appearance-none [&::-webkit-calendar-picker-indicator]:hidden [&::-webkit-calendar-picker-indicator]:appearance-none"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -1,78 +0,0 @@
|
||||
"use client"
|
||||
|
||||
import * as React from "react"
|
||||
import { ChevronDownIcon } from "lucide-react"
|
||||
|
||||
import { Button } from "@/registry/default/ui/button"
|
||||
import { Calendar } from "@/registry/default/ui/calendar"
|
||||
import { Input } from "@/registry/default/ui/input"
|
||||
import { Label } from "@/registry/default/ui/label"
|
||||
import {
|
||||
Popover,
|
||||
PopoverContent,
|
||||
PopoverTrigger,
|
||||
} from "@/registry/default/ui/popover"
|
||||
|
||||
export default function Calendar25() {
|
||||
const [open, setOpen] = React.useState(false)
|
||||
const [date, setDate] = React.useState<Date | undefined>(undefined)
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-6">
|
||||
<div className="flex flex-col gap-3">
|
||||
<Label htmlFor="date" className="px-1">
|
||||
Date
|
||||
</Label>
|
||||
<Popover open={open} onOpenChange={setOpen}>
|
||||
<PopoverTrigger asChild>
|
||||
<Button
|
||||
variant="outline"
|
||||
id="date"
|
||||
className="w-full justify-between font-normal"
|
||||
>
|
||||
{date ? date.toLocaleDateString() : "Select date"}
|
||||
<ChevronDownIcon />
|
||||
</Button>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent className="w-auto overflow-hidden p-0" align="start">
|
||||
<Calendar
|
||||
mode="single"
|
||||
selected={date}
|
||||
captionLayout="dropdown"
|
||||
onSelect={(date) => {
|
||||
setDate(date)
|
||||
setOpen(false)
|
||||
}}
|
||||
/>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
</div>
|
||||
<div className="flex gap-4">
|
||||
<div className="flex flex-col gap-3">
|
||||
<Label htmlFor="time-from" className="px-1">
|
||||
From
|
||||
</Label>
|
||||
<Input
|
||||
type="time"
|
||||
id="time-from"
|
||||
step="1"
|
||||
defaultValue="10:30:00"
|
||||
className="bg-background appearance-none [&::-webkit-calendar-picker-indicator]:hidden [&::-webkit-calendar-picker-indicator]:appearance-none"
|
||||
/>
|
||||
</div>
|
||||
<div className="flex flex-col gap-3">
|
||||
<Label htmlFor="time-to" className="px-1">
|
||||
To
|
||||
</Label>
|
||||
<Input
|
||||
type="time"
|
||||
id="time-to"
|
||||
step="1"
|
||||
defaultValue="12:30:00"
|
||||
className="bg-background appearance-none [&::-webkit-calendar-picker-indicator]:hidden [&::-webkit-calendar-picker-indicator]:appearance-none"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -1,133 +0,0 @@
|
||||
"use client"
|
||||
|
||||
import * as React from "react"
|
||||
import { ChevronDownIcon } from "lucide-react"
|
||||
|
||||
import { Button } from "@/registry/default/ui/button"
|
||||
import { Calendar } from "@/registry/default/ui/calendar"
|
||||
import { Input } from "@/registry/default/ui/input"
|
||||
import { Label } from "@/registry/default/ui/label"
|
||||
import {
|
||||
Popover,
|
||||
PopoverContent,
|
||||
PopoverTrigger,
|
||||
} from "@/registry/default/ui/popover"
|
||||
|
||||
export default function Calendar26() {
|
||||
const [openFrom, setOpenFrom] = React.useState(false)
|
||||
const [openTo, setOpenTo] = React.useState(false)
|
||||
const [dateFrom, setDateFrom] = React.useState<Date | undefined>(
|
||||
new Date("2025-06-01")
|
||||
)
|
||||
const [dateTo, setDateTo] = React.useState<Date | undefined>(
|
||||
new Date("2025-06-03")
|
||||
)
|
||||
|
||||
return (
|
||||
<div className="flex w-full max-w-64 min-w-0 flex-col gap-6">
|
||||
<div className="flex gap-4">
|
||||
<div className="flex flex-1 flex-col gap-3">
|
||||
<Label htmlFor="date-from" className="px-1">
|
||||
Check-in
|
||||
</Label>
|
||||
<Popover open={openFrom} onOpenChange={setOpenFrom}>
|
||||
<PopoverTrigger asChild>
|
||||
<Button
|
||||
variant="outline"
|
||||
id="date-from"
|
||||
className="w-full justify-between font-normal"
|
||||
>
|
||||
{dateFrom
|
||||
? dateFrom.toLocaleDateString("en-US", {
|
||||
day: "2-digit",
|
||||
month: "short",
|
||||
year: "numeric",
|
||||
})
|
||||
: "Select date"}
|
||||
<ChevronDownIcon />
|
||||
</Button>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent
|
||||
className="w-auto overflow-hidden p-0"
|
||||
align="start"
|
||||
>
|
||||
<Calendar
|
||||
mode="single"
|
||||
selected={dateFrom}
|
||||
captionLayout="dropdown"
|
||||
onSelect={(date) => {
|
||||
setDateFrom(date)
|
||||
setOpenFrom(false)
|
||||
}}
|
||||
/>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
</div>
|
||||
<div className="flex flex-col gap-3">
|
||||
<Label htmlFor="time-from" className="invisible px-1">
|
||||
From
|
||||
</Label>
|
||||
<Input
|
||||
type="time"
|
||||
id="time-from"
|
||||
step="1"
|
||||
defaultValue="10:30:00"
|
||||
className="bg-background appearance-none [&::-webkit-calendar-picker-indicator]:hidden [&::-webkit-calendar-picker-indicator]:appearance-none"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex gap-4">
|
||||
<div className="flex flex-1 flex-col gap-3">
|
||||
<Label htmlFor="date-to" className="px-1">
|
||||
Check-out
|
||||
</Label>
|
||||
<Popover open={openTo} onOpenChange={setOpenTo}>
|
||||
<PopoverTrigger asChild>
|
||||
<Button
|
||||
variant="outline"
|
||||
id="date-to"
|
||||
className="w-full justify-between font-normal"
|
||||
>
|
||||
{dateTo
|
||||
? dateTo.toLocaleDateString("en-US", {
|
||||
day: "2-digit",
|
||||
month: "short",
|
||||
year: "numeric",
|
||||
})
|
||||
: "Select date"}
|
||||
<ChevronDownIcon />
|
||||
</Button>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent
|
||||
className="w-auto overflow-hidden p-0"
|
||||
align="start"
|
||||
>
|
||||
<Calendar
|
||||
mode="single"
|
||||
selected={dateTo}
|
||||
captionLayout="dropdown"
|
||||
onSelect={(date) => {
|
||||
setDateTo(date)
|
||||
setOpenTo(false)
|
||||
}}
|
||||
disabled={dateFrom && { before: dateFrom }}
|
||||
/>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
</div>
|
||||
<div className="flex flex-col gap-3">
|
||||
<Label htmlFor="time-to" className="invisible px-1">
|
||||
To
|
||||
</Label>
|
||||
<Input
|
||||
type="time"
|
||||
id="time-to"
|
||||
step="1"
|
||||
defaultValue="12:30:00"
|
||||
className="bg-background appearance-none [&::-webkit-calendar-picker-indicator]:hidden [&::-webkit-calendar-picker-indicator]:appearance-none"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -1,177 +0,0 @@
|
||||
"use client"
|
||||
|
||||
import * as React from "react"
|
||||
import { CalendarIcon } from "lucide-react"
|
||||
import { DateRange } from "react-day-picker"
|
||||
import { Bar, BarChart, CartesianGrid, XAxis } from "recharts"
|
||||
|
||||
import { Button } from "@/registry/default/ui/button"
|
||||
import { Calendar } from "@/registry/default/ui/calendar"
|
||||
import {
|
||||
Card,
|
||||
CardContent,
|
||||
CardDescription,
|
||||
CardFooter,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
} from "@/registry/default/ui/card"
|
||||
import {
|
||||
ChartConfig,
|
||||
ChartContainer,
|
||||
ChartTooltip,
|
||||
ChartTooltipContent,
|
||||
} from "@/registry/default/ui/chart"
|
||||
import {
|
||||
Popover,
|
||||
PopoverContent,
|
||||
PopoverTrigger,
|
||||
} from "@/registry/default/ui/popover"
|
||||
|
||||
const chartData = [
|
||||
{ date: "2025-06-01", visitors: 178 },
|
||||
{ date: "2025-06-02", visitors: 470 },
|
||||
{ date: "2025-06-03", visitors: 103 },
|
||||
{ date: "2025-06-04", visitors: 439 },
|
||||
{ date: "2025-06-05", visitors: 88 },
|
||||
{ date: "2025-06-06", visitors: 294 },
|
||||
{ date: "2025-06-07", visitors: 323 },
|
||||
{ date: "2025-06-08", visitors: 385 },
|
||||
{ date: "2025-06-09", visitors: 438 },
|
||||
{ date: "2025-06-10", visitors: 155 },
|
||||
{ date: "2025-06-11", visitors: 92 },
|
||||
{ date: "2025-06-12", visitors: 492 },
|
||||
{ date: "2025-06-13", visitors: 81 },
|
||||
{ date: "2025-06-14", visitors: 426 },
|
||||
{ date: "2025-06-15", visitors: 307 },
|
||||
{ date: "2025-06-16", visitors: 371 },
|
||||
{ date: "2025-06-17", visitors: 475 },
|
||||
{ date: "2025-06-18", visitors: 107 },
|
||||
{ date: "2025-06-19", visitors: 341 },
|
||||
{ date: "2025-06-20", visitors: 408 },
|
||||
{ date: "2025-06-21", visitors: 169 },
|
||||
{ date: "2025-06-22", visitors: 317 },
|
||||
{ date: "2025-06-23", visitors: 480 },
|
||||
{ date: "2025-06-24", visitors: 132 },
|
||||
{ date: "2025-06-25", visitors: 141 },
|
||||
{ date: "2025-06-26", visitors: 434 },
|
||||
{ date: "2025-06-27", visitors: 448 },
|
||||
{ date: "2025-06-28", visitors: 149 },
|
||||
{ date: "2025-06-29", visitors: 103 },
|
||||
{ date: "2025-06-30", visitors: 446 },
|
||||
]
|
||||
|
||||
const total = chartData.reduce((acc, curr) => acc + curr.visitors, 0)
|
||||
|
||||
const chartConfig = {
|
||||
visitors: {
|
||||
label: "Visitors",
|
||||
color: "hsl(var(--primary))",
|
||||
},
|
||||
} satisfies ChartConfig
|
||||
|
||||
export default function Calendar27() {
|
||||
const [range, setRange] = React.useState<DateRange | undefined>({
|
||||
from: new Date(2025, 5, 5),
|
||||
to: new Date(2025, 5, 20),
|
||||
})
|
||||
const filteredData = React.useMemo(() => {
|
||||
if (!range?.from && !range?.to) {
|
||||
return chartData
|
||||
}
|
||||
|
||||
return chartData.filter((item) => {
|
||||
const date = new Date(item.date)
|
||||
return date >= range.from! && date <= range.to!
|
||||
})
|
||||
}, [range])
|
||||
|
||||
return (
|
||||
<Card className="@container/card w-full max-w-xl">
|
||||
<CardHeader className="@md/card:grid relative flex flex-col border-b">
|
||||
<CardTitle>Web Analytics</CardTitle>
|
||||
<CardDescription>
|
||||
Showing total visitors for this month.
|
||||
</CardDescription>
|
||||
<Popover>
|
||||
<PopoverTrigger asChild>
|
||||
<Button variant="outline" className="absolute right-4 top-4">
|
||||
<CalendarIcon />
|
||||
{range?.from && range?.to
|
||||
? `${range.from.toLocaleDateString()} - ${range.to.toLocaleDateString()}`
|
||||
: "June 2025"}
|
||||
</Button>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent className="w-auto overflow-hidden p-0" align="end">
|
||||
<Calendar
|
||||
className="w-full"
|
||||
mode="range"
|
||||
defaultMonth={range?.from}
|
||||
selected={range}
|
||||
onSelect={setRange}
|
||||
disableNavigation
|
||||
startMonth={range?.from}
|
||||
fixedWeeks
|
||||
showOutsideDays
|
||||
disabled={{
|
||||
after: new Date(2025, 5, 31),
|
||||
}}
|
||||
/>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
</CardHeader>
|
||||
<CardContent className="px-4">
|
||||
<ChartContainer
|
||||
config={chartConfig}
|
||||
className="aspect-auto h-[250px] w-full"
|
||||
>
|
||||
<BarChart
|
||||
accessibilityLayer
|
||||
data={filteredData}
|
||||
margin={{
|
||||
left: 12,
|
||||
right: 12,
|
||||
}}
|
||||
>
|
||||
<CartesianGrid vertical={false} />
|
||||
<XAxis
|
||||
dataKey="date"
|
||||
tickLine={false}
|
||||
axisLine={false}
|
||||
tickMargin={8}
|
||||
minTickGap={20}
|
||||
tickFormatter={(value) => {
|
||||
const date = new Date(value)
|
||||
return date.toLocaleDateString("en-US", {
|
||||
day: "numeric",
|
||||
})
|
||||
}}
|
||||
/>
|
||||
<ChartTooltip
|
||||
content={
|
||||
<ChartTooltipContent
|
||||
className="w-[150px]"
|
||||
nameKey="visitors"
|
||||
labelFormatter={(value) => {
|
||||
return new Date(value).toLocaleDateString("en-US", {
|
||||
month: "short",
|
||||
day: "numeric",
|
||||
year: "numeric",
|
||||
})
|
||||
}}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
<Bar dataKey="visitors" fill={`var(--color-visitors)`} radius={4} />
|
||||
</BarChart>
|
||||
</ChartContainer>
|
||||
</CardContent>
|
||||
<CardFooter className="border-t pt-6">
|
||||
<div className="text-sm">
|
||||
You had{" "}
|
||||
<span className="font-semibold">{total.toLocaleString()}</span>{" "}
|
||||
visitors for the month of June.
|
||||
</div>
|
||||
</CardFooter>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
@@ -1,104 +0,0 @@
|
||||
"use client"
|
||||
|
||||
import * as React from "react"
|
||||
import { CalendarIcon } from "lucide-react"
|
||||
|
||||
import { Button } from "@/registry/default/ui/button"
|
||||
import { Calendar } from "@/registry/default/ui/calendar"
|
||||
import { Input } from "@/registry/default/ui/input"
|
||||
import { Label } from "@/registry/default/ui/label"
|
||||
import {
|
||||
Popover,
|
||||
PopoverContent,
|
||||
PopoverTrigger,
|
||||
} from "@/registry/default/ui/popover"
|
||||
|
||||
function formatDate(date: Date | undefined) {
|
||||
if (!date) {
|
||||
return ""
|
||||
}
|
||||
|
||||
return date.toLocaleDateString("en-US", {
|
||||
day: "2-digit",
|
||||
month: "long",
|
||||
year: "numeric",
|
||||
})
|
||||
}
|
||||
|
||||
function isValidDate(date: Date | undefined) {
|
||||
if (!date) {
|
||||
return false
|
||||
}
|
||||
return !isNaN(date.getTime())
|
||||
}
|
||||
|
||||
export default function Calendar28() {
|
||||
const [open, setOpen] = React.useState(false)
|
||||
const [date, setDate] = React.useState<Date | undefined>(
|
||||
new Date("2025-06-01")
|
||||
)
|
||||
const [month, setMonth] = React.useState<Date | undefined>(date)
|
||||
const [value, setValue] = React.useState(formatDate(date))
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-3">
|
||||
<Label htmlFor="date" className="px-1">
|
||||
Subscription Date
|
||||
</Label>
|
||||
<div className="relative flex gap-2">
|
||||
<Input
|
||||
id="date"
|
||||
value={value}
|
||||
placeholder="June 01, 2025"
|
||||
className="bg-background pr-10"
|
||||
onChange={(e) => {
|
||||
const date = new Date(e.target.value)
|
||||
setValue(e.target.value)
|
||||
if (isValidDate(date)) {
|
||||
setDate(date)
|
||||
setMonth(date)
|
||||
}
|
||||
}}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === "ArrowDown") {
|
||||
e.preventDefault()
|
||||
setOpen(true)
|
||||
}
|
||||
}}
|
||||
/>
|
||||
<Popover open={open} onOpenChange={setOpen}>
|
||||
<PopoverTrigger asChild>
|
||||
<Button
|
||||
id="date-picker"
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="absolute right-2 top-1/2 h-6 w-6 -translate-y-1/2"
|
||||
>
|
||||
<CalendarIcon className="size-3" />
|
||||
<span className="sr-only">Select date</span>
|
||||
</Button>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent
|
||||
className="w-auto overflow-hidden p-0"
|
||||
align="end"
|
||||
alignOffset={-8}
|
||||
sideOffset={10}
|
||||
>
|
||||
<Calendar
|
||||
mode="single"
|
||||
selected={date}
|
||||
captionLayout="dropdown"
|
||||
month={month}
|
||||
onMonthChange={setMonth}
|
||||
onSelect={(date) => {
|
||||
setDate(date)
|
||||
setValue(formatDate(date))
|
||||
setOpen(false)
|
||||
}}
|
||||
/>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -1,96 +0,0 @@
|
||||
"use client"
|
||||
|
||||
import * as React from "react"
|
||||
// import { parseDate } from "chrono-node" - temporarily disabled
|
||||
import { CalendarIcon } from "lucide-react"
|
||||
|
||||
import { Button } from "@/registry/default/ui/button"
|
||||
import { Calendar } from "@/registry/default/ui/calendar"
|
||||
import { Input } from "@/registry/default/ui/input"
|
||||
import { Label } from "@/registry/default/ui/label"
|
||||
import {
|
||||
Popover,
|
||||
PopoverContent,
|
||||
PopoverTrigger,
|
||||
} from "@/registry/default/ui/popover"
|
||||
|
||||
function formatDate(date: Date | undefined) {
|
||||
if (!date) {
|
||||
return ""
|
||||
}
|
||||
|
||||
return date.toLocaleDateString("en-US", {
|
||||
day: "2-digit",
|
||||
month: "long",
|
||||
year: "numeric",
|
||||
})
|
||||
}
|
||||
|
||||
export default function Calendar29() {
|
||||
const [open, setOpen] = React.useState(false)
|
||||
const [value, setValue] = React.useState("In 2 days")
|
||||
const [date, setDate] = React.useState<Date | undefined>(
|
||||
new Date() // parseDate stub
|
||||
)
|
||||
const [month, setMonth] = React.useState<Date | undefined>(date)
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-3">
|
||||
<Label htmlFor="date" className="px-1">
|
||||
Schedule Date
|
||||
</Label>
|
||||
<div className="relative flex gap-2">
|
||||
<Input
|
||||
id="date"
|
||||
value={value}
|
||||
placeholder="Tomorrow or next week"
|
||||
className="bg-background pr-10"
|
||||
onChange={(e) => {
|
||||
setValue(e.target.value)
|
||||
const date = null // parseDate stub
|
||||
if (date) {
|
||||
setDate(date)
|
||||
setMonth(date)
|
||||
}
|
||||
}}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === "ArrowDown") {
|
||||
e.preventDefault()
|
||||
setOpen(true)
|
||||
}
|
||||
}}
|
||||
/>
|
||||
<Popover open={open} onOpenChange={setOpen}>
|
||||
<PopoverTrigger asChild>
|
||||
<Button
|
||||
id="date-picker"
|
||||
variant="ghost"
|
||||
className="absolute top-1/2 right-2 size-6 -translate-y-1/2"
|
||||
>
|
||||
<CalendarIcon className="size-3.5" />
|
||||
<span className="sr-only">Select date</span>
|
||||
</Button>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent className="w-auto overflow-hidden p-0" align="end">
|
||||
<Calendar
|
||||
mode="single"
|
||||
selected={date}
|
||||
captionLayout="dropdown"
|
||||
month={month}
|
||||
onMonthChange={setMonth}
|
||||
onSelect={(date) => {
|
||||
setDate(date)
|
||||
setValue(formatDate(date))
|
||||
setOpen(false)
|
||||
}}
|
||||
/>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
</div>
|
||||
<div className="text-muted-foreground px-1 text-sm">
|
||||
Your post will be published on{" "}
|
||||
<span className="font-medium">{formatDate(date)}</span>.
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -1,56 +0,0 @@
|
||||
"use client"
|
||||
|
||||
import * as React from "react"
|
||||
import { formatDateRange } from "little-date"
|
||||
import { ChevronDownIcon } from "lucide-react"
|
||||
import { type DateRange } from "react-day-picker"
|
||||
|
||||
import { Button } from "@/registry/default/ui/button"
|
||||
import { Calendar } from "@/registry/default/ui/calendar"
|
||||
import { Label } from "@/registry/default/ui/label"
|
||||
import {
|
||||
Popover,
|
||||
PopoverContent,
|
||||
PopoverTrigger,
|
||||
} from "@/registry/default/ui/popover"
|
||||
|
||||
export default function Calendar30() {
|
||||
const [range, setRange] = React.useState<DateRange | undefined>({
|
||||
from: new Date(2025, 5, 4),
|
||||
to: new Date(2025, 5, 10),
|
||||
})
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-3">
|
||||
<Label htmlFor="dates" className="px-1">
|
||||
Select your stay
|
||||
</Label>
|
||||
<Popover>
|
||||
<PopoverTrigger asChild>
|
||||
<Button
|
||||
variant="outline"
|
||||
id="dates"
|
||||
className="w-56 justify-between font-normal"
|
||||
>
|
||||
{range?.from && range?.to
|
||||
? formatDateRange(range.from, range.to, {
|
||||
includeTime: false,
|
||||
})
|
||||
: "Select date"}
|
||||
<ChevronDownIcon />
|
||||
</Button>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent className="w-auto overflow-hidden p-0" align="start">
|
||||
<Calendar
|
||||
mode="range"
|
||||
selected={range}
|
||||
captionLayout="dropdown"
|
||||
onSelect={(range) => {
|
||||
setRange(range)
|
||||
}}
|
||||
/>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -1,80 +0,0 @@
|
||||
"use client"
|
||||
|
||||
import * as React from "react"
|
||||
import { formatDateRange } from "little-date"
|
||||
import { PlusIcon } from "lucide-react"
|
||||
|
||||
import { Button } from "@/registry/default/ui/button"
|
||||
import { Calendar } from "@/registry/default/ui/calendar"
|
||||
import { Card, CardContent, CardFooter } from "@/registry/default/ui/card"
|
||||
|
||||
const events = [
|
||||
{
|
||||
title: "Team Sync Meeting",
|
||||
from: "2025-06-12T09:00:00",
|
||||
to: "2025-06-12T10:00:00",
|
||||
},
|
||||
{
|
||||
title: "Design Review",
|
||||
from: "2025-06-12T11:30:00",
|
||||
to: "2025-06-12T12:30:00",
|
||||
},
|
||||
{
|
||||
title: "Client Presentation",
|
||||
from: "2025-06-12T14:00:00",
|
||||
to: "2025-06-12T15:00:00",
|
||||
},
|
||||
]
|
||||
|
||||
export default function Calendar31() {
|
||||
const [date, setDate] = React.useState<Date | undefined>(
|
||||
new Date(2025, 5, 12)
|
||||
)
|
||||
|
||||
return (
|
||||
<Card className="w-fit py-4">
|
||||
<CardContent className="px-4">
|
||||
<Calendar
|
||||
mode="single"
|
||||
selected={date}
|
||||
onSelect={setDate}
|
||||
className="bg-transparent p-0"
|
||||
required
|
||||
/>
|
||||
</CardContent>
|
||||
<CardFooter className="flex flex-col items-start gap-3 border-t px-4 pb-0 pt-4">
|
||||
<div className="flex w-full items-center justify-between px-1">
|
||||
<div className="text-sm font-medium">
|
||||
{date?.toLocaleDateString("en-US", {
|
||||
day: "numeric",
|
||||
month: "long",
|
||||
year: "numeric",
|
||||
})}
|
||||
</div>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="h-6 w-6"
|
||||
title="Add Event"
|
||||
>
|
||||
<PlusIcon />
|
||||
<span className="sr-only">Add Event</span>
|
||||
</Button>
|
||||
</div>
|
||||
<div className="flex w-full flex-col gap-2">
|
||||
{events.map((event) => (
|
||||
<div
|
||||
key={event.title}
|
||||
className="bg-muted after:bg-primary/70 relative rounded-md p-2 pl-6 text-sm after:absolute after:inset-y-2 after:left-2 after:w-1 after:rounded-full"
|
||||
>
|
||||
<div className="font-medium">{event.title}</div>
|
||||
<div className="text-muted-foreground text-xs">
|
||||
{formatDateRange(new Date(event.from), new Date(event.to))}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</CardFooter>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
@@ -1,60 +0,0 @@
|
||||
"use client"
|
||||
|
||||
import * as React from "react"
|
||||
import { CalendarPlusIcon } from "lucide-react"
|
||||
|
||||
import { Button } from "@/registry/default/ui/button"
|
||||
import { Calendar } from "@/registry/default/ui/calendar"
|
||||
import {
|
||||
Drawer,
|
||||
DrawerContent,
|
||||
DrawerDescription,
|
||||
DrawerHeader,
|
||||
DrawerTitle,
|
||||
DrawerTrigger,
|
||||
} from "@/registry/default/ui/drawer"
|
||||
import { Label } from "@/registry/default/ui/label"
|
||||
|
||||
export default function Calendar32() {
|
||||
const [open, setOpen] = React.useState(false)
|
||||
const [date, setDate] = React.useState<Date | undefined>(undefined)
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-3">
|
||||
<Label htmlFor="date" className="px-1">
|
||||
Date of birth
|
||||
</Label>
|
||||
<Drawer open={open} onOpenChange={setOpen}>
|
||||
<DrawerTrigger asChild>
|
||||
<Button
|
||||
variant="outline"
|
||||
id="date"
|
||||
className="w-48 justify-between font-normal"
|
||||
>
|
||||
{date ? date.toLocaleDateString() : "Select date"}
|
||||
<CalendarPlusIcon />
|
||||
</Button>
|
||||
</DrawerTrigger>
|
||||
<DrawerContent className="w-auto overflow-hidden p-0">
|
||||
<DrawerHeader className="sr-only">
|
||||
<DrawerTitle>Select date</DrawerTitle>
|
||||
<DrawerDescription>Set your date of birth</DrawerDescription>
|
||||
</DrawerHeader>
|
||||
<Calendar
|
||||
mode="single"
|
||||
selected={date}
|
||||
captionLayout="dropdown"
|
||||
onSelect={(date) => {
|
||||
setDate(date)
|
||||
setOpen(false)
|
||||
}}
|
||||
className="mx-auto [--cell-size:clamp(0px,calc(100vw/7.5),52px)]"
|
||||
/>
|
||||
</DrawerContent>
|
||||
</Drawer>
|
||||
<div className="text-muted-foreground px-1 text-sm">
|
||||
This example works best on mobile.
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -1,455 +0,0 @@
|
||||
import Link from "next/link"
|
||||
import {
|
||||
Activity,
|
||||
ArrowUpRight,
|
||||
CircleUser,
|
||||
CreditCard,
|
||||
DollarSign,
|
||||
Menu,
|
||||
Package2,
|
||||
Search,
|
||||
Users,
|
||||
} from "lucide-react"
|
||||
|
||||
import {
|
||||
Avatar,
|
||||
AvatarFallback,
|
||||
AvatarImage,
|
||||
} from "@/registry/default/ui/avatar"
|
||||
import { Badge } from "@/registry/default/ui/badge"
|
||||
import { Button } from "@/registry/default/ui/button"
|
||||
import {
|
||||
Card,
|
||||
CardContent,
|
||||
CardDescription,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
} from "@/registry/default/ui/card"
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuLabel,
|
||||
DropdownMenuSeparator,
|
||||
DropdownMenuTrigger,
|
||||
} from "@/registry/default/ui/dropdown-menu"
|
||||
import { Input } from "@/registry/default/ui/input"
|
||||
import { Sheet, SheetContent, SheetTrigger } from "@/registry/default/ui/sheet"
|
||||
import {
|
||||
Table,
|
||||
TableBody,
|
||||
TableCell,
|
||||
TableHead,
|
||||
TableHeader,
|
||||
TableRow,
|
||||
} from "@/registry/default/ui/table"
|
||||
|
||||
export const description =
|
||||
"An application shell with a header and main content area. The header has a navbar, a search input and and a user nav dropdown. The user nav is toggled by a button with an avatar image."
|
||||
|
||||
export const iframeHeight = "825px"
|
||||
|
||||
export const containerClassName = "w-full h-full"
|
||||
|
||||
export default function Dashboard() {
|
||||
return (
|
||||
<div className="flex min-h-screen w-full flex-col">
|
||||
<header className="sticky top-0 flex h-16 items-center gap-4 border-b bg-background px-4 md:px-6">
|
||||
<nav className="hidden flex-col gap-6 text-lg font-medium md:flex md:flex-row md:items-center md:gap-5 md:text-sm lg:gap-6">
|
||||
<Link
|
||||
href="#"
|
||||
className="flex items-center gap-2 text-lg font-semibold md:text-base"
|
||||
>
|
||||
<Package2 className="h-6 w-6" />
|
||||
<span className="sr-only">Acme Inc</span>
|
||||
</Link>
|
||||
<Link
|
||||
href="#"
|
||||
className="text-foreground transition-colors hover:text-foreground"
|
||||
>
|
||||
Dashboard
|
||||
</Link>
|
||||
<Link
|
||||
href="#"
|
||||
className="text-muted-foreground transition-colors hover:text-foreground"
|
||||
>
|
||||
Orders
|
||||
</Link>
|
||||
<Link
|
||||
href="#"
|
||||
className="text-muted-foreground transition-colors hover:text-foreground"
|
||||
>
|
||||
Products
|
||||
</Link>
|
||||
<Link
|
||||
href="#"
|
||||
className="text-muted-foreground transition-colors hover:text-foreground"
|
||||
>
|
||||
Customers
|
||||
</Link>
|
||||
<Link
|
||||
href="#"
|
||||
className="text-muted-foreground transition-colors hover:text-foreground"
|
||||
>
|
||||
Analytics
|
||||
</Link>
|
||||
</nav>
|
||||
<Sheet>
|
||||
<SheetTrigger asChild>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="icon"
|
||||
className="shrink-0 md:hidden"
|
||||
>
|
||||
<Menu className="h-5 w-5" />
|
||||
<span className="sr-only">Toggle navigation menu</span>
|
||||
</Button>
|
||||
</SheetTrigger>
|
||||
<SheetContent side="left">
|
||||
<nav className="grid gap-6 text-lg font-medium">
|
||||
<Link
|
||||
href="#"
|
||||
className="flex items-center gap-2 text-lg font-semibold"
|
||||
>
|
||||
<Package2 className="h-6 w-6" />
|
||||
<span className="sr-only">Acme Inc</span>
|
||||
</Link>
|
||||
<Link href="#" className="hover:text-foreground">
|
||||
Dashboard
|
||||
</Link>
|
||||
<Link
|
||||
href="#"
|
||||
className="text-muted-foreground hover:text-foreground"
|
||||
>
|
||||
Orders
|
||||
</Link>
|
||||
<Link
|
||||
href="#"
|
||||
className="text-muted-foreground hover:text-foreground"
|
||||
>
|
||||
Products
|
||||
</Link>
|
||||
<Link
|
||||
href="#"
|
||||
className="text-muted-foreground hover:text-foreground"
|
||||
>
|
||||
Customers
|
||||
</Link>
|
||||
<Link
|
||||
href="#"
|
||||
className="text-muted-foreground hover:text-foreground"
|
||||
>
|
||||
Analytics
|
||||
</Link>
|
||||
</nav>
|
||||
</SheetContent>
|
||||
</Sheet>
|
||||
<div className="flex w-full items-center gap-4 md:ml-auto md:gap-2 lg:gap-4">
|
||||
<form className="ml-auto flex-1 sm:flex-initial">
|
||||
<div className="relative">
|
||||
<Search className="absolute left-2.5 top-2.5 h-4 w-4 text-muted-foreground" />
|
||||
<Input
|
||||
type="search"
|
||||
placeholder="Search products..."
|
||||
className="pl-8 sm:w-[300px] md:w-[200px] lg:w-[300px]"
|
||||
/>
|
||||
</div>
|
||||
</form>
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button variant="secondary" size="icon" className="rounded-full">
|
||||
<CircleUser className="h-5 w-5" />
|
||||
<span className="sr-only">Toggle user menu</span>
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="end">
|
||||
<DropdownMenuLabel>My Account</DropdownMenuLabel>
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuItem>Settings</DropdownMenuItem>
|
||||
<DropdownMenuItem>Support</DropdownMenuItem>
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuItem>Logout</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</div>
|
||||
</header>
|
||||
<main className="flex flex-1 flex-col gap-4 p-4 md:gap-8 md:p-8">
|
||||
<div className="grid gap-4 md:grid-cols-2 md:gap-8 lg:grid-cols-4">
|
||||
<Card x-chunk="dashboard-01-chunk-0">
|
||||
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
|
||||
<CardTitle className="text-sm font-medium">
|
||||
Total Revenue
|
||||
</CardTitle>
|
||||
<DollarSign className="h-4 w-4 text-muted-foreground" />
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="text-2xl font-bold">$45,231.89</div>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
+20.1% from last month
|
||||
</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
<Card x-chunk="dashboard-01-chunk-1">
|
||||
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
|
||||
<CardTitle className="text-sm font-medium">
|
||||
Subscriptions
|
||||
</CardTitle>
|
||||
<Users className="h-4 w-4 text-muted-foreground" />
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="text-2xl font-bold">+2350</div>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
+180.1% from last month
|
||||
</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
<Card x-chunk="dashboard-01-chunk-2">
|
||||
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
|
||||
<CardTitle className="text-sm font-medium">Sales</CardTitle>
|
||||
<CreditCard className="h-4 w-4 text-muted-foreground" />
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="text-2xl font-bold">+12,234</div>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
+19% from last month
|
||||
</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
<Card x-chunk="dashboard-01-chunk-3">
|
||||
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
|
||||
<CardTitle className="text-sm font-medium">Active Now</CardTitle>
|
||||
<Activity className="h-4 w-4 text-muted-foreground" />
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="text-2xl font-bold">+573</div>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
+201 since last hour
|
||||
</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
<div className="grid gap-4 md:gap-8 lg:grid-cols-2 xl:grid-cols-3">
|
||||
<Card className="xl:col-span-2" x-chunk="dashboard-01-chunk-4">
|
||||
<CardHeader className="flex flex-row items-center">
|
||||
<div className="grid gap-2">
|
||||
<CardTitle>Transactions</CardTitle>
|
||||
<CardDescription>
|
||||
Recent transactions from your store.
|
||||
</CardDescription>
|
||||
</div>
|
||||
<Button asChild size="sm" className="ml-auto gap-1">
|
||||
<Link href="#">
|
||||
View All
|
||||
<ArrowUpRight className="h-4 w-4" />
|
||||
</Link>
|
||||
</Button>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead>Customer</TableHead>
|
||||
<TableHead className="hidden xl:table-column">
|
||||
Type
|
||||
</TableHead>
|
||||
<TableHead className="hidden xl:table-column">
|
||||
Status
|
||||
</TableHead>
|
||||
<TableHead className="hidden xl:table-column">
|
||||
Date
|
||||
</TableHead>
|
||||
<TableHead className="text-right">Amount</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
<TableRow>
|
||||
<TableCell>
|
||||
<div className="font-medium">Liam Johnson</div>
|
||||
<div className="hidden text-sm text-muted-foreground md:inline">
|
||||
liam@example.com
|
||||
</div>
|
||||
</TableCell>
|
||||
<TableCell className="hidden xl:table-column">
|
||||
Sale
|
||||
</TableCell>
|
||||
<TableCell className="hidden xl:table-column">
|
||||
<Badge className="text-xs" variant="outline">
|
||||
Approved
|
||||
</Badge>
|
||||
</TableCell>
|
||||
<TableCell className="hidden md:table-cell lg:hidden xl:table-column">
|
||||
2023-06-23
|
||||
</TableCell>
|
||||
<TableCell className="text-right">$250.00</TableCell>
|
||||
</TableRow>
|
||||
<TableRow>
|
||||
<TableCell>
|
||||
<div className="font-medium">Olivia Smith</div>
|
||||
<div className="hidden text-sm text-muted-foreground md:inline">
|
||||
olivia@example.com
|
||||
</div>
|
||||
</TableCell>
|
||||
<TableCell className="hidden xl:table-column">
|
||||
Refund
|
||||
</TableCell>
|
||||
<TableCell className="hidden xl:table-column">
|
||||
<Badge className="text-xs" variant="outline">
|
||||
Declined
|
||||
</Badge>
|
||||
</TableCell>
|
||||
<TableCell className="hidden md:table-cell lg:hidden xl:table-column">
|
||||
2023-06-24
|
||||
</TableCell>
|
||||
<TableCell className="text-right">$150.00</TableCell>
|
||||
</TableRow>
|
||||
<TableRow>
|
||||
<TableCell>
|
||||
<div className="font-medium">Noah Williams</div>
|
||||
<div className="hidden text-sm text-muted-foreground md:inline">
|
||||
noah@example.com
|
||||
</div>
|
||||
</TableCell>
|
||||
<TableCell className="hidden xl:table-column">
|
||||
Subscription
|
||||
</TableCell>
|
||||
<TableCell className="hidden xl:table-column">
|
||||
<Badge className="text-xs" variant="outline">
|
||||
Approved
|
||||
</Badge>
|
||||
</TableCell>
|
||||
<TableCell className="hidden md:table-cell lg:hidden xl:table-column">
|
||||
2023-06-25
|
||||
</TableCell>
|
||||
<TableCell className="text-right">$350.00</TableCell>
|
||||
</TableRow>
|
||||
<TableRow>
|
||||
<TableCell>
|
||||
<div className="font-medium">Emma Brown</div>
|
||||
<div className="hidden text-sm text-muted-foreground md:inline">
|
||||
emma@example.com
|
||||
</div>
|
||||
</TableCell>
|
||||
<TableCell className="hidden xl:table-column">
|
||||
Sale
|
||||
</TableCell>
|
||||
<TableCell className="hidden xl:table-column">
|
||||
<Badge className="text-xs" variant="outline">
|
||||
Approved
|
||||
</Badge>
|
||||
</TableCell>
|
||||
<TableCell className="hidden md:table-cell lg:hidden xl:table-column">
|
||||
2023-06-26
|
||||
</TableCell>
|
||||
<TableCell className="text-right">$450.00</TableCell>
|
||||
</TableRow>
|
||||
<TableRow>
|
||||
<TableCell>
|
||||
<div className="font-medium">Liam Johnson</div>
|
||||
<div className="hidden text-sm text-muted-foreground md:inline">
|
||||
liam@example.com
|
||||
</div>
|
||||
</TableCell>
|
||||
<TableCell className="hidden xl:table-column">
|
||||
Sale
|
||||
</TableCell>
|
||||
<TableCell className="hidden xl:table-column">
|
||||
<Badge className="text-xs" variant="outline">
|
||||
Approved
|
||||
</Badge>
|
||||
</TableCell>
|
||||
<TableCell className="hidden md:table-cell lg:hidden xl:table-column">
|
||||
2023-06-27
|
||||
</TableCell>
|
||||
<TableCell className="text-right">$550.00</TableCell>
|
||||
</TableRow>
|
||||
</TableBody>
|
||||
</Table>
|
||||
</CardContent>
|
||||
</Card>
|
||||
<Card x-chunk="dashboard-01-chunk-5">
|
||||
<CardHeader>
|
||||
<CardTitle>Recent Sales</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="grid gap-8">
|
||||
<div className="flex items-center gap-4">
|
||||
<Avatar className="hidden h-9 w-9 sm:flex">
|
||||
<AvatarImage src="/avatars/01.png" alt="Avatar" />
|
||||
<AvatarFallback>OM</AvatarFallback>
|
||||
</Avatar>
|
||||
<div className="grid gap-1">
|
||||
<p className="text-sm font-medium leading-none">
|
||||
Olivia Martin
|
||||
</p>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
olivia.martin@email.com
|
||||
</p>
|
||||
</div>
|
||||
<div className="ml-auto font-medium">+$1,999.00</div>
|
||||
</div>
|
||||
<div className="flex items-center gap-4">
|
||||
<Avatar className="hidden h-9 w-9 sm:flex">
|
||||
<AvatarImage src="/avatars/02.png" alt="Avatar" />
|
||||
<AvatarFallback>JL</AvatarFallback>
|
||||
</Avatar>
|
||||
<div className="grid gap-1">
|
||||
<p className="text-sm font-medium leading-none">
|
||||
Jackson Lee
|
||||
</p>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
jackson.lee@email.com
|
||||
</p>
|
||||
</div>
|
||||
<div className="ml-auto font-medium">+$39.00</div>
|
||||
</div>
|
||||
<div className="flex items-center gap-4">
|
||||
<Avatar className="hidden h-9 w-9 sm:flex">
|
||||
<AvatarImage src="/avatars/03.png" alt="Avatar" />
|
||||
<AvatarFallback>IN</AvatarFallback>
|
||||
</Avatar>
|
||||
<div className="grid gap-1">
|
||||
<p className="text-sm font-medium leading-none">
|
||||
Isabella Nguyen
|
||||
</p>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
isabella.nguyen@email.com
|
||||
</p>
|
||||
</div>
|
||||
<div className="ml-auto font-medium">+$299.00</div>
|
||||
</div>
|
||||
<div className="flex items-center gap-4">
|
||||
<Avatar className="hidden h-9 w-9 sm:flex">
|
||||
<AvatarImage src="/avatars/04.png" alt="Avatar" />
|
||||
<AvatarFallback>WK</AvatarFallback>
|
||||
</Avatar>
|
||||
<div className="grid gap-1">
|
||||
<p className="text-sm font-medium leading-none">
|
||||
William Kim
|
||||
</p>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
will@email.com
|
||||
</p>
|
||||
</div>
|
||||
<div className="ml-auto font-medium">+$99.00</div>
|
||||
</div>
|
||||
<div className="flex items-center gap-4">
|
||||
<Avatar className="hidden h-9 w-9 sm:flex">
|
||||
<AvatarImage src="/avatars/05.png" alt="Avatar" />
|
||||
<AvatarFallback>SD</AvatarFallback>
|
||||
</Avatar>
|
||||
<div className="grid gap-1">
|
||||
<p className="text-sm font-medium leading-none">
|
||||
Sofia Davis
|
||||
</p>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
sofia.davis@email.com
|
||||
</p>
|
||||
</div>
|
||||
<div className="ml-auto font-medium">+$39.00</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -1,246 +0,0 @@
|
||||
import Link from "next/link"
|
||||
import {
|
||||
Bell,
|
||||
CircleUser,
|
||||
Home,
|
||||
LineChart,
|
||||
Menu,
|
||||
Package,
|
||||
Package2,
|
||||
Search,
|
||||
ShoppingCart,
|
||||
Users,
|
||||
} from "lucide-react"
|
||||
|
||||
import { Badge } from "@/registry/default/ui/badge"
|
||||
import { Button } from "@/registry/default/ui/button"
|
||||
import {
|
||||
Card,
|
||||
CardContent,
|
||||
CardDescription,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
} from "@/registry/default/ui/card"
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuLabel,
|
||||
DropdownMenuSeparator,
|
||||
DropdownMenuTrigger,
|
||||
} from "@/registry/default/ui/dropdown-menu"
|
||||
import { Input } from "@/registry/default/ui/input"
|
||||
import { Sheet, SheetContent, SheetTrigger } from "@/registry/default/ui/sheet"
|
||||
|
||||
export const description =
|
||||
"A products dashboard with a sidebar navigation and a main content area. The dashboard has a header with a search input and a user menu. The sidebar has a logo, navigation links, and a card with a call to action. The main content area shows an empty state with a call to action."
|
||||
|
||||
export const iframeHeight = "800px"
|
||||
|
||||
export const containerClassName = "w-full h-full"
|
||||
|
||||
export default function Dashboard() {
|
||||
return (
|
||||
<div className="grid min-h-screen w-full md:grid-cols-[220px_1fr] lg:grid-cols-[280px_1fr]">
|
||||
<div className="hidden border-r bg-muted/40 md:block">
|
||||
<div className="flex h-full max-h-screen flex-col gap-2">
|
||||
<div className="flex h-14 items-center border-b px-4 lg:h-[60px] lg:px-6">
|
||||
<Link href="/" className="flex items-center gap-2 font-semibold">
|
||||
<Package2 className="h-6 w-6" />
|
||||
<span className="">Acme Inc</span>
|
||||
</Link>
|
||||
<Button variant="outline" size="icon" className="ml-auto h-8 w-8">
|
||||
<Bell className="h-4 w-4" />
|
||||
<span className="sr-only">Toggle notifications</span>
|
||||
</Button>
|
||||
</div>
|
||||
<div className="flex-1">
|
||||
<nav className="grid items-start px-2 text-sm font-medium lg:px-4">
|
||||
<Link
|
||||
href="#"
|
||||
className="flex items-center gap-3 rounded-lg px-3 py-2 text-muted-foreground transition-all hover:text-primary"
|
||||
>
|
||||
<Home className="h-4 w-4" />
|
||||
Dashboard
|
||||
</Link>
|
||||
<Link
|
||||
href="#"
|
||||
className="flex items-center gap-3 rounded-lg px-3 py-2 text-muted-foreground transition-all hover:text-primary"
|
||||
>
|
||||
<ShoppingCart className="h-4 w-4" />
|
||||
Orders
|
||||
<Badge className="ml-auto flex h-6 w-6 shrink-0 items-center justify-center rounded-full">
|
||||
6
|
||||
</Badge>
|
||||
</Link>
|
||||
<Link
|
||||
href="#"
|
||||
className="flex items-center gap-3 rounded-lg bg-muted px-3 py-2 text-primary transition-all hover:text-primary"
|
||||
>
|
||||
<Package className="h-4 w-4" />
|
||||
Products{" "}
|
||||
</Link>
|
||||
<Link
|
||||
href="#"
|
||||
className="flex items-center gap-3 rounded-lg px-3 py-2 text-muted-foreground transition-all hover:text-primary"
|
||||
>
|
||||
<Users className="h-4 w-4" />
|
||||
Customers
|
||||
</Link>
|
||||
<Link
|
||||
href="#"
|
||||
className="flex items-center gap-3 rounded-lg px-3 py-2 text-muted-foreground transition-all hover:text-primary"
|
||||
>
|
||||
<LineChart className="h-4 w-4" />
|
||||
Analytics
|
||||
</Link>
|
||||
</nav>
|
||||
</div>
|
||||
<div className="mt-auto p-4">
|
||||
<Card x-chunk="dashboard-02-chunk-0">
|
||||
<CardHeader className="p-2 pt-0 md:p-4">
|
||||
<CardTitle>Upgrade to Pro</CardTitle>
|
||||
<CardDescription>
|
||||
Unlock all features and get unlimited access to our support
|
||||
team.
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="p-2 pt-0 md:p-4 md:pt-0">
|
||||
<Button size="sm" className="w-full">
|
||||
Upgrade
|
||||
</Button>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex flex-col">
|
||||
<header className="flex h-14 items-center gap-4 border-b bg-muted/40 px-4 lg:h-[60px] lg:px-6">
|
||||
<Sheet>
|
||||
<SheetTrigger asChild>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="icon"
|
||||
className="shrink-0 md:hidden"
|
||||
>
|
||||
<Menu className="h-5 w-5" />
|
||||
<span className="sr-only">Toggle navigation menu</span>
|
||||
</Button>
|
||||
</SheetTrigger>
|
||||
<SheetContent side="left" className="flex flex-col">
|
||||
<nav className="grid gap-2 text-lg font-medium">
|
||||
<Link
|
||||
href="#"
|
||||
className="flex items-center gap-2 text-lg font-semibold"
|
||||
>
|
||||
<Package2 className="h-6 w-6" />
|
||||
<span className="sr-only">Acme Inc</span>
|
||||
</Link>
|
||||
<Link
|
||||
href="#"
|
||||
className="mx-[-0.65rem] flex items-center gap-4 rounded-xl px-3 py-2 text-muted-foreground hover:text-foreground"
|
||||
>
|
||||
<Home className="h-5 w-5" />
|
||||
Dashboard
|
||||
</Link>
|
||||
<Link
|
||||
href="#"
|
||||
className="mx-[-0.65rem] flex items-center gap-4 rounded-xl bg-muted px-3 py-2 text-foreground hover:text-foreground"
|
||||
>
|
||||
<ShoppingCart className="h-5 w-5" />
|
||||
Orders
|
||||
<Badge className="ml-auto flex h-6 w-6 shrink-0 items-center justify-center rounded-full">
|
||||
6
|
||||
</Badge>
|
||||
</Link>
|
||||
<Link
|
||||
href="#"
|
||||
className="mx-[-0.65rem] flex items-center gap-4 rounded-xl px-3 py-2 text-muted-foreground hover:text-foreground"
|
||||
>
|
||||
<Package className="h-5 w-5" />
|
||||
Products
|
||||
</Link>
|
||||
<Link
|
||||
href="#"
|
||||
className="mx-[-0.65rem] flex items-center gap-4 rounded-xl px-3 py-2 text-muted-foreground hover:text-foreground"
|
||||
>
|
||||
<Users className="h-5 w-5" />
|
||||
Customers
|
||||
</Link>
|
||||
<Link
|
||||
href="#"
|
||||
className="mx-[-0.65rem] flex items-center gap-4 rounded-xl px-3 py-2 text-muted-foreground hover:text-foreground"
|
||||
>
|
||||
<LineChart className="h-5 w-5" />
|
||||
Analytics
|
||||
</Link>
|
||||
</nav>
|
||||
<div className="mt-auto">
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Upgrade to Pro</CardTitle>
|
||||
<CardDescription>
|
||||
Unlock all features and get unlimited access to our
|
||||
support team.
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<Button size="sm" className="w-full">
|
||||
Upgrade
|
||||
</Button>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
</SheetContent>
|
||||
</Sheet>
|
||||
<div className="w-full flex-1">
|
||||
<form>
|
||||
<div className="relative">
|
||||
<Search className="absolute left-2.5 top-2.5 h-4 w-4 text-muted-foreground" />
|
||||
<Input
|
||||
type="search"
|
||||
placeholder="Search products..."
|
||||
className="w-full appearance-none bg-background pl-8 shadow-none md:w-2/3 lg:w-1/3"
|
||||
/>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button variant="secondary" size="icon" className="rounded-full">
|
||||
<CircleUser className="h-5 w-5" />
|
||||
<span className="sr-only">Toggle user menu</span>
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="end">
|
||||
<DropdownMenuLabel>My Account</DropdownMenuLabel>
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuItem>Settings</DropdownMenuItem>
|
||||
<DropdownMenuItem>Support</DropdownMenuItem>
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuItem>Logout</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</header>
|
||||
<main className="flex flex-1 flex-col gap-4 p-4 lg:gap-6 lg:p-6">
|
||||
<div className="flex items-center">
|
||||
<h1 className="text-lg font-semibold md:text-2xl">Inventory</h1>
|
||||
</div>
|
||||
<div
|
||||
className="flex flex-1 items-center justify-center rounded-lg border border-dashed shadow-sm" x-chunk="dashboard-02-chunk-1"
|
||||
>
|
||||
<div className="flex flex-col items-center gap-1 text-center">
|
||||
<h3 className="text-2xl font-bold tracking-tight">
|
||||
You have no products
|
||||
</h3>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
You can start selling as soon as you add a product.
|
||||
</p>
|
||||
<Button className="mt-4">Add Product</Button>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -1,462 +0,0 @@
|
||||
import {
|
||||
Bird,
|
||||
Book,
|
||||
Bot,
|
||||
Code2,
|
||||
CornerDownLeft,
|
||||
LifeBuoy,
|
||||
Mic,
|
||||
Paperclip,
|
||||
Rabbit,
|
||||
Settings,
|
||||
Settings2,
|
||||
Share,
|
||||
SquareTerminal,
|
||||
SquareUser,
|
||||
Triangle,
|
||||
Turtle,
|
||||
} from "lucide-react"
|
||||
|
||||
import { Badge } from "@/registry/default/ui/badge"
|
||||
import { Button } from "@/registry/default/ui/button"
|
||||
import {
|
||||
Drawer,
|
||||
DrawerContent,
|
||||
DrawerDescription,
|
||||
DrawerHeader,
|
||||
DrawerTitle,
|
||||
DrawerTrigger,
|
||||
} from "@/registry/default/ui/drawer"
|
||||
import { Input } from "@/registry/default/ui/input"
|
||||
import { Label } from "@/registry/default/ui/label"
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from "@/registry/default/ui/select"
|
||||
import { Textarea } from "@/registry/default/ui/textarea"
|
||||
import {
|
||||
Tooltip,
|
||||
TooltipContent,
|
||||
TooltipTrigger,
|
||||
} from "@/registry/default/ui/tooltip"
|
||||
|
||||
export const description =
|
||||
"An AI playground with a sidebar navigation and a main content area. The playground has a header with a settings drawer and a share button. The sidebar has navigation links and a user menu. The main content area shows a form to configure the model and messages."
|
||||
|
||||
export const iframeHeight = "740px"
|
||||
|
||||
export const containerClassName = "w-full h-full"
|
||||
|
||||
export default function Dashboard() {
|
||||
return (
|
||||
<div className="grid h-screen w-full pl-[56px]">
|
||||
<aside className="inset-y fixed left-0 z-20 flex h-full flex-col border-r">
|
||||
<div className="border-b p-2">
|
||||
<Button variant="outline" size="icon" aria-label="Home">
|
||||
<Triangle className="size-5 fill-foreground" />
|
||||
</Button>
|
||||
</div>
|
||||
<nav className="grid gap-1 p-2">
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="rounded-lg bg-muted"
|
||||
aria-label="Playground"
|
||||
>
|
||||
<SquareTerminal className="size-5" />
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent side="right" sideOffset={5}>
|
||||
Playground
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="rounded-lg"
|
||||
aria-label="Models"
|
||||
>
|
||||
<Bot className="size-5" />
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent side="right" sideOffset={5}>
|
||||
Models
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="rounded-lg"
|
||||
aria-label="API"
|
||||
>
|
||||
<Code2 className="size-5" />
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent side="right" sideOffset={5}>
|
||||
API
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="rounded-lg"
|
||||
aria-label="Documentation"
|
||||
>
|
||||
<Book className="size-5" />
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent side="right" sideOffset={5}>
|
||||
Documentation
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="rounded-lg"
|
||||
aria-label="Settings"
|
||||
>
|
||||
<Settings2 className="size-5" />
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent side="right" sideOffset={5}>
|
||||
Settings
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</nav>
|
||||
<nav className="mt-auto grid gap-1 p-2">
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="mt-auto rounded-lg"
|
||||
aria-label="Help"
|
||||
>
|
||||
<LifeBuoy className="size-5" />
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent side="right" sideOffset={5}>
|
||||
Help
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="mt-auto rounded-lg"
|
||||
aria-label="Account"
|
||||
>
|
||||
<SquareUser className="size-5" />
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent side="right" sideOffset={5}>
|
||||
Account
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</nav>
|
||||
</aside>
|
||||
<div className="flex flex-col">
|
||||
<header className="sticky top-0 z-10 flex h-[57px] items-center gap-1 border-b bg-background px-4">
|
||||
<h1 className="text-xl font-semibold">Playground</h1>
|
||||
<Drawer>
|
||||
<DrawerTrigger asChild>
|
||||
<Button variant="ghost" size="icon" className="md:hidden">
|
||||
<Settings className="size-4" />
|
||||
<span className="sr-only">Settings</span>
|
||||
</Button>
|
||||
</DrawerTrigger>
|
||||
<DrawerContent className="max-h-[80vh]">
|
||||
<DrawerHeader>
|
||||
<DrawerTitle>Configuration</DrawerTitle>
|
||||
<DrawerDescription>
|
||||
Configure the settings for the model and messages.
|
||||
</DrawerDescription>
|
||||
</DrawerHeader>
|
||||
<form className="grid w-full items-start gap-6 overflow-auto p-4 pt-0">
|
||||
<fieldset className="grid gap-6 rounded-lg border p-4">
|
||||
<legend className="-ml-1 px-1 text-sm font-medium">
|
||||
Settings
|
||||
</legend>
|
||||
<div className="grid gap-3">
|
||||
<Label htmlFor="model">Model</Label>
|
||||
<Select>
|
||||
<SelectTrigger
|
||||
id="model"
|
||||
className="items-start [&_[data-description]]:hidden"
|
||||
>
|
||||
<SelectValue placeholder="Select a model" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="genesis">
|
||||
<div className="flex items-start gap-3 text-muted-foreground">
|
||||
<Rabbit className="size-5" />
|
||||
<div className="grid gap-0.5">
|
||||
<p>
|
||||
Neural{" "}
|
||||
<span className="font-medium text-foreground">
|
||||
Genesis
|
||||
</span>
|
||||
</p>
|
||||
<p className="text-xs" data-description>
|
||||
Our fastest model for general use cases.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</SelectItem>
|
||||
<SelectItem value="explorer">
|
||||
<div className="flex items-start gap-3 text-muted-foreground">
|
||||
<Bird className="size-5" />
|
||||
<div className="grid gap-0.5">
|
||||
<p>
|
||||
Neural{" "}
|
||||
<span className="font-medium text-foreground">
|
||||
Explorer
|
||||
</span>
|
||||
</p>
|
||||
<p className="text-xs" data-description>
|
||||
Performance and speed for efficiency.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</SelectItem>
|
||||
<SelectItem value="quantum">
|
||||
<div className="flex items-start gap-3 text-muted-foreground">
|
||||
<Turtle className="size-5" />
|
||||
<div className="grid gap-0.5">
|
||||
<p>
|
||||
Neural{" "}
|
||||
<span className="font-medium text-foreground">
|
||||
Quantum
|
||||
</span>
|
||||
</p>
|
||||
<p className="text-xs" data-description>
|
||||
The most powerful model for complex
|
||||
computations.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
<div className="grid gap-3">
|
||||
<Label htmlFor="temperature">Temperature</Label>
|
||||
<Input id="temperature" type="number" placeholder="0.4" />
|
||||
</div>
|
||||
<div className="grid gap-3">
|
||||
<Label htmlFor="top-p">Top P</Label>
|
||||
<Input id="top-p" type="number" placeholder="0.7" />
|
||||
</div>
|
||||
<div className="grid gap-3">
|
||||
<Label htmlFor="top-k">Top K</Label>
|
||||
<Input id="top-k" type="number" placeholder="0.0" />
|
||||
</div>
|
||||
</fieldset>
|
||||
<fieldset className="grid gap-6 rounded-lg border p-4">
|
||||
<legend className="-ml-1 px-1 text-sm font-medium">
|
||||
Messages
|
||||
</legend>
|
||||
<div className="grid gap-3">
|
||||
<Label htmlFor="role">Role</Label>
|
||||
<Select defaultValue="system">
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder="Select a role" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="system">System</SelectItem>
|
||||
<SelectItem value="user">User</SelectItem>
|
||||
<SelectItem value="assistant">Assistant</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
<div className="grid gap-3">
|
||||
<Label htmlFor="content">Content</Label>
|
||||
<Textarea id="content" placeholder="You are a..." />
|
||||
</div>
|
||||
</fieldset>
|
||||
</form>
|
||||
</DrawerContent>
|
||||
</Drawer>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
className="ml-auto gap-1.5 text-sm"
|
||||
>
|
||||
<Share className="size-3.5" />
|
||||
Share
|
||||
</Button>
|
||||
</header>
|
||||
<main className="grid flex-1 gap-4 overflow-auto p-4 md:grid-cols-2 lg:grid-cols-3">
|
||||
<div
|
||||
className="relative hidden flex-col items-start gap-8 md:flex" x-chunk="dashboard-03-chunk-0"
|
||||
>
|
||||
<form className="grid w-full items-start gap-6">
|
||||
<fieldset className="grid gap-6 rounded-lg border p-4">
|
||||
<legend className="-ml-1 px-1 text-sm font-medium">
|
||||
Settings
|
||||
</legend>
|
||||
<div className="grid gap-3">
|
||||
<Label htmlFor="model">Model</Label>
|
||||
<Select>
|
||||
<SelectTrigger
|
||||
id="model"
|
||||
className="items-start [&_[data-description]]:hidden"
|
||||
>
|
||||
<SelectValue placeholder="Select a model" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="genesis">
|
||||
<div className="flex items-start gap-3 text-muted-foreground">
|
||||
<Rabbit className="size-5" />
|
||||
<div className="grid gap-0.5">
|
||||
<p>
|
||||
Neural{" "}
|
||||
<span className="font-medium text-foreground">
|
||||
Genesis
|
||||
</span>
|
||||
</p>
|
||||
<p className="text-xs" data-description>
|
||||
Our fastest model for general use cases.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</SelectItem>
|
||||
<SelectItem value="explorer">
|
||||
<div className="flex items-start gap-3 text-muted-foreground">
|
||||
<Bird className="size-5" />
|
||||
<div className="grid gap-0.5">
|
||||
<p>
|
||||
Neural{" "}
|
||||
<span className="font-medium text-foreground">
|
||||
Explorer
|
||||
</span>
|
||||
</p>
|
||||
<p className="text-xs" data-description>
|
||||
Performance and speed for efficiency.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</SelectItem>
|
||||
<SelectItem value="quantum">
|
||||
<div className="flex items-start gap-3 text-muted-foreground">
|
||||
<Turtle className="size-5" />
|
||||
<div className="grid gap-0.5">
|
||||
<p>
|
||||
Neural{" "}
|
||||
<span className="font-medium text-foreground">
|
||||
Quantum
|
||||
</span>
|
||||
</p>
|
||||
<p className="text-xs" data-description>
|
||||
The most powerful model for complex computations.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
<div className="grid gap-3">
|
||||
<Label htmlFor="temperature">Temperature</Label>
|
||||
<Input id="temperature" type="number" placeholder="0.4" />
|
||||
</div>
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div className="grid gap-3">
|
||||
<Label htmlFor="top-p">Top P</Label>
|
||||
<Input id="top-p" type="number" placeholder="0.7" />
|
||||
</div>
|
||||
<div className="grid gap-3">
|
||||
<Label htmlFor="top-k">Top K</Label>
|
||||
<Input id="top-k" type="number" placeholder="0.0" />
|
||||
</div>
|
||||
</div>
|
||||
</fieldset>
|
||||
<fieldset className="grid gap-6 rounded-lg border p-4">
|
||||
<legend className="-ml-1 px-1 text-sm font-medium">
|
||||
Messages
|
||||
</legend>
|
||||
<div className="grid gap-3">
|
||||
<Label htmlFor="role">Role</Label>
|
||||
<Select defaultValue="system">
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder="Select a role" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="system">System</SelectItem>
|
||||
<SelectItem value="user">User</SelectItem>
|
||||
<SelectItem value="assistant">Assistant</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
<div className="grid gap-3">
|
||||
<Label htmlFor="content">Content</Label>
|
||||
<Textarea
|
||||
id="content"
|
||||
placeholder="You are a..."
|
||||
className="min-h-[9.5rem]"
|
||||
/>
|
||||
</div>
|
||||
</fieldset>
|
||||
</form>
|
||||
</div>
|
||||
<div className="relative flex h-full min-h-[50vh] flex-col rounded-xl bg-muted/50 p-4 lg:col-span-2">
|
||||
<Badge variant="outline" className="absolute right-3 top-3">
|
||||
Output
|
||||
</Badge>
|
||||
<div className="flex-1" />
|
||||
<form
|
||||
className="relative overflow-hidden rounded-lg border bg-background focus-within:ring-1 focus-within:ring-ring" x-chunk="dashboard-03-chunk-1"
|
||||
>
|
||||
<Label htmlFor="message" className="sr-only">
|
||||
Message
|
||||
</Label>
|
||||
<Textarea
|
||||
id="message"
|
||||
placeholder="Type your message here..."
|
||||
className="min-h-12 resize-none border-0 p-3 shadow-none focus-visible:ring-0"
|
||||
/>
|
||||
<div className="flex items-center p-3 pt-0">
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Button variant="ghost" size="icon">
|
||||
<Paperclip className="size-4" />
|
||||
<span className="sr-only">Attach file</span>
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent side="top">Attach File</TooltipContent>
|
||||
</Tooltip>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Button variant="ghost" size="icon">
|
||||
<Mic className="size-4" />
|
||||
<span className="sr-only">Use Microphone</span>
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent side="top">Use Microphone</TooltipContent>
|
||||
</Tooltip>
|
||||
<Button type="submit" size="sm" className="ml-auto gap-1.5">
|
||||
Send Message
|
||||
<CornerDownLeft className="size-3.5" />
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -1,222 +0,0 @@
|
||||
import Link from "next/link"
|
||||
import { CircleUser, Menu, Package2, Search } from "lucide-react"
|
||||
|
||||
import { Button } from "@/registry/default/ui/button"
|
||||
import {
|
||||
Card,
|
||||
CardContent,
|
||||
CardDescription,
|
||||
CardFooter,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
} from "@/registry/default/ui/card"
|
||||
import { Checkbox } from "@/registry/default/ui/checkbox"
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuLabel,
|
||||
DropdownMenuSeparator,
|
||||
DropdownMenuTrigger,
|
||||
} from "@/registry/default/ui/dropdown-menu"
|
||||
import { Input } from "@/registry/default/ui/input"
|
||||
import { Sheet, SheetContent, SheetTrigger } from "@/registry/default/ui/sheet"
|
||||
|
||||
export const description =
|
||||
"A settings page. The settings page has a sidebar navigation and a main content area. The main content area has a form to update the store name and a form to update the plugins directory. The sidebar navigation has links to general, security, integrations, support, organizations, and advanced settings."
|
||||
|
||||
export const iframeHeight = "780px"
|
||||
|
||||
export const containerClassName = "w-full h-full"
|
||||
|
||||
export default function Dashboard() {
|
||||
return (
|
||||
<div className="flex min-h-screen w-full flex-col">
|
||||
<header className="sticky top-0 flex h-16 items-center gap-4 border-b bg-background px-4 md:px-6">
|
||||
<nav className="hidden flex-col gap-6 text-lg font-medium md:flex md:flex-row md:items-center md:gap-5 md:text-sm lg:gap-6">
|
||||
<Link
|
||||
href="#"
|
||||
className="flex items-center gap-2 text-lg font-semibold md:text-base"
|
||||
>
|
||||
<Package2 className="h-6 w-6" />
|
||||
<span className="sr-only">Acme Inc</span>
|
||||
</Link>
|
||||
<Link
|
||||
href="#"
|
||||
className="text-muted-foreground transition-colors hover:text-foreground"
|
||||
>
|
||||
Dashboard
|
||||
</Link>
|
||||
<Link
|
||||
href="#"
|
||||
className="text-muted-foreground transition-colors hover:text-foreground"
|
||||
>
|
||||
Orders
|
||||
</Link>
|
||||
<Link
|
||||
href="#"
|
||||
className="text-muted-foreground transition-colors hover:text-foreground"
|
||||
>
|
||||
Products
|
||||
</Link>
|
||||
<Link
|
||||
href="#"
|
||||
className="text-muted-foreground transition-colors hover:text-foreground"
|
||||
>
|
||||
Customers
|
||||
</Link>
|
||||
<Link
|
||||
href="#"
|
||||
className="text-foreground transition-colors hover:text-foreground"
|
||||
>
|
||||
Settings
|
||||
</Link>
|
||||
</nav>
|
||||
<Sheet>
|
||||
<SheetTrigger asChild>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="icon"
|
||||
className="shrink-0 md:hidden"
|
||||
>
|
||||
<Menu className="h-5 w-5" />
|
||||
<span className="sr-only">Toggle navigation menu</span>
|
||||
</Button>
|
||||
</SheetTrigger>
|
||||
<SheetContent side="left">
|
||||
<nav className="grid gap-6 text-lg font-medium">
|
||||
<Link
|
||||
href="#"
|
||||
className="flex items-center gap-2 text-lg font-semibold"
|
||||
>
|
||||
<Package2 className="h-6 w-6" />
|
||||
<span className="sr-only">Acme Inc</span>
|
||||
</Link>
|
||||
<Link
|
||||
href="#"
|
||||
className="text-muted-foreground hover:text-foreground"
|
||||
>
|
||||
Dashboard
|
||||
</Link>
|
||||
<Link
|
||||
href="#"
|
||||
className="text-muted-foreground hover:text-foreground"
|
||||
>
|
||||
Orders
|
||||
</Link>
|
||||
<Link
|
||||
href="#"
|
||||
className="text-muted-foreground hover:text-foreground"
|
||||
>
|
||||
Products
|
||||
</Link>
|
||||
<Link
|
||||
href="#"
|
||||
className="text-muted-foreground hover:text-foreground"
|
||||
>
|
||||
Customers
|
||||
</Link>
|
||||
<Link href="#" className="hover:text-foreground">
|
||||
Settings
|
||||
</Link>
|
||||
</nav>
|
||||
</SheetContent>
|
||||
</Sheet>
|
||||
<div className="flex w-full items-center gap-4 md:ml-auto md:gap-2 lg:gap-4">
|
||||
<form className="ml-auto flex-1 sm:flex-initial">
|
||||
<div className="relative">
|
||||
<Search className="absolute left-2.5 top-2.5 h-4 w-4 text-muted-foreground" />
|
||||
<Input
|
||||
type="search"
|
||||
placeholder="Search products..."
|
||||
className="pl-8 sm:w-[300px] md:w-[200px] lg:w-[300px]"
|
||||
/>
|
||||
</div>
|
||||
</form>
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button variant="secondary" size="icon" className="rounded-full">
|
||||
<CircleUser className="h-5 w-5" />
|
||||
<span className="sr-only">Toggle user menu</span>
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="end">
|
||||
<DropdownMenuLabel>My Account</DropdownMenuLabel>
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuItem>Settings</DropdownMenuItem>
|
||||
<DropdownMenuItem>Support</DropdownMenuItem>
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuItem>Logout</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</div>
|
||||
</header>
|
||||
<main className="flex min-h-[calc(100vh_-_theme(spacing.16))] flex-1 flex-col gap-4 bg-muted/40 p-4 md:gap-8 md:p-10">
|
||||
<div className="mx-auto grid w-full max-w-6xl gap-2">
|
||||
<h1 className="text-3xl font-semibold">Settings</h1>
|
||||
</div>
|
||||
<div className="mx-auto grid w-full max-w-6xl items-start gap-6 md:grid-cols-[180px_1fr] lg:grid-cols-[250px_1fr]">
|
||||
<nav
|
||||
className="grid gap-4 text-sm text-muted-foreground" x-chunk="dashboard-04-chunk-0"
|
||||
>
|
||||
<Link href="#" className="font-semibold text-primary">
|
||||
General
|
||||
</Link>
|
||||
<Link href="#">Security</Link>
|
||||
<Link href="#">Integrations</Link>
|
||||
<Link href="#">Support</Link>
|
||||
<Link href="#">Organizations</Link>
|
||||
<Link href="#">Advanced</Link>
|
||||
</nav>
|
||||
<div className="grid gap-6">
|
||||
<Card x-chunk="dashboard-04-chunk-1">
|
||||
<CardHeader>
|
||||
<CardTitle>Store Name</CardTitle>
|
||||
<CardDescription>
|
||||
Used to identify your store in the marketplace.
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<form>
|
||||
<Input placeholder="Store Name" />
|
||||
</form>
|
||||
</CardContent>
|
||||
<CardFooter className="border-t px-6 py-4">
|
||||
<Button>Save</Button>
|
||||
</CardFooter>
|
||||
</Card>
|
||||
<Card x-chunk="dashboard-04-chunk-2">
|
||||
<CardHeader>
|
||||
<CardTitle>Plugins Directory</CardTitle>
|
||||
<CardDescription>
|
||||
The directory within your project, in which your plugins are
|
||||
located.
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<form className="flex flex-col gap-4">
|
||||
<Input
|
||||
placeholder="Project Name"
|
||||
defaultValue="/content/plugins"
|
||||
/>
|
||||
<div className="flex items-center space-x-2">
|
||||
<Checkbox id="include" defaultChecked />
|
||||
<label
|
||||
htmlFor="include"
|
||||
className="text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70"
|
||||
>
|
||||
Allow administrators to change the directory.
|
||||
</label>
|
||||
</div>
|
||||
</form>
|
||||
</CardContent>
|
||||
<CardFooter className="border-t px-6 py-4">
|
||||
<Button>Save</Button>
|
||||
</CardFooter>
|
||||
</Card>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -1,721 +0,0 @@
|
||||
import Image from "next/image"
|
||||
import Link from "next/link"
|
||||
import {
|
||||
ChevronLeft,
|
||||
ChevronRight,
|
||||
Copy,
|
||||
CreditCard,
|
||||
File,
|
||||
Home,
|
||||
LineChart,
|
||||
ListFilter,
|
||||
MoreVertical,
|
||||
Package,
|
||||
Package2,
|
||||
PanelLeft,
|
||||
Search,
|
||||
Settings,
|
||||
ShoppingCart,
|
||||
Truck,
|
||||
Users2,
|
||||
} from "lucide-react"
|
||||
|
||||
import { Badge } from "@/registry/default/ui/badge"
|
||||
import {
|
||||
Breadcrumb,
|
||||
BreadcrumbItem,
|
||||
BreadcrumbLink,
|
||||
BreadcrumbList,
|
||||
BreadcrumbPage,
|
||||
BreadcrumbSeparator,
|
||||
} from "@/registry/default/ui/breadcrumb"
|
||||
import { Button } from "@/registry/default/ui/button"
|
||||
import {
|
||||
Card,
|
||||
CardContent,
|
||||
CardDescription,
|
||||
CardFooter,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
} from "@/registry/default/ui/card"
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuCheckboxItem,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuLabel,
|
||||
DropdownMenuSeparator,
|
||||
DropdownMenuTrigger,
|
||||
} from "@/registry/default/ui/dropdown-menu"
|
||||
import { Input } from "@/registry/default/ui/input"
|
||||
import {
|
||||
Pagination,
|
||||
PaginationContent,
|
||||
PaginationItem,
|
||||
} from "@/registry/default/ui/pagination"
|
||||
import { Progress } from "@/registry/default/ui/progress"
|
||||
import { Separator } from "@/registry/default/ui/separator"
|
||||
import { Sheet, SheetContent, SheetTrigger } from "@/registry/default/ui/sheet"
|
||||
import {
|
||||
Table,
|
||||
TableBody,
|
||||
TableCell,
|
||||
TableHead,
|
||||
TableHeader,
|
||||
TableRow,
|
||||
} from "@/registry/default/ui/table"
|
||||
import {
|
||||
Tabs,
|
||||
TabsContent,
|
||||
TabsList,
|
||||
TabsTrigger,
|
||||
} from "@/registry/default/ui/tabs"
|
||||
import {
|
||||
Tooltip,
|
||||
TooltipContent,
|
||||
TooltipTrigger,
|
||||
} from "@/registry/default/ui/tooltip"
|
||||
|
||||
export const description =
|
||||
"An orders dashboard with a sidebar navigation. The sidebar has icon navigation. The content area has a breadcrumb and search in the header. The main area has a list of recent orders with a filter and export button. The main area also has a detailed view of a single order with order details, shipping information, billing information, customer information, and payment information."
|
||||
|
||||
export const iframeHeight = "1112px"
|
||||
|
||||
export const containerClassName = "w-full h-full"
|
||||
|
||||
export default function Dashboard() {
|
||||
return (
|
||||
<div className="flex min-h-screen w-full flex-col bg-muted/40">
|
||||
<aside className="fixed inset-y-0 left-0 z-10 hidden w-14 flex-col border-r bg-background sm:flex">
|
||||
<nav className="flex flex-col items-center gap-4 px-2 sm:py-5">
|
||||
<Link
|
||||
href="#"
|
||||
className="group flex h-9 w-9 shrink-0 items-center justify-center gap-2 rounded-full bg-primary text-lg font-semibold text-primary-foreground md:h-8 md:w-8 md:text-base"
|
||||
>
|
||||
<Package2 className="h-4 w-4 transition-all group-hover:scale-110" />
|
||||
<span className="sr-only">Acme Inc</span>
|
||||
</Link>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Link
|
||||
href="#"
|
||||
className="flex h-9 w-9 items-center justify-center rounded-lg text-muted-foreground transition-colors hover:text-foreground md:h-8 md:w-8"
|
||||
>
|
||||
<Home className="h-5 w-5" />
|
||||
<span className="sr-only">Dashboard</span>
|
||||
</Link>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent side="right">Dashboard</TooltipContent>
|
||||
</Tooltip>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Link
|
||||
href="#"
|
||||
className="flex h-9 w-9 items-center justify-center rounded-lg bg-accent text-accent-foreground transition-colors hover:text-foreground md:h-8 md:w-8"
|
||||
>
|
||||
<ShoppingCart className="h-5 w-5" />
|
||||
<span className="sr-only">Orders</span>
|
||||
</Link>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent side="right">Orders</TooltipContent>
|
||||
</Tooltip>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Link
|
||||
href="#"
|
||||
className="flex h-9 w-9 items-center justify-center rounded-lg text-muted-foreground transition-colors hover:text-foreground md:h-8 md:w-8"
|
||||
>
|
||||
<Package className="h-5 w-5" />
|
||||
<span className="sr-only">Products</span>
|
||||
</Link>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent side="right">Products</TooltipContent>
|
||||
</Tooltip>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Link
|
||||
href="#"
|
||||
className="flex h-9 w-9 items-center justify-center rounded-lg text-muted-foreground transition-colors hover:text-foreground md:h-8 md:w-8"
|
||||
>
|
||||
<Users2 className="h-5 w-5" />
|
||||
<span className="sr-only">Customers</span>
|
||||
</Link>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent side="right">Customers</TooltipContent>
|
||||
</Tooltip>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Link
|
||||
href="#"
|
||||
className="flex h-9 w-9 items-center justify-center rounded-lg text-muted-foreground transition-colors hover:text-foreground md:h-8 md:w-8"
|
||||
>
|
||||
<LineChart className="h-5 w-5" />
|
||||
<span className="sr-only">Analytics</span>
|
||||
</Link>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent side="right">Analytics</TooltipContent>
|
||||
</Tooltip>
|
||||
</nav>
|
||||
<nav className="mt-auto flex flex-col items-center gap-4 px-2 sm:py-5">
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Link
|
||||
href="#"
|
||||
className="flex h-9 w-9 items-center justify-center rounded-lg text-muted-foreground transition-colors hover:text-foreground md:h-8 md:w-8"
|
||||
>
|
||||
<Settings className="h-5 w-5" />
|
||||
<span className="sr-only">Settings</span>
|
||||
</Link>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent side="right">Settings</TooltipContent>
|
||||
</Tooltip>
|
||||
</nav>
|
||||
</aside>
|
||||
<div className="flex flex-col sm:gap-4 sm:py-4 sm:pl-14">
|
||||
<header className="sticky top-0 z-30 flex h-14 items-center gap-4 border-b bg-background px-4 sm:static sm:h-auto sm:border-0 sm:bg-transparent sm:px-6">
|
||||
<Sheet>
|
||||
<SheetTrigger asChild>
|
||||
<Button size="icon" variant="outline" className="sm:hidden">
|
||||
<PanelLeft className="h-5 w-5" />
|
||||
<span className="sr-only">Toggle Menu</span>
|
||||
</Button>
|
||||
</SheetTrigger>
|
||||
<SheetContent side="left" className="sm:max-w-xs">
|
||||
<nav className="grid gap-6 text-lg font-medium">
|
||||
<Link
|
||||
href="#"
|
||||
className="group flex h-10 w-10 shrink-0 items-center justify-center gap-2 rounded-full bg-primary text-lg font-semibold text-primary-foreground md:text-base"
|
||||
>
|
||||
<Package2 className="h-5 w-5 transition-all group-hover:scale-110" />
|
||||
<span className="sr-only">Acme Inc</span>
|
||||
</Link>
|
||||
<Link
|
||||
href="#"
|
||||
className="flex items-center gap-4 px-2.5 text-muted-foreground hover:text-foreground"
|
||||
>
|
||||
<Home className="h-5 w-5" />
|
||||
Dashboard
|
||||
</Link>
|
||||
<Link
|
||||
href="#"
|
||||
className="flex items-center gap-4 px-2.5 text-foreground"
|
||||
>
|
||||
<ShoppingCart className="h-5 w-5" />
|
||||
Orders
|
||||
</Link>
|
||||
<Link
|
||||
href="#"
|
||||
className="flex items-center gap-4 px-2.5 text-muted-foreground hover:text-foreground"
|
||||
>
|
||||
<Package className="h-5 w-5" />
|
||||
Products
|
||||
</Link>
|
||||
<Link
|
||||
href="#"
|
||||
className="flex items-center gap-4 px-2.5 text-muted-foreground hover:text-foreground"
|
||||
>
|
||||
<Users2 className="h-5 w-5" />
|
||||
Customers
|
||||
</Link>
|
||||
<Link
|
||||
href="#"
|
||||
className="flex items-center gap-4 px-2.5 text-muted-foreground hover:text-foreground"
|
||||
>
|
||||
<LineChart className="h-5 w-5" />
|
||||
Settings
|
||||
</Link>
|
||||
</nav>
|
||||
</SheetContent>
|
||||
</Sheet>
|
||||
<Breadcrumb className="hidden md:flex">
|
||||
<BreadcrumbList>
|
||||
<BreadcrumbItem>
|
||||
<BreadcrumbLink asChild>
|
||||
<Link href="#">Dashboard</Link>
|
||||
</BreadcrumbLink>
|
||||
</BreadcrumbItem>
|
||||
<BreadcrumbSeparator />
|
||||
<BreadcrumbItem>
|
||||
<BreadcrumbLink asChild>
|
||||
<Link href="#">Orders</Link>
|
||||
</BreadcrumbLink>
|
||||
</BreadcrumbItem>
|
||||
<BreadcrumbSeparator />
|
||||
<BreadcrumbItem>
|
||||
<BreadcrumbPage>Recent Orders</BreadcrumbPage>
|
||||
</BreadcrumbItem>
|
||||
</BreadcrumbList>
|
||||
</Breadcrumb>
|
||||
<div className="relative ml-auto flex-1 md:grow-0">
|
||||
<Search className="absolute left-2.5 top-2.5 h-4 w-4 text-muted-foreground" />
|
||||
<Input
|
||||
type="search"
|
||||
placeholder="Search..."
|
||||
className="w-full rounded-lg bg-background pl-8 md:w-[200px] lg:w-[336px]"
|
||||
/>
|
||||
</div>
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="icon"
|
||||
className="overflow-hidden rounded-full"
|
||||
>
|
||||
<Image
|
||||
src="/placeholder-user.jpg"
|
||||
width={36}
|
||||
height={36}
|
||||
alt="Avatar"
|
||||
className="overflow-hidden rounded-full"
|
||||
loading="lazy"
|
||||
/>
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="end">
|
||||
<DropdownMenuLabel>My Account</DropdownMenuLabel>
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuItem>Settings</DropdownMenuItem>
|
||||
<DropdownMenuItem>Support</DropdownMenuItem>
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuItem>Logout</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</header>
|
||||
<main className="grid flex-1 items-start gap-4 p-4 sm:px-6 sm:py-0 md:gap-8 lg:grid-cols-3 xl:grid-cols-3">
|
||||
<div className="grid auto-rows-max items-start gap-4 md:gap-8 lg:col-span-2">
|
||||
<div className="grid gap-4 sm:grid-cols-2 md:grid-cols-4 lg:grid-cols-2 xl:grid-cols-4">
|
||||
<Card className="sm:col-span-2" x-chunk="dashboard-05-chunk-0">
|
||||
<CardHeader className="pb-3">
|
||||
<CardTitle>Your Orders</CardTitle>
|
||||
<CardDescription className="max-w-lg text-balance leading-relaxed">
|
||||
Introducing Our Dynamic Orders Dashboard for Seamless
|
||||
Management and Insightful Analysis.
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardFooter>
|
||||
<Button>Create New Order</Button>
|
||||
</CardFooter>
|
||||
</Card>
|
||||
<Card x-chunk="dashboard-05-chunk-1">
|
||||
<CardHeader className="pb-2">
|
||||
<CardDescription>This Week</CardDescription>
|
||||
<CardTitle className="text-4xl">$1,329</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="text-xs text-muted-foreground">
|
||||
+25% from last week
|
||||
</div>
|
||||
</CardContent>
|
||||
<CardFooter>
|
||||
<Progress value={25} aria-label="25% increase" />
|
||||
</CardFooter>
|
||||
</Card>
|
||||
<Card x-chunk="dashboard-05-chunk-2">
|
||||
<CardHeader className="pb-2">
|
||||
<CardDescription>This Month</CardDescription>
|
||||
<CardTitle className="text-4xl">$5,329</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="text-xs text-muted-foreground">
|
||||
+10% from last month
|
||||
</div>
|
||||
</CardContent>
|
||||
<CardFooter>
|
||||
<Progress value={12} aria-label="12% increase" />
|
||||
</CardFooter>
|
||||
</Card>
|
||||
</div>
|
||||
<Tabs defaultValue="week">
|
||||
<div className="flex items-center">
|
||||
<TabsList>
|
||||
<TabsTrigger value="week">Week</TabsTrigger>
|
||||
<TabsTrigger value="month">Month</TabsTrigger>
|
||||
<TabsTrigger value="year">Year</TabsTrigger>
|
||||
</TabsList>
|
||||
<div className="ml-auto flex items-center gap-2">
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
className="h-7 gap-1 text-sm"
|
||||
>
|
||||
<ListFilter className="h-3.5 w-3.5" />
|
||||
<span className="sr-only sm:not-sr-only">Filter</span>
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="end">
|
||||
<DropdownMenuLabel>Filter by</DropdownMenuLabel>
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuCheckboxItem checked>
|
||||
Fulfilled
|
||||
</DropdownMenuCheckboxItem>
|
||||
<DropdownMenuCheckboxItem>
|
||||
Declined
|
||||
</DropdownMenuCheckboxItem>
|
||||
<DropdownMenuCheckboxItem>
|
||||
Refunded
|
||||
</DropdownMenuCheckboxItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
<Button
|
||||
size="sm"
|
||||
variant="outline"
|
||||
className="h-7 gap-1 text-sm"
|
||||
>
|
||||
<File className="h-3.5 w-3.5" />
|
||||
<span className="sr-only sm:not-sr-only">Export</span>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
<TabsContent value="week">
|
||||
<Card x-chunk="dashboard-05-chunk-3">
|
||||
<CardHeader className="px-7">
|
||||
<CardTitle>Orders</CardTitle>
|
||||
<CardDescription>
|
||||
Recent orders from your store.
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead>Customer</TableHead>
|
||||
<TableHead className="hidden sm:table-cell">
|
||||
Type
|
||||
</TableHead>
|
||||
<TableHead className="hidden sm:table-cell">
|
||||
Status
|
||||
</TableHead>
|
||||
<TableHead className="hidden md:table-cell">
|
||||
Date
|
||||
</TableHead>
|
||||
<TableHead className="text-right">Amount</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
<TableRow className="bg-accent">
|
||||
<TableCell>
|
||||
<div className="font-medium">Liam Johnson</div>
|
||||
<div className="hidden text-sm text-muted-foreground md:inline">
|
||||
liam@example.com
|
||||
</div>
|
||||
</TableCell>
|
||||
<TableCell className="hidden sm:table-cell">
|
||||
Sale
|
||||
</TableCell>
|
||||
<TableCell className="hidden sm:table-cell">
|
||||
<Badge className="text-xs" variant="secondary">
|
||||
Fulfilled
|
||||
</Badge>
|
||||
</TableCell>
|
||||
<TableCell className="hidden md:table-cell">
|
||||
2023-06-23
|
||||
</TableCell>
|
||||
<TableCell className="text-right">$250.00</TableCell>
|
||||
</TableRow>
|
||||
<TableRow>
|
||||
<TableCell>
|
||||
<div className="font-medium">Olivia Smith</div>
|
||||
<div className="hidden text-sm text-muted-foreground md:inline">
|
||||
olivia@example.com
|
||||
</div>
|
||||
</TableCell>
|
||||
<TableCell className="hidden sm:table-cell">
|
||||
Refund
|
||||
</TableCell>
|
||||
<TableCell className="hidden sm:table-cell">
|
||||
<Badge className="text-xs" variant="outline">
|
||||
Declined
|
||||
</Badge>
|
||||
</TableCell>
|
||||
<TableCell className="hidden md:table-cell">
|
||||
2023-06-24
|
||||
</TableCell>
|
||||
<TableCell className="text-right">$150.00</TableCell>
|
||||
</TableRow>
|
||||
<TableRow>
|
||||
<TableCell>
|
||||
<div className="font-medium">Noah Williams</div>
|
||||
<div className="hidden text-sm text-muted-foreground md:inline">
|
||||
noah@example.com
|
||||
</div>
|
||||
</TableCell>
|
||||
<TableCell className="hidden sm:table-cell">
|
||||
Subscription
|
||||
</TableCell>
|
||||
<TableCell className="hidden sm:table-cell">
|
||||
<Badge className="text-xs" variant="secondary">
|
||||
Fulfilled
|
||||
</Badge>
|
||||
</TableCell>
|
||||
<TableCell className="hidden md:table-cell">
|
||||
2023-06-25
|
||||
</TableCell>
|
||||
<TableCell className="text-right">$350.00</TableCell>
|
||||
</TableRow>
|
||||
<TableRow>
|
||||
<TableCell>
|
||||
<div className="font-medium">Emma Brown</div>
|
||||
<div className="hidden text-sm text-muted-foreground md:inline">
|
||||
emma@example.com
|
||||
</div>
|
||||
</TableCell>
|
||||
<TableCell className="hidden sm:table-cell">
|
||||
Sale
|
||||
</TableCell>
|
||||
<TableCell className="hidden sm:table-cell">
|
||||
<Badge className="text-xs" variant="secondary">
|
||||
Fulfilled
|
||||
</Badge>
|
||||
</TableCell>
|
||||
<TableCell className="hidden md:table-cell">
|
||||
2023-06-26
|
||||
</TableCell>
|
||||
<TableCell className="text-right">$450.00</TableCell>
|
||||
</TableRow>
|
||||
<TableRow>
|
||||
<TableCell>
|
||||
<div className="font-medium">Liam Johnson</div>
|
||||
<div className="hidden text-sm text-muted-foreground md:inline">
|
||||
liam@example.com
|
||||
</div>
|
||||
</TableCell>
|
||||
<TableCell className="hidden sm:table-cell">
|
||||
Sale
|
||||
</TableCell>
|
||||
<TableCell className="hidden sm:table-cell">
|
||||
<Badge className="text-xs" variant="secondary">
|
||||
Fulfilled
|
||||
</Badge>
|
||||
</TableCell>
|
||||
<TableCell className="hidden md:table-cell">
|
||||
2023-06-23
|
||||
</TableCell>
|
||||
<TableCell className="text-right">$250.00</TableCell>
|
||||
</TableRow>
|
||||
<TableRow>
|
||||
<TableCell>
|
||||
<div className="font-medium">Liam Johnson</div>
|
||||
<div className="hidden text-sm text-muted-foreground md:inline">
|
||||
liam@example.com
|
||||
</div>
|
||||
</TableCell>
|
||||
<TableCell className="hidden sm:table-cell">
|
||||
Sale
|
||||
</TableCell>
|
||||
<TableCell className="hidden sm:table-cell">
|
||||
<Badge className="text-xs" variant="secondary">
|
||||
Fulfilled
|
||||
</Badge>
|
||||
</TableCell>
|
||||
<TableCell className="hidden md:table-cell">
|
||||
2023-06-23
|
||||
</TableCell>
|
||||
<TableCell className="text-right">$250.00</TableCell>
|
||||
</TableRow>
|
||||
<TableRow>
|
||||
<TableCell>
|
||||
<div className="font-medium">Olivia Smith</div>
|
||||
<div className="hidden text-sm text-muted-foreground md:inline">
|
||||
olivia@example.com
|
||||
</div>
|
||||
</TableCell>
|
||||
<TableCell className="hidden sm:table-cell">
|
||||
Refund
|
||||
</TableCell>
|
||||
<TableCell className="hidden sm:table-cell">
|
||||
<Badge className="text-xs" variant="outline">
|
||||
Declined
|
||||
</Badge>
|
||||
</TableCell>
|
||||
<TableCell className="hidden md:table-cell">
|
||||
2023-06-24
|
||||
</TableCell>
|
||||
<TableCell className="text-right">$150.00</TableCell>
|
||||
</TableRow>
|
||||
<TableRow>
|
||||
<TableCell>
|
||||
<div className="font-medium">Emma Brown</div>
|
||||
<div className="hidden text-sm text-muted-foreground md:inline">
|
||||
emma@example.com
|
||||
</div>
|
||||
</TableCell>
|
||||
<TableCell className="hidden sm:table-cell">
|
||||
Sale
|
||||
</TableCell>
|
||||
<TableCell className="hidden sm:table-cell">
|
||||
<Badge className="text-xs" variant="secondary">
|
||||
Fulfilled
|
||||
</Badge>
|
||||
</TableCell>
|
||||
<TableCell className="hidden md:table-cell">
|
||||
2023-06-26
|
||||
</TableCell>
|
||||
<TableCell className="text-right">$450.00</TableCell>
|
||||
</TableRow>
|
||||
</TableBody>
|
||||
</Table>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</TabsContent>
|
||||
</Tabs>
|
||||
</div>
|
||||
<div>
|
||||
<Card className="overflow-hidden" x-chunk="dashboard-05-chunk-4">
|
||||
<CardHeader className="flex flex-row items-start bg-muted/50">
|
||||
<div className="grid gap-0.5">
|
||||
<CardTitle className="group flex items-center gap-2 text-lg">
|
||||
Order Oe31b70H
|
||||
<Button
|
||||
size="icon"
|
||||
variant="outline"
|
||||
className="h-6 w-6 opacity-0 transition-opacity group-hover:opacity-100"
|
||||
>
|
||||
<Copy className="h-3 w-3" />
|
||||
<span className="sr-only">Copy Order ID</span>
|
||||
</Button>
|
||||
</CardTitle>
|
||||
<CardDescription>Date: November 23, 2023</CardDescription>
|
||||
</div>
|
||||
<div className="ml-auto flex items-center gap-1">
|
||||
<Button size="sm" variant="outline" className="h-8 gap-1">
|
||||
<Truck className="h-3.5 w-3.5" />
|
||||
<span className="lg:sr-only xl:not-sr-only xl:whitespace-nowrap">
|
||||
Track Order
|
||||
</span>
|
||||
</Button>
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button size="icon" variant="outline" className="h-8 w-8">
|
||||
<MoreVertical className="h-3.5 w-3.5" />
|
||||
<span className="sr-only">More</span>
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="end">
|
||||
<DropdownMenuItem>Edit</DropdownMenuItem>
|
||||
<DropdownMenuItem>Export</DropdownMenuItem>
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuItem>Trash</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</div>
|
||||
</CardHeader>
|
||||
<CardContent className="p-6 text-sm">
|
||||
<div className="grid gap-3">
|
||||
<div className="font-semibold">Order Details</div>
|
||||
<ul className="grid gap-3">
|
||||
<li className="flex items-center justify-between">
|
||||
<span className="text-muted-foreground">
|
||||
Glimmer Lamps x <span>2</span>
|
||||
</span>
|
||||
<span>$250.00</span>
|
||||
</li>
|
||||
<li className="flex items-center justify-between">
|
||||
<span className="text-muted-foreground">
|
||||
Aqua Filters x <span>1</span>
|
||||
</span>
|
||||
<span>$49.00</span>
|
||||
</li>
|
||||
</ul>
|
||||
<Separator className="my-2" />
|
||||
<ul className="grid gap-3">
|
||||
<li className="flex items-center justify-between">
|
||||
<span className="text-muted-foreground">Subtotal</span>
|
||||
<span>$299.00</span>
|
||||
</li>
|
||||
<li className="flex items-center justify-between">
|
||||
<span className="text-muted-foreground">Shipping</span>
|
||||
<span>$5.00</span>
|
||||
</li>
|
||||
<li className="flex items-center justify-between">
|
||||
<span className="text-muted-foreground">Tax</span>
|
||||
<span>$25.00</span>
|
||||
</li>
|
||||
<li className="flex items-center justify-between font-semibold">
|
||||
<span className="text-muted-foreground">Total</span>
|
||||
<span>$329.00</span>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
<Separator className="my-4" />
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div className="grid gap-3">
|
||||
<div className="font-semibold">Shipping Information</div>
|
||||
<address className="grid gap-0.5 not-italic text-muted-foreground">
|
||||
<span>Liam Johnson</span>
|
||||
<span>1234 Main St.</span>
|
||||
<span>Anytown, CA 12345</span>
|
||||
</address>
|
||||
</div>
|
||||
<div className="grid auto-rows-max gap-3">
|
||||
<div className="font-semibold">Billing Information</div>
|
||||
<div className="text-muted-foreground">
|
||||
Same as shipping address
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<Separator className="my-4" />
|
||||
<div className="grid gap-3">
|
||||
<div className="font-semibold">Customer Information</div>
|
||||
<dl className="grid gap-3">
|
||||
<div className="flex items-center justify-between">
|
||||
<dt className="text-muted-foreground">Customer</dt>
|
||||
<dd>Liam Johnson</dd>
|
||||
</div>
|
||||
<div className="flex items-center justify-between">
|
||||
<dt className="text-muted-foreground">Email</dt>
|
||||
<dd>
|
||||
<a href="mailto:">liam@acme.com</a>
|
||||
</dd>
|
||||
</div>
|
||||
<div className="flex items-center justify-between">
|
||||
<dt className="text-muted-foreground">Phone</dt>
|
||||
<dd>
|
||||
<a href="tel:">+1 234 567 890</a>
|
||||
</dd>
|
||||
</div>
|
||||
</dl>
|
||||
</div>
|
||||
<Separator className="my-4" />
|
||||
<div className="grid gap-3">
|
||||
<div className="font-semibold">Payment Information</div>
|
||||
<dl className="grid gap-3">
|
||||
<div className="flex items-center justify-between">
|
||||
<dt className="flex items-center gap-1 text-muted-foreground">
|
||||
<CreditCard className="h-4 w-4" />
|
||||
Visa
|
||||
</dt>
|
||||
<dd>**** **** **** 4532</dd>
|
||||
</div>
|
||||
</dl>
|
||||
</div>
|
||||
</CardContent>
|
||||
<CardFooter className="flex flex-row items-center border-t bg-muted/50 px-6 py-3">
|
||||
<div className="text-xs text-muted-foreground">
|
||||
Updated <time dateTime="2023-11-23">November 23, 2023</time>
|
||||
</div>
|
||||
<Pagination className="ml-auto mr-0 w-auto">
|
||||
<PaginationContent>
|
||||
<PaginationItem>
|
||||
<Button size="icon" variant="outline" className="h-6 w-6">
|
||||
<ChevronLeft className="h-3.5 w-3.5" />
|
||||
<span className="sr-only">Previous Order</span>
|
||||
</Button>
|
||||
</PaginationItem>
|
||||
<PaginationItem>
|
||||
<Button size="icon" variant="outline" className="h-6 w-6">
|
||||
<ChevronRight className="h-3.5 w-3.5" />
|
||||
<span className="sr-only">Next Order</span>
|
||||
</Button>
|
||||
</PaginationItem>
|
||||
</PaginationContent>
|
||||
</Pagination>
|
||||
</CardFooter>
|
||||
</Card>
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -1,644 +0,0 @@
|
||||
import Image from "next/image"
|
||||
import Link from "next/link"
|
||||
import {
|
||||
File,
|
||||
Home,
|
||||
LineChart,
|
||||
ListFilter,
|
||||
MoreHorizontal,
|
||||
Package,
|
||||
Package2,
|
||||
PanelLeft,
|
||||
PlusCircle,
|
||||
Search,
|
||||
Settings,
|
||||
ShoppingCart,
|
||||
Users2,
|
||||
} from "lucide-react"
|
||||
|
||||
import { Badge } from "@/registry/default/ui/badge"
|
||||
import {
|
||||
Breadcrumb,
|
||||
BreadcrumbItem,
|
||||
BreadcrumbLink,
|
||||
BreadcrumbList,
|
||||
BreadcrumbPage,
|
||||
BreadcrumbSeparator,
|
||||
} from "@/registry/default/ui/breadcrumb"
|
||||
import { Button } from "@/registry/default/ui/button"
|
||||
import {
|
||||
Card,
|
||||
CardContent,
|
||||
CardDescription,
|
||||
CardFooter,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
} from "@/registry/default/ui/card"
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuCheckboxItem,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuLabel,
|
||||
DropdownMenuSeparator,
|
||||
DropdownMenuTrigger,
|
||||
} from "@/registry/default/ui/dropdown-menu"
|
||||
import { Input } from "@/registry/default/ui/input"
|
||||
import { Sheet, SheetContent, SheetTrigger } from "@/registry/default/ui/sheet"
|
||||
import {
|
||||
Table,
|
||||
TableBody,
|
||||
TableCell,
|
||||
TableHead,
|
||||
TableHeader,
|
||||
TableRow,
|
||||
} from "@/registry/default/ui/table"
|
||||
import {
|
||||
Tabs,
|
||||
TabsContent,
|
||||
TabsList,
|
||||
TabsTrigger,
|
||||
} from "@/registry/default/ui/tabs"
|
||||
import {
|
||||
Tooltip,
|
||||
TooltipContent,
|
||||
TooltipTrigger,
|
||||
} from "@/registry/default/ui/tooltip"
|
||||
|
||||
export const description =
|
||||
"An products dashboard with a sidebar navigation. The sidebar has icon navigation. The content area has a breadcrumb and search in the header. It displays a list of products in a table with actions."
|
||||
|
||||
export const iframeHeight = "938px"
|
||||
|
||||
export const containerClassName = "w-full h-full"
|
||||
|
||||
export default function Dashboard() {
|
||||
return (
|
||||
<div className="flex min-h-screen w-full flex-col bg-muted/40">
|
||||
<aside className="fixed inset-y-0 left-0 z-10 hidden w-14 flex-col border-r bg-background sm:flex">
|
||||
<nav className="flex flex-col items-center gap-4 px-2 sm:py-5">
|
||||
<Link
|
||||
href="#"
|
||||
className="group flex h-9 w-9 shrink-0 items-center justify-center gap-2 rounded-full bg-primary text-lg font-semibold text-primary-foreground md:h-8 md:w-8 md:text-base"
|
||||
>
|
||||
<Package2 className="h-4 w-4 transition-all group-hover:scale-110" />
|
||||
<span className="sr-only">Acme Inc</span>
|
||||
</Link>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Link
|
||||
href="#"
|
||||
className="flex h-9 w-9 items-center justify-center rounded-lg text-muted-foreground transition-colors hover:text-foreground md:h-8 md:w-8"
|
||||
>
|
||||
<Home className="h-5 w-5" />
|
||||
<span className="sr-only">Dashboard</span>
|
||||
</Link>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent side="right">Dashboard</TooltipContent>
|
||||
</Tooltip>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Link
|
||||
href="#"
|
||||
className="flex h-9 w-9 items-center justify-center rounded-lg bg-accent text-accent-foreground transition-colors hover:text-foreground md:h-8 md:w-8"
|
||||
>
|
||||
<ShoppingCart className="h-5 w-5" />
|
||||
<span className="sr-only">Orders</span>
|
||||
</Link>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent side="right">Orders</TooltipContent>
|
||||
</Tooltip>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Link
|
||||
href="#"
|
||||
className="flex h-9 w-9 items-center justify-center rounded-lg text-muted-foreground transition-colors hover:text-foreground md:h-8 md:w-8"
|
||||
>
|
||||
<Package className="h-5 w-5" />
|
||||
<span className="sr-only">Products</span>
|
||||
</Link>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent side="right">Products</TooltipContent>
|
||||
</Tooltip>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Link
|
||||
href="#"
|
||||
className="flex h-9 w-9 items-center justify-center rounded-lg text-muted-foreground transition-colors hover:text-foreground md:h-8 md:w-8"
|
||||
>
|
||||
<Users2 className="h-5 w-5" />
|
||||
<span className="sr-only">Customers</span>
|
||||
</Link>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent side="right">Customers</TooltipContent>
|
||||
</Tooltip>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Link
|
||||
href="#"
|
||||
className="flex h-9 w-9 items-center justify-center rounded-lg text-muted-foreground transition-colors hover:text-foreground md:h-8 md:w-8"
|
||||
>
|
||||
<LineChart className="h-5 w-5" />
|
||||
<span className="sr-only">Analytics</span>
|
||||
</Link>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent side="right">Analytics</TooltipContent>
|
||||
</Tooltip>
|
||||
</nav>
|
||||
<nav className="mt-auto flex flex-col items-center gap-4 px-2 sm:py-5">
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Link
|
||||
href="#"
|
||||
className="flex h-9 w-9 items-center justify-center rounded-lg text-muted-foreground transition-colors hover:text-foreground md:h-8 md:w-8"
|
||||
>
|
||||
<Settings className="h-5 w-5" />
|
||||
<span className="sr-only">Settings</span>
|
||||
</Link>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent side="right">Settings</TooltipContent>
|
||||
</Tooltip>
|
||||
</nav>
|
||||
</aside>
|
||||
<div className="flex flex-col sm:gap-4 sm:py-4 sm:pl-14">
|
||||
<header className="sticky top-0 z-30 flex h-14 items-center gap-4 border-b bg-background px-4 sm:static sm:h-auto sm:border-0 sm:bg-transparent sm:px-6">
|
||||
<Sheet>
|
||||
<SheetTrigger asChild>
|
||||
<Button size="icon" variant="outline" className="sm:hidden">
|
||||
<PanelLeft className="h-5 w-5" />
|
||||
<span className="sr-only">Toggle Menu</span>
|
||||
</Button>
|
||||
</SheetTrigger>
|
||||
<SheetContent side="left" className="sm:max-w-xs">
|
||||
<nav className="grid gap-6 text-lg font-medium">
|
||||
<Link
|
||||
href="#"
|
||||
className="group flex h-10 w-10 shrink-0 items-center justify-center gap-2 rounded-full bg-primary text-lg font-semibold text-primary-foreground md:text-base"
|
||||
>
|
||||
<Package2 className="h-5 w-5 transition-all group-hover:scale-110" />
|
||||
<span className="sr-only">Acme Inc</span>
|
||||
</Link>
|
||||
<Link
|
||||
href="#"
|
||||
className="flex items-center gap-4 px-2.5 text-muted-foreground hover:text-foreground"
|
||||
>
|
||||
<Home className="h-5 w-5" />
|
||||
Dashboard
|
||||
</Link>
|
||||
<Link
|
||||
href="#"
|
||||
className="flex items-center gap-4 px-2.5 text-muted-foreground hover:text-foreground"
|
||||
>
|
||||
<ShoppingCart className="h-5 w-5" />
|
||||
Orders
|
||||
</Link>
|
||||
<Link
|
||||
href="#"
|
||||
className="flex items-center gap-4 px-2.5 text-foreground"
|
||||
>
|
||||
<Package className="h-5 w-5" />
|
||||
Products
|
||||
</Link>
|
||||
<Link
|
||||
href="#"
|
||||
className="flex items-center gap-4 px-2.5 text-muted-foreground hover:text-foreground"
|
||||
>
|
||||
<Users2 className="h-5 w-5" />
|
||||
Customers
|
||||
</Link>
|
||||
<Link
|
||||
href="#"
|
||||
className="flex items-center gap-4 px-2.5 text-muted-foreground hover:text-foreground"
|
||||
>
|
||||
<LineChart className="h-5 w-5" />
|
||||
Settings
|
||||
</Link>
|
||||
</nav>
|
||||
</SheetContent>
|
||||
</Sheet>
|
||||
<Breadcrumb className="hidden md:flex">
|
||||
<BreadcrumbList>
|
||||
<BreadcrumbItem>
|
||||
<BreadcrumbLink asChild>
|
||||
<Link href="#">Dashboard</Link>
|
||||
</BreadcrumbLink>
|
||||
</BreadcrumbItem>
|
||||
<BreadcrumbSeparator />
|
||||
<BreadcrumbItem>
|
||||
<BreadcrumbLink asChild>
|
||||
<Link href="#">Products</Link>
|
||||
</BreadcrumbLink>
|
||||
</BreadcrumbItem>
|
||||
<BreadcrumbSeparator />
|
||||
<BreadcrumbItem>
|
||||
<BreadcrumbPage>All Products</BreadcrumbPage>
|
||||
</BreadcrumbItem>
|
||||
</BreadcrumbList>
|
||||
</Breadcrumb>
|
||||
<div className="relative ml-auto flex-1 md:grow-0">
|
||||
<Search className="absolute left-2.5 top-2.5 h-4 w-4 text-muted-foreground" />
|
||||
<Input
|
||||
type="search"
|
||||
placeholder="Search..."
|
||||
className="w-full rounded-lg bg-background pl-8 md:w-[200px] lg:w-[336px]"
|
||||
/>
|
||||
</div>
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="icon"
|
||||
className="overflow-hidden rounded-full"
|
||||
>
|
||||
<Image
|
||||
src="/placeholder-user.jpg"
|
||||
width={36}
|
||||
height={36}
|
||||
alt="Avatar"
|
||||
className="overflow-hidden rounded-full"
|
||||
loading="lazy"
|
||||
/>
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="end">
|
||||
<DropdownMenuLabel>My Account</DropdownMenuLabel>
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuItem>Settings</DropdownMenuItem>
|
||||
<DropdownMenuItem>Support</DropdownMenuItem>
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuItem>Logout</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</header>
|
||||
<main className="grid flex-1 items-start gap-4 p-4 sm:px-6 sm:py-0 md:gap-8">
|
||||
<Tabs defaultValue="all">
|
||||
<div className="flex items-center">
|
||||
<TabsList>
|
||||
<TabsTrigger value="all">All</TabsTrigger>
|
||||
<TabsTrigger value="active">Active</TabsTrigger>
|
||||
<TabsTrigger value="draft">Draft</TabsTrigger>
|
||||
<TabsTrigger value="archived" className="hidden sm:flex">
|
||||
Archived
|
||||
</TabsTrigger>
|
||||
</TabsList>
|
||||
<div className="ml-auto flex items-center gap-2">
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button variant="outline" size="sm" className="h-8 gap-1">
|
||||
<ListFilter className="h-3.5 w-3.5" />
|
||||
<span className="sr-only sm:not-sr-only sm:whitespace-nowrap">
|
||||
Filter
|
||||
</span>
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="end">
|
||||
<DropdownMenuLabel>Filter by</DropdownMenuLabel>
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuCheckboxItem checked>
|
||||
Active
|
||||
</DropdownMenuCheckboxItem>
|
||||
<DropdownMenuCheckboxItem>Draft</DropdownMenuCheckboxItem>
|
||||
<DropdownMenuCheckboxItem>
|
||||
Archived
|
||||
</DropdownMenuCheckboxItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
<Button size="sm" variant="outline" className="h-8 gap-1">
|
||||
<File className="h-3.5 w-3.5" />
|
||||
<span className="sr-only sm:not-sr-only sm:whitespace-nowrap">
|
||||
Export
|
||||
</span>
|
||||
</Button>
|
||||
<Button size="sm" className="h-8 gap-1">
|
||||
<PlusCircle className="h-3.5 w-3.5" />
|
||||
<span className="sr-only sm:not-sr-only sm:whitespace-nowrap">
|
||||
Add Product
|
||||
</span>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
<TabsContent value="all">
|
||||
<Card x-chunk="dashboard-06-chunk-0">
|
||||
<CardHeader>
|
||||
<CardTitle>Products</CardTitle>
|
||||
<CardDescription>
|
||||
Manage your products and view their sales performance.
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead className="hidden w-[100px] sm:table-cell">
|
||||
<span className="sr-only">Image</span>
|
||||
</TableHead>
|
||||
<TableHead>Name</TableHead>
|
||||
<TableHead>Status</TableHead>
|
||||
<TableHead className="hidden md:table-cell">
|
||||
Price
|
||||
</TableHead>
|
||||
<TableHead className="hidden md:table-cell">
|
||||
Total Sales
|
||||
</TableHead>
|
||||
<TableHead className="hidden md:table-cell">
|
||||
Created at
|
||||
</TableHead>
|
||||
<TableHead>
|
||||
<span className="sr-only">Actions</span>
|
||||
</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
<TableRow>
|
||||
<TableCell className="hidden sm:table-cell">
|
||||
<Image
|
||||
alt="Product image"
|
||||
className="aspect-square rounded-md object-cover"
|
||||
height="64"
|
||||
src="/placeholder.svg"
|
||||
width="64"
|
||||
loading="lazy"
|
||||
/>
|
||||
</TableCell>
|
||||
<TableCell className="font-medium">
|
||||
Laser Lemonade Machine
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<Badge variant="outline">Draft</Badge>
|
||||
</TableCell>
|
||||
<TableCell className="hidden md:table-cell">
|
||||
$499.99
|
||||
</TableCell>
|
||||
<TableCell className="hidden md:table-cell">
|
||||
25
|
||||
</TableCell>
|
||||
<TableCell className="hidden md:table-cell">
|
||||
2023-07-12 10:42 AM
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button
|
||||
aria-haspopup="true"
|
||||
size="icon"
|
||||
variant="ghost"
|
||||
>
|
||||
<MoreHorizontal className="h-4 w-4" />
|
||||
<span className="sr-only">Toggle menu</span>
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="end">
|
||||
<DropdownMenuLabel>Actions</DropdownMenuLabel>
|
||||
<DropdownMenuItem>Edit</DropdownMenuItem>
|
||||
<DropdownMenuItem>Delete</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
<TableRow>
|
||||
<TableCell className="hidden sm:table-cell">
|
||||
<Image
|
||||
alt="Product image"
|
||||
className="aspect-square rounded-md object-cover"
|
||||
height="64"
|
||||
src="/placeholder.svg"
|
||||
width="64"
|
||||
loading="lazy"
|
||||
/>
|
||||
</TableCell>
|
||||
<TableCell className="font-medium">
|
||||
Hypernova Headphones
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<Badge variant="outline">Active</Badge>
|
||||
</TableCell>
|
||||
<TableCell className="hidden md:table-cell">
|
||||
$129.99
|
||||
</TableCell>
|
||||
<TableCell className="hidden md:table-cell">
|
||||
100
|
||||
</TableCell>
|
||||
<TableCell className="hidden md:table-cell">
|
||||
2023-10-18 03:21 PM
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button
|
||||
aria-haspopup="true"
|
||||
size="icon"
|
||||
variant="ghost"
|
||||
>
|
||||
<MoreHorizontal className="h-4 w-4" />
|
||||
<span className="sr-only">Toggle menu</span>
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="end">
|
||||
<DropdownMenuLabel>Actions</DropdownMenuLabel>
|
||||
<DropdownMenuItem>Edit</DropdownMenuItem>
|
||||
<DropdownMenuItem>Delete</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
<TableRow>
|
||||
<TableCell className="hidden sm:table-cell">
|
||||
<Image
|
||||
alt="Product image"
|
||||
className="aspect-square rounded-md object-cover"
|
||||
height="64"
|
||||
src="/placeholder.svg"
|
||||
width="64"
|
||||
loading="lazy"
|
||||
/>
|
||||
</TableCell>
|
||||
<TableCell className="font-medium">
|
||||
AeroGlow Desk Lamp
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<Badge variant="outline">Active</Badge>
|
||||
</TableCell>
|
||||
<TableCell className="hidden md:table-cell">
|
||||
$39.99
|
||||
</TableCell>
|
||||
<TableCell className="hidden md:table-cell">
|
||||
50
|
||||
</TableCell>
|
||||
<TableCell className="hidden md:table-cell">
|
||||
2023-11-29 08:15 AM
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button
|
||||
aria-haspopup="true"
|
||||
size="icon"
|
||||
variant="ghost"
|
||||
>
|
||||
<MoreHorizontal className="h-4 w-4" />
|
||||
<span className="sr-only">Toggle menu</span>
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="end">
|
||||
<DropdownMenuLabel>Actions</DropdownMenuLabel>
|
||||
<DropdownMenuItem>Edit</DropdownMenuItem>
|
||||
<DropdownMenuItem>Delete</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
<TableRow>
|
||||
<TableCell className="hidden sm:table-cell">
|
||||
<Image
|
||||
alt="Product image"
|
||||
className="aspect-square rounded-md object-cover"
|
||||
height="64"
|
||||
src="/placeholder.svg"
|
||||
width="64"
|
||||
loading="lazy"
|
||||
/>
|
||||
</TableCell>
|
||||
<TableCell className="font-medium">
|
||||
TechTonic Energy Drink
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<Badge variant="secondary">Draft</Badge>
|
||||
</TableCell>
|
||||
<TableCell className="hidden md:table-cell">
|
||||
$2.99
|
||||
</TableCell>
|
||||
<TableCell className="hidden md:table-cell">
|
||||
0
|
||||
</TableCell>
|
||||
<TableCell className="hidden md:table-cell">
|
||||
2023-12-25 11:59 PM
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button
|
||||
aria-haspopup="true"
|
||||
size="icon"
|
||||
variant="ghost"
|
||||
>
|
||||
<MoreHorizontal className="h-4 w-4" />
|
||||
<span className="sr-only">Toggle menu</span>
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="end">
|
||||
<DropdownMenuLabel>Actions</DropdownMenuLabel>
|
||||
<DropdownMenuItem>Edit</DropdownMenuItem>
|
||||
<DropdownMenuItem>Delete</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
<TableRow>
|
||||
<TableCell className="hidden sm:table-cell">
|
||||
<Image
|
||||
alt="Product image"
|
||||
className="aspect-square rounded-md object-cover"
|
||||
height="64"
|
||||
src="/placeholder.svg"
|
||||
width="64"
|
||||
loading="lazy"
|
||||
/>
|
||||
</TableCell>
|
||||
<TableCell className="font-medium">
|
||||
Gamer Gear Pro Controller
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<Badge variant="outline">Active</Badge>
|
||||
</TableCell>
|
||||
<TableCell className="hidden md:table-cell">
|
||||
$59.99
|
||||
</TableCell>
|
||||
<TableCell className="hidden md:table-cell">
|
||||
75
|
||||
</TableCell>
|
||||
<TableCell className="hidden md:table-cell">
|
||||
2024-01-01 12:00 AM
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button
|
||||
aria-haspopup="true"
|
||||
size="icon"
|
||||
variant="ghost"
|
||||
>
|
||||
<MoreHorizontal className="h-4 w-4" />
|
||||
<span className="sr-only">Toggle menu</span>
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="end">
|
||||
<DropdownMenuLabel>Actions</DropdownMenuLabel>
|
||||
<DropdownMenuItem>Edit</DropdownMenuItem>
|
||||
<DropdownMenuItem>Delete</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
<TableRow>
|
||||
<TableCell className="hidden sm:table-cell">
|
||||
<Image
|
||||
alt="Product image"
|
||||
className="aspect-square rounded-md object-cover"
|
||||
height="64"
|
||||
src="/placeholder.svg"
|
||||
width="64"
|
||||
loading="lazy"
|
||||
/>
|
||||
</TableCell>
|
||||
<TableCell className="font-medium">
|
||||
Luminous VR Headset
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<Badge variant="outline">Active</Badge>
|
||||
</TableCell>
|
||||
<TableCell className="hidden md:table-cell">
|
||||
$199.99
|
||||
</TableCell>
|
||||
<TableCell className="hidden md:table-cell">
|
||||
30
|
||||
</TableCell>
|
||||
<TableCell className="hidden md:table-cell">
|
||||
2024-02-14 02:14 PM
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button
|
||||
aria-haspopup="true"
|
||||
size="icon"
|
||||
variant="ghost"
|
||||
>
|
||||
<MoreHorizontal className="h-4 w-4" />
|
||||
<span className="sr-only">Toggle menu</span>
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="end">
|
||||
<DropdownMenuLabel>Actions</DropdownMenuLabel>
|
||||
<DropdownMenuItem>Edit</DropdownMenuItem>
|
||||
<DropdownMenuItem>Delete</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
</TableBody>
|
||||
</Table>
|
||||
</CardContent>
|
||||
<CardFooter>
|
||||
<div className="text-xs text-muted-foreground">
|
||||
Showing <strong>1-10</strong> of <strong>32</strong>{" "}
|
||||
products
|
||||
</div>
|
||||
</CardFooter>
|
||||
</Card>
|
||||
</TabsContent>
|
||||
</Tabs>
|
||||
</main>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -1,613 +0,0 @@
|
||||
import Image from "next/image"
|
||||
import Link from "next/link"
|
||||
import {
|
||||
ChevronLeft,
|
||||
Home,
|
||||
LineChart,
|
||||
Package,
|
||||
Package2,
|
||||
PanelLeft,
|
||||
PlusCircle,
|
||||
Search,
|
||||
Settings,
|
||||
ShoppingCart,
|
||||
Upload,
|
||||
Users2,
|
||||
} from "lucide-react"
|
||||
|
||||
import { Badge } from "@/registry/default/ui/badge"
|
||||
import {
|
||||
Breadcrumb,
|
||||
BreadcrumbItem,
|
||||
BreadcrumbLink,
|
||||
BreadcrumbList,
|
||||
BreadcrumbPage,
|
||||
BreadcrumbSeparator,
|
||||
} from "@/registry/default/ui/breadcrumb"
|
||||
import { Button } from "@/registry/default/ui/button"
|
||||
import {
|
||||
Card,
|
||||
CardContent,
|
||||
CardDescription,
|
||||
CardFooter,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
} from "@/registry/default/ui/card"
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuLabel,
|
||||
DropdownMenuSeparator,
|
||||
DropdownMenuTrigger,
|
||||
} from "@/registry/default/ui/dropdown-menu"
|
||||
import { Input } from "@/registry/default/ui/input"
|
||||
import { Label } from "@/registry/default/ui/label"
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from "@/registry/default/ui/select"
|
||||
import { Sheet, SheetContent, SheetTrigger } from "@/registry/default/ui/sheet"
|
||||
import {
|
||||
Table,
|
||||
TableBody,
|
||||
TableCell,
|
||||
TableHead,
|
||||
TableHeader,
|
||||
TableRow,
|
||||
} from "@/registry/default/ui/table"
|
||||
import { Textarea } from "@/registry/default/ui/textarea"
|
||||
import {
|
||||
ToggleGroup,
|
||||
ToggleGroupItem,
|
||||
} from "@/registry/default/ui/toggle-group"
|
||||
import {
|
||||
Tooltip,
|
||||
TooltipContent,
|
||||
TooltipTrigger,
|
||||
} from "@/registry/default/ui/tooltip"
|
||||
|
||||
export const description =
|
||||
"A product edit page. The product edit page has a form to edit the product details, stock, product category, product status, and product images. The product edit page has a sidebar navigation and a main content area. The main content area has a form to edit the product details, stock, product category, product status, and product images. The sidebar navigation has links to product details, stock, product category, product status, and product images."
|
||||
|
||||
export const iframeHeight = "1200px"
|
||||
|
||||
export const containerClassName = "w-full h-full"
|
||||
|
||||
export default function Dashboard() {
|
||||
return (
|
||||
<div className="flex min-h-screen w-full flex-col bg-muted/40">
|
||||
<aside className="fixed inset-y-0 left-0 z-10 hidden w-14 flex-col border-r bg-background sm:flex">
|
||||
<nav className="flex flex-col items-center gap-4 px-2 sm:py-5">
|
||||
<Link
|
||||
href="#"
|
||||
className="group flex h-9 w-9 shrink-0 items-center justify-center gap-2 rounded-full bg-primary text-lg font-semibold text-primary-foreground md:h-8 md:w-8 md:text-base"
|
||||
>
|
||||
<Package2 className="h-4 w-4 transition-all group-hover:scale-110" />
|
||||
<span className="sr-only">Acme Inc</span>
|
||||
</Link>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Link
|
||||
href="#"
|
||||
className="flex h-9 w-9 items-center justify-center rounded-lg text-muted-foreground transition-colors hover:text-foreground md:h-8 md:w-8"
|
||||
>
|
||||
<Home className="h-5 w-5" />
|
||||
<span className="sr-only">Dashboard</span>
|
||||
</Link>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent side="right">Dashboard</TooltipContent>
|
||||
</Tooltip>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Link
|
||||
href="#"
|
||||
className="flex h-9 w-9 items-center justify-center rounded-lg bg-accent text-accent-foreground transition-colors hover:text-foreground md:h-8 md:w-8"
|
||||
>
|
||||
<ShoppingCart className="h-5 w-5" />
|
||||
<span className="sr-only">Orders</span>
|
||||
</Link>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent side="right">Orders</TooltipContent>
|
||||
</Tooltip>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Link
|
||||
href="#"
|
||||
className="flex h-9 w-9 items-center justify-center rounded-lg text-muted-foreground transition-colors hover:text-foreground md:h-8 md:w-8"
|
||||
>
|
||||
<Package className="h-5 w-5" />
|
||||
<span className="sr-only">Products</span>
|
||||
</Link>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent side="right">Products</TooltipContent>
|
||||
</Tooltip>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Link
|
||||
href="#"
|
||||
className="flex h-9 w-9 items-center justify-center rounded-lg text-muted-foreground transition-colors hover:text-foreground md:h-8 md:w-8"
|
||||
>
|
||||
<Users2 className="h-5 w-5" />
|
||||
<span className="sr-only">Customers</span>
|
||||
</Link>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent side="right">Customers</TooltipContent>
|
||||
</Tooltip>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Link
|
||||
href="#"
|
||||
className="flex h-9 w-9 items-center justify-center rounded-lg text-muted-foreground transition-colors hover:text-foreground md:h-8 md:w-8"
|
||||
>
|
||||
<LineChart className="h-5 w-5" />
|
||||
<span className="sr-only">Analytics</span>
|
||||
</Link>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent side="right">Analytics</TooltipContent>
|
||||
</Tooltip>
|
||||
</nav>
|
||||
<nav className="mt-auto flex flex-col items-center gap-4 px-2 sm:py-5">
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Link
|
||||
href="#"
|
||||
className="flex h-9 w-9 items-center justify-center rounded-lg text-muted-foreground transition-colors hover:text-foreground md:h-8 md:w-8"
|
||||
>
|
||||
<Settings className="h-5 w-5" />
|
||||
<span className="sr-only">Settings</span>
|
||||
</Link>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent side="right">Settings</TooltipContent>
|
||||
</Tooltip>
|
||||
</nav>
|
||||
</aside>
|
||||
<div className="flex flex-col sm:gap-4 sm:py-4 sm:pl-14">
|
||||
<header className="sticky top-0 z-30 flex h-14 items-center gap-4 border-b bg-background px-4 sm:static sm:h-auto sm:border-0 sm:bg-transparent sm:px-6">
|
||||
<Sheet>
|
||||
<SheetTrigger asChild>
|
||||
<Button size="icon" variant="outline" className="sm:hidden">
|
||||
<PanelLeft className="h-5 w-5" />
|
||||
<span className="sr-only">Toggle Menu</span>
|
||||
</Button>
|
||||
</SheetTrigger>
|
||||
<SheetContent side="left" className="sm:max-w-xs">
|
||||
<nav className="grid gap-6 text-lg font-medium">
|
||||
<Link
|
||||
href="#"
|
||||
className="group flex h-10 w-10 shrink-0 items-center justify-center gap-2 rounded-full bg-primary text-lg font-semibold text-primary-foreground md:text-base"
|
||||
>
|
||||
<Package2 className="h-5 w-5 transition-all group-hover:scale-110" />
|
||||
<span className="sr-only">Acme Inc</span>
|
||||
</Link>
|
||||
<Link
|
||||
href="#"
|
||||
className="flex items-center gap-4 px-2.5 text-muted-foreground hover:text-foreground"
|
||||
>
|
||||
<Home className="h-5 w-5" />
|
||||
Dashboard
|
||||
</Link>
|
||||
<Link
|
||||
href="#"
|
||||
className="flex items-center gap-4 px-2.5 text-muted-foreground hover:text-foreground"
|
||||
>
|
||||
<ShoppingCart className="h-5 w-5" />
|
||||
Orders
|
||||
</Link>
|
||||
<Link
|
||||
href="#"
|
||||
className="flex items-center gap-4 px-2.5 text-foreground"
|
||||
>
|
||||
<Package className="h-5 w-5" />
|
||||
Products
|
||||
</Link>
|
||||
<Link
|
||||
href="#"
|
||||
className="flex items-center gap-4 px-2.5 text-muted-foreground hover:text-foreground"
|
||||
>
|
||||
<Users2 className="h-5 w-5" />
|
||||
Customers
|
||||
</Link>
|
||||
<Link
|
||||
href="#"
|
||||
className="flex items-center gap-4 px-2.5 text-muted-foreground hover:text-foreground"
|
||||
>
|
||||
<LineChart className="h-5 w-5" />
|
||||
Settings
|
||||
</Link>
|
||||
</nav>
|
||||
</SheetContent>
|
||||
</Sheet>
|
||||
<Breadcrumb className="hidden md:flex">
|
||||
<BreadcrumbList>
|
||||
<BreadcrumbItem>
|
||||
<BreadcrumbLink asChild>
|
||||
<Link href="#">Dashboard</Link>
|
||||
</BreadcrumbLink>
|
||||
</BreadcrumbItem>
|
||||
<BreadcrumbSeparator />
|
||||
<BreadcrumbItem>
|
||||
<BreadcrumbLink asChild>
|
||||
<Link href="#">Products</Link>
|
||||
</BreadcrumbLink>
|
||||
</BreadcrumbItem>
|
||||
<BreadcrumbSeparator />
|
||||
<BreadcrumbItem>
|
||||
<BreadcrumbPage>Edit Product</BreadcrumbPage>
|
||||
</BreadcrumbItem>
|
||||
</BreadcrumbList>
|
||||
</Breadcrumb>
|
||||
<div className="relative ml-auto flex-1 md:grow-0">
|
||||
<Search className="absolute left-2.5 top-2.5 h-4 w-4 text-muted-foreground" />
|
||||
<Input
|
||||
type="search"
|
||||
placeholder="Search..."
|
||||
className="w-full rounded-lg bg-background pl-8 md:w-[200px] lg:w-[336px]"
|
||||
/>
|
||||
</div>
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="icon"
|
||||
className="overflow-hidden rounded-full"
|
||||
>
|
||||
<Image
|
||||
src="/placeholder-user.jpg"
|
||||
width={36}
|
||||
height={36}
|
||||
loading="lazy"
|
||||
alt="Avatar"
|
||||
className="overflow-hidden rounded-full"
|
||||
/>
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="end">
|
||||
<DropdownMenuLabel>My Account</DropdownMenuLabel>
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuItem>Settings</DropdownMenuItem>
|
||||
<DropdownMenuItem>Support</DropdownMenuItem>
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuItem>Logout</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</header>
|
||||
<main className="grid flex-1 items-start gap-4 p-4 sm:px-6 sm:py-0 md:gap-8">
|
||||
<div className="mx-auto grid max-w-[59rem] flex-1 auto-rows-max gap-4">
|
||||
<div className="flex items-center gap-4">
|
||||
<Button variant="outline" size="icon" className="h-7 w-7">
|
||||
<ChevronLeft className="h-4 w-4" />
|
||||
<span className="sr-only">Back</span>
|
||||
</Button>
|
||||
<h1 className="flex-1 shrink-0 whitespace-nowrap text-xl font-semibold tracking-tight sm:grow-0">
|
||||
Pro Controller
|
||||
</h1>
|
||||
<Badge variant="outline" className="ml-auto sm:ml-0">
|
||||
In stock
|
||||
</Badge>
|
||||
<div className="hidden items-center gap-2 md:ml-auto md:flex">
|
||||
<Button variant="outline" size="sm">
|
||||
Discard
|
||||
</Button>
|
||||
<Button size="sm">Save Product</Button>
|
||||
</div>
|
||||
</div>
|
||||
<div className="grid gap-4 md:grid-cols-[1fr_250px] lg:grid-cols-3 lg:gap-8">
|
||||
<div className="grid auto-rows-max items-start gap-4 lg:col-span-2 lg:gap-8">
|
||||
<Card x-chunk="dashboard-07-chunk-0">
|
||||
<CardHeader>
|
||||
<CardTitle>Product Details</CardTitle>
|
||||
<CardDescription>
|
||||
Lipsum dolor sit amet, consectetur adipiscing elit
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="grid gap-6">
|
||||
<div className="grid gap-3">
|
||||
<Label htmlFor="name">Name</Label>
|
||||
<Input
|
||||
id="name"
|
||||
type="text"
|
||||
className="w-full"
|
||||
defaultValue="Gamer Gear Pro Controller"
|
||||
/>
|
||||
</div>
|
||||
<div className="grid gap-3">
|
||||
<Label htmlFor="description">Description</Label>
|
||||
<Textarea
|
||||
id="description"
|
||||
defaultValue="Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nullam auctor, nisl nec ultricies ultricies, nunc nisl ultricies nunc, nec ultricies nunc nisl nec nunc."
|
||||
className="min-h-32"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
<Card x-chunk="dashboard-07-chunk-1">
|
||||
<CardHeader>
|
||||
<CardTitle>Stock</CardTitle>
|
||||
<CardDescription>
|
||||
Lipsum dolor sit amet, consectetur adipiscing elit
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead className="w-[100px]">SKU</TableHead>
|
||||
<TableHead>Stock</TableHead>
|
||||
<TableHead>Price</TableHead>
|
||||
<TableHead className="w-[100px]">Size</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
<TableRow>
|
||||
<TableCell className="font-semibold">
|
||||
GGPC-001
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<Label htmlFor="stock-1" className="sr-only">
|
||||
Stock
|
||||
</Label>
|
||||
<Input
|
||||
id="stock-1"
|
||||
type="number"
|
||||
defaultValue="100"
|
||||
/>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<Label htmlFor="price-1" className="sr-only">
|
||||
Price
|
||||
</Label>
|
||||
<Input
|
||||
id="price-1"
|
||||
type="number"
|
||||
defaultValue="99.99"
|
||||
/>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<ToggleGroup
|
||||
type="single"
|
||||
defaultValue="s"
|
||||
variant="outline"
|
||||
>
|
||||
<ToggleGroupItem value="s">S</ToggleGroupItem>
|
||||
<ToggleGroupItem value="m">M</ToggleGroupItem>
|
||||
<ToggleGroupItem value="l">L</ToggleGroupItem>
|
||||
</ToggleGroup>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
<TableRow>
|
||||
<TableCell className="font-semibold">
|
||||
GGPC-002
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<Label htmlFor="stock-2" className="sr-only">
|
||||
Stock
|
||||
</Label>
|
||||
<Input
|
||||
id="stock-2"
|
||||
type="number"
|
||||
defaultValue="143"
|
||||
/>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<Label htmlFor="price-2" className="sr-only">
|
||||
Price
|
||||
</Label>
|
||||
<Input
|
||||
id="price-2"
|
||||
type="number"
|
||||
defaultValue="99.99"
|
||||
/>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<ToggleGroup
|
||||
type="single"
|
||||
defaultValue="m"
|
||||
variant="outline"
|
||||
>
|
||||
<ToggleGroupItem value="s">S</ToggleGroupItem>
|
||||
<ToggleGroupItem value="m">M</ToggleGroupItem>
|
||||
<ToggleGroupItem value="l">L</ToggleGroupItem>
|
||||
</ToggleGroup>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
<TableRow>
|
||||
<TableCell className="font-semibold">
|
||||
GGPC-003
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<Label htmlFor="stock-3" className="sr-only">
|
||||
Stock
|
||||
</Label>
|
||||
<Input
|
||||
id="stock-3"
|
||||
type="number"
|
||||
defaultValue="32"
|
||||
/>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<Label htmlFor="price-3" className="sr-only">
|
||||
Stock
|
||||
</Label>
|
||||
<Input
|
||||
id="price-3"
|
||||
type="number"
|
||||
defaultValue="99.99"
|
||||
/>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<ToggleGroup
|
||||
type="single"
|
||||
defaultValue="s"
|
||||
variant="outline"
|
||||
>
|
||||
<ToggleGroupItem value="s">S</ToggleGroupItem>
|
||||
<ToggleGroupItem value="m">M</ToggleGroupItem>
|
||||
<ToggleGroupItem value="l">L</ToggleGroupItem>
|
||||
</ToggleGroup>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
</TableBody>
|
||||
</Table>
|
||||
</CardContent>
|
||||
<CardFooter className="justify-center border-t p-4">
|
||||
<Button size="sm" variant="ghost" className="gap-1">
|
||||
<PlusCircle className="h-3.5 w-3.5" />
|
||||
Add Variant
|
||||
</Button>
|
||||
</CardFooter>
|
||||
</Card>
|
||||
<Card x-chunk="dashboard-07-chunk-2">
|
||||
<CardHeader>
|
||||
<CardTitle>Product Category</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="grid gap-6 sm:grid-cols-3">
|
||||
<div className="grid gap-3">
|
||||
<Label htmlFor="category">Category</Label>
|
||||
<Select>
|
||||
<SelectTrigger
|
||||
id="category"
|
||||
aria-label="Select category"
|
||||
>
|
||||
<SelectValue placeholder="Select category" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="clothing">Clothing</SelectItem>
|
||||
<SelectItem value="electronics">
|
||||
Electronics
|
||||
</SelectItem>
|
||||
<SelectItem value="accessories">
|
||||
Accessories
|
||||
</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
<div className="grid gap-3">
|
||||
<Label htmlFor="subcategory">
|
||||
Subcategory (optional)
|
||||
</Label>
|
||||
<Select>
|
||||
<SelectTrigger
|
||||
id="subcategory"
|
||||
aria-label="Select subcategory"
|
||||
>
|
||||
<SelectValue placeholder="Select subcategory" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="t-shirts">T-Shirts</SelectItem>
|
||||
<SelectItem value="hoodies">Hoodies</SelectItem>
|
||||
<SelectItem value="sweatshirts">
|
||||
Sweatshirts
|
||||
</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
<div className="grid auto-rows-max items-start gap-4 lg:gap-8">
|
||||
<Card x-chunk="dashboard-07-chunk-3">
|
||||
<CardHeader>
|
||||
<CardTitle>Product Status</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="grid gap-6">
|
||||
<div className="grid gap-3">
|
||||
<Label htmlFor="status">Status</Label>
|
||||
<Select>
|
||||
<SelectTrigger id="status" aria-label="Select status">
|
||||
<SelectValue placeholder="Select status" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="draft">Draft</SelectItem>
|
||||
<SelectItem value="published">Active</SelectItem>
|
||||
<SelectItem value="archived">Archived</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
<Card
|
||||
className="overflow-hidden" x-chunk="dashboard-07-chunk-4"
|
||||
>
|
||||
<CardHeader>
|
||||
<CardTitle>Product Images</CardTitle>
|
||||
<CardDescription>
|
||||
Lipsum dolor sit amet, consectetur adipiscing elit
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="grid gap-2">
|
||||
<Image
|
||||
alt="Product image"
|
||||
className="aspect-square w-full rounded-md object-cover"
|
||||
height="300"
|
||||
src="/placeholder.svg"
|
||||
width="300"
|
||||
loading="lazy"
|
||||
/>
|
||||
<div className="grid grid-cols-3 gap-2">
|
||||
<button>
|
||||
<Image
|
||||
alt="Product thumbnail image"
|
||||
className="aspect-square w-full rounded-md object-cover"
|
||||
height="84"
|
||||
src="/placeholder.svg"
|
||||
width="84"
|
||||
loading="lazy"
|
||||
/>
|
||||
</button>
|
||||
<button>
|
||||
<Image
|
||||
alt="Product thumbnail image"
|
||||
className="aspect-square w-full rounded-md object-cover"
|
||||
height="84"
|
||||
src="/placeholder.svg"
|
||||
width="84"
|
||||
loading="lazy"
|
||||
/>
|
||||
</button>
|
||||
<button className="flex aspect-square w-full items-center justify-center rounded-md border border-dashed">
|
||||
<Upload className="h-4 w-4 text-muted-foreground" />
|
||||
<span className="sr-only">Upload</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
<Card x-chunk="dashboard-07-chunk-5">
|
||||
<CardHeader>
|
||||
<CardTitle>Archive Product</CardTitle>
|
||||
<CardDescription>
|
||||
Lipsum dolor sit amet, consectetur adipiscing elit.
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div></div>
|
||||
<Button size="sm" variant="secondary">
|
||||
Archive Product
|
||||
</Button>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center justify-center gap-2 md:hidden">
|
||||
<Button variant="outline" size="sm">
|
||||
Discard
|
||||
</Button>
|
||||
<Button size="sm">Save Product</Button>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -1,91 +0,0 @@
|
||||
"use client"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
import { Button } from "@/registry/default/ui/button"
|
||||
import {
|
||||
Card,
|
||||
CardContent,
|
||||
CardDescription,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
} from "@/registry/default/ui/card"
|
||||
import { Input } from "@/registry/default/ui/input"
|
||||
import { Label } from "@/registry/default/ui/label"
|
||||
|
||||
export default function Page() {
|
||||
return (
|
||||
<div className="flex min-h-svh w-full items-center justify-center p-6 md:p-10">
|
||||
<div className="w-full max-w-sm">
|
||||
<LoginForm01 />
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export function LoginForm01({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentPropsWithoutRef<"div">) {
|
||||
const handleSubmit = async (e: React.FormEvent<HTMLFormElement>) => {
|
||||
e.preventDefault()
|
||||
const formData = new FormData(e.currentTarget)
|
||||
|
||||
// TODO: Add your authentication logic here
|
||||
console.log("Form submitted:", Object.fromEntries(formData))
|
||||
|
||||
// Example: await signIn(formData)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={cn("flex flex-col gap-6", className)} {...props}>
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="text-2xl">Login</CardTitle>
|
||||
<CardDescription>
|
||||
Enter your email below to login to your account
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<form onSubmit={handleSubmit}>
|
||||
<div className="flex flex-col gap-6">
|
||||
<div className="grid gap-2">
|
||||
<Label htmlFor="email">Email</Label>
|
||||
<Input
|
||||
id="email"
|
||||
name="email"
|
||||
type="email"
|
||||
placeholder="m@example.com"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
<div className="grid gap-2">
|
||||
<div className="flex items-center">
|
||||
<Label htmlFor="password">Password</Label>
|
||||
<a
|
||||
href="#"
|
||||
className="ml-auto inline-block text-sm underline-offset-4 hover:underline"
|
||||
>
|
||||
Forgot your password?
|
||||
</a>
|
||||
</div>
|
||||
<Input id="password" name="password" type="password" required />
|
||||
</div>
|
||||
<Button type="submit" className="w-full">
|
||||
Login
|
||||
</Button>
|
||||
<Button variant="outline" className="w-full">
|
||||
Login with Google
|
||||
</Button>
|
||||
</div>
|
||||
<div className="mt-4 text-center text-sm">
|
||||
Don't have an account?{" "}
|
||||
<a href="#" className="underline underline-offset-4">
|
||||
Sign up
|
||||
</a>
|
||||
</div>
|
||||
</form>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -1,91 +0,0 @@
|
||||
"use client"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
import { Button } from "@/registry/default/ui/button"
|
||||
import {
|
||||
Card,
|
||||
CardContent,
|
||||
CardDescription,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
} from "@/registry/default/ui/card"
|
||||
import { Input } from "@/registry/default/ui/input"
|
||||
import { Label } from "@/registry/default/ui/label"
|
||||
|
||||
export default function Page() {
|
||||
return (
|
||||
<div className="flex min-h-svh w-full items-center justify-center p-6 md:p-10">
|
||||
<div className="w-full max-w-sm">
|
||||
<LoginForm02 />
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export function LoginForm02({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentPropsWithoutRef<"div">) {
|
||||
const handleSubmit = async (e: React.FormEvent<HTMLFormElement>) => {
|
||||
e.preventDefault()
|
||||
const formData = new FormData(e.currentTarget)
|
||||
|
||||
// TODO: Add your authentication logic here
|
||||
console.log("Form submitted:", Object.fromEntries(formData))
|
||||
|
||||
// Example: await signIn(formData)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={cn("flex flex-col gap-6", className)} {...props}>
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="text-2xl">Login</CardTitle>
|
||||
<CardDescription>
|
||||
Enter your email below to login to your account
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<form onSubmit={handleSubmit}>
|
||||
<div className="flex flex-col gap-6">
|
||||
<div className="grid gap-2">
|
||||
<Label htmlFor="email">Email</Label>
|
||||
<Input
|
||||
id="email"
|
||||
name="email"
|
||||
type="email"
|
||||
placeholder="m@example.com"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
<div className="grid gap-2">
|
||||
<div className="flex items-center">
|
||||
<Label htmlFor="password">Password</Label>
|
||||
<a
|
||||
href="#"
|
||||
className="ml-auto inline-block text-sm underline-offset-4 hover:underline"
|
||||
>
|
||||
Forgot your password?
|
||||
</a>
|
||||
</div>
|
||||
<Input id="password" name="password" type="password" required />
|
||||
</div>
|
||||
<Button type="submit" className="w-full">
|
||||
Login
|
||||
</Button>
|
||||
<Button variant="outline" className="w-full">
|
||||
Login with Google
|
||||
</Button>
|
||||
</div>
|
||||
<div className="mt-4 text-center text-sm">
|
||||
Don't have an account?{" "}
|
||||
<a href="#" className="underline underline-offset-4">
|
||||
Sign up
|
||||
</a>
|
||||
</div>
|
||||
</form>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user