ui: one home for shadcn — the legacy copy leaves this monorepo

pkgs/ui was @hanzo/ui-shadcn 5.9.2, a second copy of a component set that was
already extracted to hanzoai/shadcn and published as @hanzo/shadcn. Keeping both
is the exact duplication the consolidation set out to end, and CONSOLIDATION.md
step 5 already called for it.

@hanzo/ui-shadcn is deprecated on npm (5.9.0 and 5.9.1) pointing at @hanzo/shadcn
and @hanzo/ui@8. The two dependents in this repo — the docs/registry app and
pkgs/commerce — resolved it as `workspace:@hanzo/ui-shadcn@^`; they now resolve
the published `npm:@hanzo/ui-shadcn@^5` instead, so nothing they import moves.

Co-authored-by: Hanzo Dev <dev@hanzo.ai>
This commit is contained in:
2026-07-28 18:55:36 -07:00
parent 2cc85a6e51
commit 5dbdb29435
885 changed files with 2 additions and 100957 deletions
+1 -1
View File
@@ -37,7 +37,7 @@
"@hanzo/docs-mdx": "14.3.0",
"@hanzo/docs-ui": "16.5.3",
"@hanzo/logo": "^1.0.5",
"@hanzo/ui": "workspace:@hanzo/ui-shadcn@^",
"@hanzo/ui": "npm:@hanzo/ui-shadcn@^5",
"@hookform/resolvers": "5.2.2",
"@monaco-editor/react": "^4.7.0",
"@radix-ui/react-accessible-icon": "^1.1.8",
+1 -1
View File
@@ -52,7 +52,7 @@
"zod": "^3.23.8"
},
"devDependencies": {
"@hanzo/ui": "workspace:@hanzo/ui-shadcn@^",
"@hanzo/ui": "npm:@hanzo/ui-shadcn@^5",
"@types/react": "19.2.14",
"@types/react-dom": "19.2.3",
"cross-fetch": "^4.1.0",
-1
View File
@@ -1 +0,0 @@
.npmrc
-59
View File
@@ -1,59 +0,0 @@
# Bases — framework-specific subpath exports
`@hanzo/ui` is a brand-neutral umbrella that re-exports framework-specific component bases. Consumers pick a base; the import path stays stable across stack swaps.
## Subpaths
| Subpath | Base | Source of truth | Status |
|---|---|---|---|
| `@hanzo/ui/primitives/bases/admin` | Hanzo GUI v7 admin chrome (Sidebar, TopBar, AdminApp, PageShell, primitives, IAM pages, data hooks) | `@hanzogui/admin` (`~/work/hanzo/gui/pkgs/ui-admin/`) | canonical |
| `@hanzo/ui/primitives/bases/gui` | Hanzo GUI v7 base primitives umbrella (XStack, YStack, Text, Input, Button, etc.) | `hanzogui` (`~/work/hanzo/gui/pkgs/ui/hanzogui/`) | canonical |
| `@hanzo/ui` (root) | shadcn/ui React + Radix + Tailwind component registry | `~/work/hanzo/ui/app/registry/default/ui/` | existing |
| `@hanzo/ui/primitives/bases/svelte` | Svelte adapter | not yet authored | placeholder, throws on import |
| `@hanzo/ui/primitives/bases/vue` | Vue adapter | not yet authored | placeholder, throws on import |
## Default
The canonical Hanzo stack for new product surfaces is **Hanzo GUI v7 via `@hanzo/ui/primitives/bases/admin`**. Use that for anything that needs the shared admin chrome (Sidebar/TopBar/PageShell, shared IAM pages, data hooks, brand-neutral primitives).
The shadcn+Radix root export (`@hanzo/ui`) is the existing 161-component shadcn fork — kept for surfaces that haven't migrated and for consumers who prefer Tailwind+Radix.
## Usage
```ts
import { AdminApp, Sidebar, TopBar, DataTable, Badge } from '@hanzo/ui/primitives/bases/admin'
import { XStack, YStack, Text, Button, Input } from '@hanzo/ui/primitives/bases/gui'
```
Tomorrow, if a Svelte port lands, the same consumers swap one import line:
```ts
import { AdminApp, Sidebar, TopBar, DataTable, Badge } from '@hanzo/ui/primitives/bases/svelte'
```
Component names stay identical across bases by contract.
## Adding a new base
1. Author the framework port at `~/work/hanzo/gui/pkgs/ui-admin-<framework>/` exporting the same component names with the same API.
2. Wire its package as a peer-dep in `@hanzo/ui` `package.json`.
3. Update the matching `src/primitives/bases/<framework>/index.ts` to re-export from it.
4. Update this doc's status table.
Throwing placeholder files for `svelte` and `vue` exist so callers see a clear runtime error when they import an unauthored base — no silent missing-component bugs.
## Why pass through `@hanzo/ui` instead of importing directly
1. **Stable import path** — when the canonical base swaps (Hanzo GUI v7 → v8, or React → Svelte for a given app), consumer imports don't move.
2. **One umbrella to depend on** — consumers add `@hanzo/ui` as one peer dep, and pick which bases they need at the subpath level. Tree-shaking does the rest.
The authoritative source files still live in `~/work/hanzo/gui/`. `@hanzo/ui` does not re-implement them.
## Brand layer
Each consumer applies its own brand at the theme/token layer:
- `@hanzo/tasks` (the SPA at `~/work/hanzo/gui/apps/admin-tasks/`) → Hanzo brand
- Downstream tenants ship their own brand via separate repos with separate registries
- `@hanzo/ui` bases ship brand-neutral — never include a `<HanzoMark/>` in the bases themselves.
See `~/work/hanzo/HANZO_BINARY.md` for the Go binary architecture (one binary + go:embed UI) that consumes these bases.
-73
View File
@@ -1,73 +0,0 @@
# Changelog
## 5.0.0 - ULTRA LEAN EDITION 🚀
### BREAKING CHANGES - READ THIS!
**CORE IS NOW 3.2KB!** Down from 15MB+ of bloat.
#### What Changed:
- Core bundle reduced by **99.98%** (3.2KB vs 15MB+)
- Main export (`@hanzo/ui`) now contains ONLY:
- `Button` - The essential component
- `Card` - Basic container
- `Input` - Form essential
- `Label` - Form essential
- `cn` - Styling utility
- ALL other components moved to separate imports
#### Migration:
**Before (v4.x):**
```js
// This used to import EVERYTHING (15MB+)
import { Button, Dialog, Select, Calendar } from '@hanzo/ui'
```
**Now (v5.0):**
```js
// Core - only 3.2KB!
import { Button, Card, Input, Label, cn } from '@hanzo/ui'
// Other components - import individually
import { Dialog } from '@hanzo/ui/dialog'
import { Select } from '@hanzo/ui/select'
import { Calendar } from '@hanzo/ui/calendar' // Requires react-day-picker
```
#### New Import Structure:
- `@hanzo/ui` - Core only (Button, Card, Input, Label, cn)
- `@hanzo/ui/accordion` - Accordion components
- `@hanzo/ui/alert` - Alert components
- `@hanzo/ui/dialog` - Dialog components
- `@hanzo/ui/select` - Select components
- `@hanzo/ui/calendar` - Calendar (needs react-day-picker)
- `@hanzo/ui/form` - Form components (needs react-hook-form)
- ...and 30+ more individual component imports
#### Dependencies:
- Core dependencies: Only essential Radix UI primitives
- Optional dependencies: Install only what you use
- Tree-shaking: Actually works now
- Bundle size: Microscopic core + pay for what you use
#### Performance:
- 95% faster initial load
- 99% smaller core bundle
- True code splitting
- No unused code in bundles
### Why This Change?
Because importing 15MB of JavaScript for a fucking button is insane.
### Quick Migration Script:
```bash
# Update imports in your codebase
find . -name "*.tsx" -o -name "*.jsx" | xargs sed -i "s/from '@hanzo\/ui'/from '@hanzo\/ui\/dialog'/g"
# Then manually fix the core imports
```
---
## Previous Versions
See GitHub releases for v4.x and earlier.
-169
View File
@@ -1,169 +0,0 @@
# @hanzo/ui - Multi-Framework Support
## 🎯 Overview
@hanzo/ui v4.7.0 introduces comprehensive multi-framework support, bringing shadcn/ui components to React, Vue, Svelte, and React Native with a unified API.
## 📦 Installation
```bash
npm install @hanzo/ui
# or
pnpm add @hanzo/ui
# or
bun add @hanzo/ui
```
## 🚀 Framework Usage
### React (Default)
```tsx
import { Button, Card, cn } from '@hanzo/ui'
// or explicitly
import { Button, Card, cn } from '@hanzo/ui/react'
function App() {
return (
<Card>
<Button variant="default">Click me</Button>
</Card>
)
}
```
### Vue
```vue
<template>
<Card>
<Button variant="default">Click me</Button>
</Card>
</template>
<script setup>
import { Button, Card } from '@hanzo/ui/vue'
</script>
```
### Svelte
```svelte
<script>
import { Button, Card } from '@hanzo/ui/svelte'
</script>
<Card>
<Button variant="default">Click me</Button>
</Card>
```
### React Native
```tsx
import { Button, Card } from '@hanzo/ui/react-native'
import { View } from 'react-native'
function App() {
return (
<View>
<Card>
<Button variant="default">Click me</Button>
</Card>
</View>
)
}
```
## 📊 Component Coverage
| Component | React | Vue | Svelte | React Native |
|-----------|-------|-----|---------|--------------|
| Accordion | ✅ | ✅ | ✅ | 🚧 |
| Alert | ✅ | ✅ | ✅ | ✅ |
| Avatar | ✅ | ✅ | ✅ | ✅ |
| Badge | ✅ | ✅ | ✅ | ✅ |
| Button | ✅ | ✅ | ✅ | ✅ |
| Card | ✅ | ✅ | ✅ | ✅ |
| Checkbox | ✅ | ✅ | ✅ | ✅ |
| Dialog | ✅ | ✅ | ✅ | ✅ |
| Dropdown | ✅ | ✅ | ✅ | 🚧 |
| Form | ✅ | ✅ | ✅ | 🚧 |
| Input | ✅ | ✅ | ✅ | ✅ |
| Label | ✅ | ✅ | ✅ | ✅ |
| Popover | ✅ | ✅ | 🚧 | ❌ |
| Progress | ✅ | ✅ | ✅ | ✅ |
| Radio | ✅ | ✅ | ✅ | ✅ |
| Select | ✅ | ✅ | ✅ | 🚧 |
| Separator | ✅ | ✅ | ✅ | ✅ |
| Sheet | ✅ | ✅ | 🚧 | ❌ |
| Skeleton | ✅ | ✅ | ✅ | ✅ |
| Slider | ✅ | ✅ | ✅ | 🚧 |
| Switch | ✅ | ✅ | ✅ | ✅ |
| Table | ✅ | ✅ | ✅ | ❌ |
| Tabs | ✅ | ✅ | ✅ | ✅ |
| Textarea | ✅ | ✅ | ✅ | ✅ |
| Toast | ✅ | ✅ | ✅ | ✅ |
| Toggle | ✅ | ✅ | ✅ | ✅ |
| Tooltip | ✅ | ✅ | 🚧 | ❌ |
Legend: ✅ Complete | 🚧 In Progress | ❌ Not Available
## 🛠 Core Utilities
All frameworks share common utilities:
```ts
import { cn } from '@hanzo/ui/core'
// Merge class names with Tailwind CSS support
const className = cn(
'base-class',
condition && 'conditional-class',
'another-class'
)
```
## 📄 Registry Support
@hanzo/ui includes a shadcn-compatible registry for component discovery:
```bash
# Using the CLI
npx @hanzo/ui add button
# Or with the registry directly
curl https://ui.hanzo.ai/r/button.json
```
## 🔄 Framework Conversion
Convert components between frameworks using our adapter tools:
```bash
# Convert React component to Vue
npx @hanzo/ui convert --from react --to vue button.tsx
# Convert to all frameworks
npx @hanzo/ui convert --from react --to all button.tsx
```
## 📈 Statistics
- **70+ Components** across all frameworks
- **4 Frameworks** supported
- **260+ Tools** in MCP registry
- **100% TypeScript** support
- **Tailwind CSS** powered
## 🤝 Credits
Built on top of amazing work by:
- [shadcn/ui](https://ui.shadcn.com) - Original React components
- [shadcn-vue](https://www.shadcn-vue.com) - Vue port
- [shadcn-svelte](https://www.shadcn-svelte.com) - Svelte port
- [react-native-reusables](https://rnr.dev) - React Native components
## 📝 License
BSD-3-Clause - See LICENSE file for details
-190
View File
@@ -1,190 +0,0 @@
╔══════════════════════════════════════════════════════════════════════════════╗
║ @HANZO/UI MULTI-FRAMEWORK TEST REPORT SUMMARY ║
║ October 5, 2025 ║
╚══════════════════════════════════════════════════════════════════════════════╝
┌─ OVERALL STATUS ─────────────────────────────────────────────────────────────┐
│ ✅ Infrastructure: PROPERLY CONFIGURED │
│ 📊 Coverage Range: 43% to 81% │
│ 🎯 Primary Gap: React Native (43%) │
│ 📦 Package Exports: ALL 4 FRAMEWORKS CONFIGURED │
└──────────────────────────────────────────────────────────────────────────────┘
┌─ FRAMEWORK COVERAGE ─────────────────────────────────────────────────────────┐
│ │
│ 🟢 VUE ████████████████████ 81% (57/70) ← BEST │
│ 🟡 SVELTE ██████████████ 70% (49/70) │
│ 🟡 REACT ████████████ 64% (45/70) ← DEFAULT │
│ 🔴 REACT NATIVE ████████ 43% (30/70) ← NEEDS WORK │
│ │
│ Universal (All 4): ██████████████████████████ 22 components │
│ Partial (Some): ████████████████████████████████████████ 48 components│
│ │
└──────────────────────────────────────────────────────────────────────────────┘
┌─ TEST RESULTS ───────────────────────────────────────────────────────────────┐
│ │
│ 📁 Directory Structure │
│ ✅ React - Structure OK │
│ ✅ Vue - Structure OK │
│ ✅ Svelte - Structure OK │
│ ✅ React Native - Structure OK │
│ │
│ 📦 Package Exports │
│ ✅ ./react - Configured (CJS/ESM/DTS) │
│ ✅ ./vue - Configured (ESM/DTS) │
│ ✅ ./svelte - Configured (ESM + svelte field) │
│ ✅ ./react-native - Configured (CJS/DTS) │
│ │
│ 🔨 Build Tests │
│ ✅ React - Builds successfully │
│ ⚠️ Vue - Missing components/index.ts │
│ ⚠️ Svelte - Missing components/index.ts │
│ ⚠️ React Native - Missing components/index.ts │
│ │
│ 📋 Registry │
│ ✅ registry.json - All frameworks listed │
│ ✅ tracker.json - Comprehensive coverage data │
│ │
└──────────────────────────────────────────────────────────────────────────────┘
┌─ UNIVERSAL COMPONENTS (22) ──────────────────────────────────────────────────┐
│ │
│ ✅ Available in ALL frameworks: │
│ │
│ accordion alert avatar badge button │
│ card checkbox collapsible dialog input │
│ label menubar popover progress select │
│ separator skeleton switch tabs textarea │
│ toggle tooltip │
│ │
└──────────────────────────────────────────────────────────────────────────────┘
┌─ CRITICAL GAPS ──────────────────────────────────────────────────────────────┐
│ │
│ React Native (40 missing): │
│ 🔴 HIGH: alert-dialog, radio-group, toggle-group, pagination │
│ 🔴 HIGH: drawer, sheet, sidebar, form │
│ 🟡 MED: calendar, carousel, command, navigation-menu │
│ 🟡 MED: chart variants, table, resizable │
│ │
│ React (25 missing): │
│ 🔴 HIGH: alert-dialog, context-menu, dropdown-menu │
│ 🔴 HIGH: radio-group, toggle-group, navigation-menu │
│ 🟡 MED: chart variants, combobox, input-otp │
│ 🟡 MED: auto-form, data-table, stepper │
│ │
│ Svelte (21 missing): │
│ 🟡 MED: chart variants, auto-form, combobox │
│ 🟡 MED: number-field, pin-input, tags-input │
│ 🟢 LOW: toast, toaster (already has sonner) │
│ │
│ Vue (13 missing) - LOWEST GAP: │
│ 🟢 LOW: data-table, input-otp, toaster │
│ 🟢 LOW: Framework-specific: aspect, context, dropdown, etc. │
│ │
└──────────────────────────────────────────────────────────────────────────────┘
┌─ PRIORITY RECOMMENDATIONS ───────────────────────────────────────────────────┐
│ │
│ 🚨 IMMEDIATE (15 min): │
│ 1. Create missing components/index.ts files for Vue/Svelte/RN │
│ 2. Fix framework builds to pass without errors │
│ │
│ 🎯 HIGH PRIORITY (2-4 hours): │
│ 1. React Native: Add 10 critical components (→ 60% coverage) │
│ - alert-dialog, radio-group, toggle-group, pagination │
│ - drawer, sheet, form, slider, sonner │
│ 2. React: Add Radix UI wrappers (→ 80% coverage) │
│ - alert-dialog, context-menu, dropdown-menu │
│ - radio-group, toggle-group, navigation-menu │
│ │
│ 📈 MEDIUM PRIORITY (4-8 hours): │
│ 1. Chart components across frameworks │
│ 2. Advanced form components (auto-form, combobox) │
│ 3. Data table implementations │
│ │
│ 🔄 ONGOING: │
│ 1. Maintain Vue at 81%+ (already excellent) │
│ 2. Bring Svelte to 80%+ (add 7 components) │
│ 3. Framework-specific optimizations │
│ │
└──────────────────────────────────────────────────────────────────────────────┘
┌─ BUILD CONFIGURATIONS ───────────────────────────────────────────────────────┐
│ │
│ 📄 tsup.config.minimal.ts (CURRENT DEFAULT) │
│ - Builds primitives-based components │
│ - Output: dist/*.{js,mjs,d.ts} │
│ - Fast, minimal bundle size │
│ │
│ 📄 tsup.config.frameworks.ts (MULTI-FRAMEWORK) │
│ - Separate builds per framework │
│ - Output: dist/{react,vue,svelte,react-native}/** │
│ - ⚠️ Needs component index files │
│ │
│ 📄 package.json exports │
│ - All 4 frameworks properly configured │
│ - Correct paths and formats │
│ - TypeScript definitions included │
│ │
└──────────────────────────────────────────────────────────────────────────────┘
┌─ NEXT STEPS ─────────────────────────────────────────────────────────────────┐
│ │
│ Day 1 (4 hours): │
│ 1. ✅ Create missing index files (15 min) │
│ 2. ✅ Add 5 React Native components (2 hours) │
│ 3. ✅ Add 5 React components (1.5 hours) │
│ 4. ✅ Test all builds (30 min) │
│ │
│ Day 2 (4 hours): │
│ 1. ✅ Add 5 more React Native components (2 hours) │
│ 2. ✅ Add remaining React components (1.5 hours) │
│ 3. ✅ Documentation updates (30 min) │
│ │
│ Result: ALL frameworks at 80%+ coverage! 🎉 │
│ │
└──────────────────────────────────────────────────────────────────────────────┘
┌─ KEY METRICS ────────────────────────────────────────────────────────────────┐
│ │
│ Total Components: 70 │
│ Universal (All): 22 (31%) │
│ Partial (Some): 48 (69%) │
│ │
│ Framework Stats: │
│ React: 45 implemented, 25 missing (64%) │
│ Vue: 57 implemented, 13 missing (81%) ← Best │
│ Svelte: 49 implemented, 21 missing (70%) │
│ React Native: 30 implemented, 40 missing (43%) ← Needs work │
│ │
│ Build Health: │
│ ✅ Structure: 4/4 frameworks │
│ ✅ Exports: 4/4 frameworks │
│ ⚠️ Builds: 1/4 frameworks (React only) │
│ ✅ Registry: Complete and accurate │
│ │
└──────────────────────────────────────────────────────────────────────────────┘
╔══════════════════════════════════════════════════════════════════════════════╗
║ CONCLUSION: Infrastructure is SOLID. Focus on component implementation. ║
║ Estimated time to 80% coverage across all frameworks: 1-2 days ║
╚══════════════════════════════════════════════════════════════════════════════╝
Files Generated:
✅ /Users/z/work/hanzo/ui/pkg/ui/FRAMEWORK_TEST_REPORT.md
✅ /Users/z/work/hanzo/ui/pkg/ui/test-frameworks.mjs
✅ /Users/z/work/hanzo/ui/pkg/ui/analyze-gaps.mjs
✅ /Users/z/work/hanzo/ui/pkg/ui/test-results-frameworks.json
Run Tests:
npm test # Run all framework tests
npm run test:frameworks # Run detailed framework tests
node test-frameworks.mjs # Run comprehensive validator
node analyze-gaps.mjs # Analyze component gaps
Build Frameworks:
npm run build # Default build (primitives)
npm run build:frameworks # Multi-framework build
npx tsup --config tsup.config.frameworks.ts
-73
View File
@@ -1,73 +0,0 @@
# Using @hanzo/ui with AI Assistants
This guide explains how to use @hanzo/ui with AI assistants through the Model Context Protocol (MCP).
## Getting Started
1. Install the package:
```bash
npm install @hanzo/ui
```
2. Start the MCP server:
```bash
npx @hanzo/ui registry:mcp
```
3. Configure your AI assistant to use the server.
## Available Commands
- **Initialize a new project**:
```bash
npx @hanzo/ui init --style=default
```
- **List available components**:
```bash
npx @hanzo/ui list
```
- **Add a component**:
```bash
npx @hanzo/ui add button
```
## Using with LLMs
AI Assistants like Claude or ChatGPT can help you explore and use the components. Just describe what you need, and the AI can guide you through:
- Finding the right component
- Installing and configuring it
- Using it in your project
Example prompt: "I need a dropdown menu component for my React project. Can you help me find and set it up using @hanzo/ui?"
## Registry Configuration
You can create a custom registry for your components, making them available through the same interface. To create a registry:
1. Set up the registry structure
2. Run the update-registry script
3. Host the registry files
4. Point to your custom registry:
```bash
npx @hanzo/ui registry:mcp --registry=https://your-registry-url.com/registry.json
```
## HTTP Mode
For web-based applications, you can run the MCP server in HTTP mode:
```bash
npx @hanzo/ui registry:mcp --http --port=3333
```
This exposes an HTTP endpoint at http://localhost:3333 that you can use to communicate with the MCP server.
## Learn More
For detailed documentation, see [README-MCP.md](./README-MCP.md) or visit the [Hanzo UI website](https://ui.hanzo.ai).
-175
View File
@@ -1,175 +0,0 @@
# Hanzo UI with MCP Support
This implementation provides Model Context Protocol (MCP) support for the Hanzo UI library, enabling AI assistants to interact with the component registry based on hanzo/ui.
## Quick Start
```bash
# Run with npx (recommended)
npx @hanzo/ui registry:mcp
# Or set a custom registry URL
npx @hanzo/ui registry:mcp --registry=https://ui.hanzo.ai/registry/registry.json
# Run in HTTP mode instead of stdio
npx @hanzo/ui registry:mcp --http --port=3333
```
## Setup and Usage
### Configure Your LLM Tool
To configure your AI assistant or LLM to use the Hanzo UI MCP server, add the following configuration:
```json
{
"name": "hanzo-ui",
"command": "npx @hanzo/ui registry:mcp"
}
```
### Available Tools
The MCP server provides the following tools for AI assistants:
1. `init` - Initialize a new project using @hanzo/ui components and styles
2. `list_components` - List all available components in the registry
3. `get_component` - Get detailed information about a specific component
4. `add_component` - Get instructions for adding a component to a project
5. `list_styles` - List all available styles in the registry
6. `search_registry` - Search the registry for components matching criteria
### Registry Configuration
The registry URL can be configured by setting the `REGISTRY_URL` environment variable:
```bash
export REGISTRY_URL="https://ui.hanzo.ai/registry/registry.json"
npx @hanzo/ui registry:mcp
```
Or by using the `--registry` command-line option:
```bash
npx @hanzo/ui registry:mcp --registry=https://ui.hanzo.ai/registry/registry.json
```
## HTTP Mode
You can run the MCP server in HTTP mode, which exposes the server over HTTP rather than stdio:
```bash
npx @hanzo/ui registry:mcp --http --port=3333
```
This allows you to access the MCP server directly from web applications or other HTTP clients.
## Developer Instructions
### Building from Source
```bash
# Clone the repository
git clone https://github.com/hanzoai/ui.git
cd ui/pkg/ui
# Install dependencies
npm install
# Build the package
npm run build
# Run the MCP server
node ./bin/cli.js registry:mcp
```
### Project Structure
- `/pkg/ui/mcp/`: MCP server implementation
- `/pkg/ui/registry/`: Registry schema and API
- `/pkg/ui/bin/`: CLI tools for running the MCP server
### Creating a Registry
The registry is compatible with hanzo/ui's registry format. To create a custom registry:
1. Create a `registry.json` file using the schema from `/pkg/ui/registry/schema.ts`
2. Build the registry using a build script
3. Host the registry files on a web server or CDN
4. Point the MCP server to your custom registry using the `--registry` option
## How It Works
The MCP server enables AI assistants to:
1. Discover and browse available components in your registry
2. Fetch detailed information about specific components
3. Provide instructions on adding components to projects
4. Initialize new projects with your component library
This makes it easy for users to interact with your UI library through conversational interfaces.
## Example Interactions
### Initialize a Project
```
Assistant: To create a new project with Hanzo UI components, you can run:
npx create-next-app@latest my-app
cd my-app
npx @hanzo/ui@latest init
```
### Add a Component
```
Assistant: To add the Button component to your project:
npx @hanzo/ui@latest add button
This will install the component and its dependencies.
```
### Search for Components
```
Assistant: I found several form-related components:
- form
- input
- checkbox
- select
- textarea
Which one would you like to add to your project?
```
## Technical Details
### Registry Schema
The registry schema is compatible with hanzo/ui and includes:
- Component metadata (name, description, type)
- Dependencies (npm packages)
- Registry dependencies (other components)
- Component files with source code
- Category and subcategory information
### MCP Integration
The MCP server implements the Model Context Protocol, enabling AI assistants to:
- Query the registry for components
- Get detailed information about components
- Provide installation instructions
- Generate example usage code
### Command-Line Interface
The CLI provides options for:
- Setting the registry URL
- Running in HTTP mode
- Specifying the port for HTTP mode
- Verbose logging
-229
View File
@@ -1,229 +0,0 @@
# @hanzo/ui
A comprehensive UI component library for Hanzo applications, built with React and TypeScript.
## Version 4.5.6
## Installation
```bash
npm install @hanzo/ui
# or
pnpm add @hanzo/ui
# or
yarn add @hanzo/ui
```
## Components
### Primitives
Core UI components based on Radix UI primitives:
- **Accordion** - Collapsible content panels
- **Alert** - Informative alert messages
- **AlertDialog** - Modal dialogs for important alerts
- **Avatar** - User avatar display
- **Badge** - Status and label badges
- **Breadcrumb** - Navigation breadcrumbs
- **Button** - Interactive buttons with variants
- **Calendar** - Date picker calendar
- **Card** - Container cards for content
- **Carousel** - Image/content carousel
- **Checkbox** - Checkbox input
- **Collapsible** - Collapsible content sections
- **Combobox** - Searchable select dropdown
- **Command** - Command palette component
- **ContextMenu** - Right-click context menus
- **Dialog** - Modal dialogs
- **Drawer** - Slide-out drawer panels
- **DropdownMenu** - Dropdown menu component
- **Form** - Form components with validation
- **HoverCard** - Hover-triggered info cards
- **Input** - Text input field
- **InputOTP** - One-time password input
- **Label** - Form labels
- **NavigationMenu** - Navigation menu bar
- **Popover** - Popover overlays
- **Progress** - Progress indicators
- **RadioGroup** - Radio button groups
- **ResizablePanel** - Resizable panel layouts
- **ScrollArea** - Custom scrollable areas
- **SearchInput** - Search input with icon
- **Select** - Select dropdown
- **Separator** - Visual separator line
- **Sheet** - Side sheet panels
- **Skeleton** - Loading skeleton screens
- **Slider** - Range slider input
- **Switch** - Toggle switch
- **Table** - Data tables
- **Tabs** - Tabbed interfaces
- **TextArea** - Multi-line text input
- **TextField** - Enhanced text input
- **Toast** - Toast notifications (via Sonner)
- **Toggle** - Toggle buttons
- **ToggleGroup** - Grouped toggle buttons
- **Tooltip** - Hover tooltips
- **VideoPlayer** - Video playback component
### Assets
Icon components and visual assets:
#### AI Provider Icons
- **AnthropicIcon** - Anthropic AI logo
- **OpenAIIcon** - OpenAI logo
- **GeminiIcon** - Google Gemini logo
- **DeepSeekIcon** - DeepSeek logo
- **MistralIcon** - Mistral AI logo
- **MetaIcon** - Meta AI logo
- **GroqIcon** - Groq logo
- **OllamaIcon** - Ollama logo
- **HanzoIcon** - Hanzo AI logo
- **TogetherAI** - Together AI logo
- **ExoIcon** - Exo logo
- **GrokIcon** - Grok logo
- **LmStudioIcon** - LM Studio logo
- **OpenRouterIcon** - OpenRouter logo
- **PerplexityIcon** - Perplexity logo
- **QwenIcon** - Qwen logo
- **AyaCohereIcon** - Aya/Cohere logo
#### Feature Icons
- **AIAgentIcon** - AI agent indicator
- **AisIcon** - AI services icon
- **ReactJsIcon** - React.js logo
- **ReasoningIcon** - AI reasoning indicator
- **ToolsIcon** - Tools/utilities icon
- **TracingIcon** - Tracing/monitoring icon
- **ScheduledTasksIcon** - Scheduled tasks icon
- **SendIcon** - Send/submit icon
#### File Type Icons
- **FileTypeIcon** - Dynamic file type icon based on extension
- **DirectoryTypeIcon** - Folder/directory icon
### Utilities
Helper functions and hooks:
- **cn()** - Class name utility (clsx + tailwind-merge)
- **markdown()** - Markdown to JSX converter
- **formatText()** - Text formatting utilities
- **useDebounce()** - Debounce hook
- **useMap()** - Map state management hook
- **formatDateToLocaleStringWithTime()** - Date formatting
- **getFileExt()** - File extension extraction
- **hexToRgb()** - Color conversion utilities
### Custom Components
Additional enhanced components:
- **ChatInput** - Chat message input
- **ChatInputArea** - Multi-line chat input
- **ChatSettingsIcon** - Chat settings icon
- **CopyToClipboardIcon** - Copy to clipboard button
- **DotsLoader** - Loading dots animation
- **FileList** - File list display
- **FileUploader** - File upload component
- **JsonForm** - JSON-based dynamic forms
- **MarkdownText** - Markdown renderer
- **PrettyJsonPrint** - Formatted JSON display
## Styling
The library uses Tailwind CSS for styling. Make sure your application includes Tailwind CSS configuration.
## Dependencies
Key peer dependencies:
- React 18.3.1+
- React DOM 18.3.1+
- @hookform/resolvers ^3.3.2
- react-hook-form 7.51.4
- lucide-react 0.456.0
- next-themes ^0.2.1
- embla-carousel ^8.1.6
## Usage Examples
### Basic Button
```tsx
import { Button } from '@hanzo/ui';
function App() {
return (
<Button variant="primary" onClick={() => console.log('clicked')}>
Click me
</Button>
);
}
```
### Alert Dialog
```tsx
import {
AlertDialog,
AlertDialogTrigger,
AlertDialogContent,
AlertDialogHeader,
AlertDialogTitle,
AlertDialogDescription,
AlertDialogFooter,
AlertDialogAction,
AlertDialogCancel
} from '@hanzo/ui';
function ConfirmDialog() {
return (
<AlertDialog>
<AlertDialogTrigger>
<Button>Open Dialog</Button>
</AlertDialogTrigger>
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle>Are you sure?</AlertDialogTitle>
<AlertDialogDescription>
This action cannot be undone.
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel>Cancel</AlertDialogCancel>
<AlertDialogAction>Continue</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
);
}
```
### AI Provider Icons
```tsx
import { OpenAIIcon, AnthropicIcon, GeminiIcon } from '@hanzo/ui/assets';
function AIProviders() {
return (
<div className="flex gap-4">
<OpenAIIcon className="h-6 w-6" />
<AnthropicIcon className="h-6 w-6" />
<GeminiIcon className="h-6 w-6" />
</div>
);
}
```
## License
BSD-3-Clause
## Author
Hanzo AI, Inc.
## Repository
https://github.com/hanzoai/react-sdk
-111
View File
@@ -1,111 +0,0 @@
#!/usr/bin/env node
import fs from 'fs';
import path from 'path';
import { fileURLToPath } from 'url';
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
const trackerPath = path.join(__dirname, 'frameworks', 'tracker.json');
const tracker = JSON.parse(fs.readFileSync(trackerPath, 'utf8'));
const frameworks = ['react', 'vue', 'svelte', 'react-native'];
const componentNames = Object.keys(tracker.components);
console.log('🔍 Multi-Framework Gap Analysis\n');
console.log('=' .repeat(80));
frameworks.forEach(fw => {
console.log(`\n📋 ${fw.toUpperCase()} - Missing Components`);
console.log('-'.repeat(80));
const missing = componentNames.filter(
comp => tracker.components[comp][fw]?.status === 'missing' || !tracker.components[comp][fw]
);
const implemented = componentNames.filter(
comp => tracker.components[comp][fw]?.status === 'complete'
);
console.log(`Coverage: ${tracker.coverage[fw]?.percentage || 0}% (${implemented.length}/${componentNames.length})`);
console.log(`Missing: ${missing.length} components\n`);
if (missing.length > 0) {
missing.forEach((comp, i) => {
const otherImplementations = frameworks
.filter(f => f !== fw && tracker.components[comp][f]?.status === 'complete')
.map(f => f);
const status = otherImplementations.length > 0
? `(✅ in ${otherImplementations.join(', ')})`
: '(❌ nowhere)';
console.log(` ${(i + 1).toString().padStart(2)}. ${comp.padEnd(25)} ${status}`);
});
} else {
console.log(' 🎉 No missing components!');
}
});
// Cross-framework consistency check
console.log('\n\n🔄 Cross-Framework Consistency Analysis');
console.log('=' .repeat(80));
const universalComponents = componentNames.filter(comp =>
frameworks.every(fw => tracker.components[comp][fw]?.status === 'complete')
);
const partialComponents = componentNames.filter(comp =>
frameworks.some(fw => tracker.components[comp][fw]?.status === 'complete') &&
!frameworks.every(fw => tracker.components[comp][fw]?.status === 'complete')
);
console.log(`\n✅ Universal Components (in all frameworks): ${universalComponents.length}`);
if (universalComponents.length > 0) {
console.log(universalComponents.map((c, i) => ` ${i + 1}. ${c}`).join('\n'));
}
console.log(`\n⚠️ Partial Components (missing in some frameworks): ${partialComponents.length}`);
if (partialComponents.length > 0) {
partialComponents.forEach((comp, i) => {
const missing = frameworks.filter(fw =>
tracker.components[comp][fw]?.status !== 'complete'
);
console.log(` ${i + 1}. ${comp.padEnd(25)} - Missing in: ${missing.join(', ')}`);
});
}
// Priority recommendations
console.log('\n\n🎯 Priority Recommendations');
console.log('=' .repeat(80));
console.log('\n1. React Native (43% coverage) - Highest Priority:');
const rnMissing = componentNames.filter(
comp => tracker.components[comp]['react-native']?.status !== 'complete'
);
console.log(` Missing ${rnMissing.length} components`);
console.log(' Top priorities:', rnMissing.slice(0, 10).join(', '));
console.log('\n2. React (64% coverage) - Medium Priority:');
const reactMissing = componentNames.filter(
comp => tracker.components[comp]['react']?.status !== 'complete'
);
console.log(` Missing ${reactMissing.length} components`);
console.log(' Top priorities:', reactMissing.slice(0, 10).join(', '));
console.log('\n3. Svelte (70% coverage) - Lower Priority:');
const svelteMissing = componentNames.filter(
comp => tracker.components[comp]['svelte']?.status !== 'complete'
);
console.log(` Missing ${svelteMissing.length} components`);
console.log(' Top priorities:', svelteMissing.slice(0, 10).join(', '));
console.log('\n4. Vue (81% coverage) - Lowest Priority:');
const vueMissing = componentNames.filter(
comp => tracker.components[comp]['vue']?.status !== 'complete'
);
console.log(` Missing ${vueMissing.length} components`);
console.log(' Top priorities:', vueMissing.slice(0, 10).join(', '));
console.log('\n' + '=' .repeat(80) + '\n');
-207
View File
@@ -1,207 +0,0 @@
// AI provider icon implementations
import React from 'react';
import { cn } from '../src/utils';
import { BrainIcon, ServerIcon, BotIcon, SparklesIcon, CpuIcon, ZapIcon, RocketIcon, ActivityIcon, NetworkIcon } from 'lucide-react';
// Simple placeholder icons for AI providers - replace with actual brand icons when available
export const AnthropicIcon = React.forwardRef<SVGSVGElement, React.SVGProps<SVGSVGElement>>(
({ className, ...props }, ref) => (
<BrainIcon ref={ref} className={cn("h-4 w-4", className)} {...props} />
)
);
AnthropicIcon.displayName = 'AnthropicIcon';
export const AyaCohereIcon = React.forwardRef<SVGSVGElement, React.SVGProps<SVGSVGElement>>(
({ className, ...props }, ref) => (
<SparklesIcon ref={ref} className={cn("h-4 w-4", className)} {...props} />
)
);
AyaCohereIcon.displayName = 'AyaCohereIcon';
export const DeepSeekIcon = React.forwardRef<SVGSVGElement, React.SVGProps<SVGSVGElement>>(
({ className, ...props }, ref) => (
<CpuIcon ref={ref} className={cn("h-4 w-4", className)} {...props} />
)
);
DeepSeekIcon.displayName = 'DeepSeekIcon';
export const ExoIcon = React.forwardRef<SVGSVGElement, React.SVGProps<SVGSVGElement>>(
({ className, ...props }, ref) => (
<ServerIcon ref={ref} className={cn("h-4 w-4", className)} {...props} />
)
);
ExoIcon.displayName = 'ExoIcon';
export const GeminiIcon = React.forwardRef<SVGSVGElement, React.SVGProps<SVGSVGElement>>(
({ className, ...props }, ref) => (
<SparklesIcon ref={ref} className={cn("h-4 w-4", className)} {...props} />
)
);
GeminiIcon.displayName = 'GeminiIcon';
export const GoogleIcon = React.forwardRef<SVGSVGElement, React.SVGProps<SVGSVGElement>>(
({ className, ...props }, ref) => (
<svg ref={ref} className={cn("h-4 w-4", className)} viewBox="0 0 24 24" fill="currentColor" {...props}>
<path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm0 18c-4.41 0-8-3.59-8-8s3.59-8 8-8 8 3.59 8 8-3.59 8-8 8z"/>
</svg>
)
);
GoogleIcon.displayName = 'GoogleIcon';
export const GrokIcon = React.forwardRef<SVGSVGElement, React.SVGProps<SVGSVGElement>>(
({ className, ...props }, ref) => (
<ZapIcon ref={ref} className={cn("h-4 w-4", className)} {...props} />
)
);
GrokIcon.displayName = 'GrokIcon';
export const GroqIcon = React.forwardRef<SVGSVGElement, React.SVGProps<SVGSVGElement>>(
({ className, ...props }, ref) => (
<RocketIcon ref={ref} className={cn("h-4 w-4", className)} {...props} />
)
);
GroqIcon.displayName = 'GroqIcon';
export const LmStudioIcon = React.forwardRef<SVGSVGElement, React.SVGProps<SVGSVGElement>>(
({ className, ...props }, ref) => (
<ServerIcon ref={ref} className={cn("h-4 w-4", className)} {...props} />
)
);
LmStudioIcon.displayName = 'LmStudioIcon';
export const MetaIcon = React.forwardRef<SVGSVGElement, React.SVGProps<SVGSVGElement>>(
({ className, ...props }, ref) => (
<svg ref={ref} className={cn("h-4 w-4", className)} viewBox="0 0 24 24" fill="currentColor" {...props}>
<path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm0 2c4.41 0 8 3.59 8 8s-3.59 8-8 8-8-3.59-8-8 3.59-8 8-8z"/>
</svg>
)
);
MetaIcon.displayName = 'MetaIcon';
export const MistralIcon = React.forwardRef<SVGSVGElement, React.SVGProps<SVGSVGElement>>(
({ className, ...props }, ref) => (
<ActivityIcon ref={ref} className={cn("h-4 w-4", className)} {...props} />
)
);
MistralIcon.displayName = 'MistralIcon';
export const OllamaIcon = React.forwardRef<SVGSVGElement, React.SVGProps<SVGSVGElement>>(
({ className, ...props }, ref) => (
<ServerIcon ref={ref} className={cn("h-4 w-4", className)} {...props} />
)
);
OllamaIcon.displayName = 'OllamaIcon';
export const OpenAIIcon = React.forwardRef<SVGSVGElement, React.SVGProps<SVGSVGElement>>(
({ className, ...props }, ref) => (
<BotIcon ref={ref} className={cn("h-4 w-4", className)} {...props} />
)
);
OpenAIIcon.displayName = 'OpenAIIcon';
export const OpenRouterIcon = React.forwardRef<SVGSVGElement, React.SVGProps<SVGSVGElement>>(
({ className, ...props }, ref) => (
<NetworkIcon ref={ref} className={cn("h-4 w-4", className)} {...props} />
)
);
OpenRouterIcon.displayName = 'OpenRouterIcon';
export const PerplexityIcon = React.forwardRef<SVGSVGElement, React.SVGProps<SVGSVGElement>>(
({ className, ...props }, ref) => (
<SparklesIcon ref={ref} className={cn("h-4 w-4", className)} {...props} />
)
);
PerplexityIcon.displayName = 'PerplexityIcon';
export const QwenIcon = React.forwardRef<SVGSVGElement, React.SVGProps<SVGSVGElement>>(
({ className, ...props }, ref) => (
<CpuIcon ref={ref} className={cn("h-4 w-4", className)} {...props} />
)
);
QwenIcon.displayName = 'QwenIcon';
export const HanzoIcon = React.forwardRef<SVGSVGElement, React.SVGProps<SVGSVGElement>>(
({ className, ...props }, ref) => (
<svg ref={ref} className={cn("h-4 w-4", className)} viewBox="0 0 24 24" fill="currentColor" {...props}>
<path d="M12 2L2 7v10c0 5.55 3.84 10.74 9 12 5.16-1.26 9-6.45 9-12V7l-10-5z"/>
</svg>
)
);
HanzoIcon.displayName = 'HanzoIcon';
export const TogetherAI = React.forwardRef<SVGSVGElement, React.SVGProps<SVGSVGElement>>(
({ className, ...props }, ref) => (
<NetworkIcon ref={ref} className={cn("h-4 w-4", className)} {...props} />
)
);
TogetherAI.displayName = 'TogetherAI';
// Generic AI icon for providers without specific icons
export const AisIcon = React.forwardRef<SVGSVGElement, React.SVGProps<SVGSVGElement>>(
({ className, ...props }, ref) => (
<BrainIcon ref={ref} className={cn("h-4 w-4", className)} {...props} />
)
);
AisIcon.displayName = 'AisIcon';
// Also export the ScheduledTasksIcon and SendIcon that might be needed
export const ScheduledTasksIcon = React.forwardRef<SVGSVGElement, React.SVGProps<SVGSVGElement>>(
({ className, ...props }, ref) => (
<ActivityIcon ref={ref} className={cn("h-4 w-4", className)} {...props} />
)
);
ScheduledTasksIcon.displayName = 'ScheduledTasksIcon';
export const SendIcon = React.forwardRef<SVGSVGElement, React.SVGProps<SVGSVGElement>>(
({ className, ...props }, ref) => (
<svg ref={ref} className={cn("h-4 w-4", className)} viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" {...props}>
<line x1="22" y1="2" x2="11" y2="13"></line>
<polygon points="22 2 15 22 11 13 2 9 22 2"></polygon>
</svg>
)
);
SendIcon.displayName = 'SendIcon';
export const ReactJsIcon = React.forwardRef<SVGSVGElement, React.SVGProps<SVGSVGElement>>(
({ className, ...props }, ref) => (
<svg ref={ref} className={cn("h-4 w-4", className)} viewBox="0 0 24 24" fill="currentColor" {...props}>
<circle cx="12" cy="12" r="10" fill="none" stroke="currentColor" strokeWidth="2"/>
<circle cx="12" cy="12" r="3" fill="currentColor"/>
<circle cx="12" cy="6" r="2" fill="currentColor"/>
<circle cx="18" cy="15" r="2" fill="currentColor"/>
<circle cx="6" cy="15" r="2" fill="currentColor"/>
<path d="M12 12 L12 6 M12 12 L18 15 M12 12 L6 15" stroke="currentColor" strokeWidth="1.5"/>
</svg>
)
);
ReactJsIcon.displayName = 'ReactJsIcon';
export const ReasoningIcon = React.forwardRef<SVGSVGElement, React.SVGProps<SVGSVGElement>>(
({ className, ...props }, ref) => (
<BrainIcon ref={ref} className={cn("h-4 w-4", className)} {...props} />
)
);
ReasoningIcon.displayName = 'ReasoningIcon';
export const ToolsIcon = React.forwardRef<SVGSVGElement, React.SVGProps<SVGSVGElement>>(
({ className, ...props }, ref) => (
<svg ref={ref} className={cn("h-4 w-4", className)} viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" {...props}>
<path d="M14 2l1.5 1.5v5.5L21 9v6l-5.5-.5V21L14 22l-2-7-2 7-1.5-1V14.5L3 15V9l5.5.5V4L10 2h4z"/>
</svg>
)
);
ToolsIcon.displayName = 'ToolsIcon';
export const AIAgentIcon = React.forwardRef<SVGSVGElement, React.SVGProps<SVGSVGElement>>(
({ className, ...props }, ref) => (
<BotIcon ref={ref} className={cn("h-4 w-4", className)} {...props} />
)
);
AIAgentIcon.displayName = 'AIAgentIcon';
export const TracingIcon = React.forwardRef<SVGSVGElement, React.SVGProps<SVGSVGElement>>(
({ className, ...props }, ref) => (
<ActivityIcon ref={ref} className={cn("h-4 w-4", className)} {...props} />
)
);
TracingIcon.displayName = 'TracingIcon';
-33
View File
@@ -1,33 +0,0 @@
import { cn } from '../src/utils';
export const EthereumIcon = ({ className }: { className?: string }) => (
<svg
className={cn('shrink-0', className)}
width="16"
height="16"
fill="none"
viewBox="0 0 24 24"
>
<path
fill="#fff"
d="M12 3v6.65l5.625 2.516zm0 0-5.625 9.166L12 9.651zm0 13.477v4.522l5.625-7.784zM12 21v-4.523l-5.625-3.262z"
/>
</svg>
);
export const USDCIcon = ({ className }: { className?: string }) => (
<svg
width="16"
className={cn('shrink-0', className)}
height="16"
fill="none"
viewBox="0 0 24 24"
>
<path
fill="#fff"
fillRule="evenodd"
d="M12 21c4.99 0 9-4.01 9-9s-4.01-9-9-9-9 4.01-9 9 4.01 9 9 9m2.475-7.578c0-1.31-.787-1.76-2.362-1.946-1.125-.152-1.35-.45-1.35-.978 0-.523.377-.86 1.125-.86.675 0 1.052.224 1.237.787.04.112.152.185.265.185h.596a.256.256 0 0 0 .264-.259v-.039c-.152-.827-.827-1.614-1.687-1.687v-.827c0-.152-.113-.265-.298-.298h-.495c-.152 0-.293.112-.332.298v.827c-1.125.151-1.873 1.012-1.873 1.951 0 1.238.748 1.722 2.323 1.913 1.052.185 1.39.41 1.39 1.012 0 .597-.53 1.013-1.238 1.013-.98 0-1.316-.416-1.429-.979-.034-.146-.146-.225-.259-.225h-.641a.256.256 0 0 0-.259.264v.04c.146.934.748 1.575 1.986 1.76v.833c0 .152.112.253.298.293h.54c.146 0 .248-.102.287-.293v-.833c1.125-.185 1.912-.939 1.912-1.952"
clipRule="evenodd"
/>
</svg>
);
-66
View File
@@ -1,66 +0,0 @@
// FileTypeIcon component
import React from 'react';
import { cn } from '../src/utils';
import { FileIcon, FolderIcon, ImageIcon, FileTextIcon, FileCodeIcon, FileArchiveIcon } from 'lucide-react';
export interface FileTypeIconProps extends React.SVGProps<SVGSVGElement> {
type?: string;
}
const getIconForType = (type?: string) => {
if (!type) return FileIcon;
const lowerType = type.toLowerCase();
// Image files
if (['png', 'jpg', 'jpeg', 'gif', 'svg', 'webp', 'ico', 'bmp'].includes(lowerType)) {
return ImageIcon;
}
// Document files
if (['pdf', 'doc', 'docx', 'txt', 'md', 'rtf'].includes(lowerType)) {
return FileTextIcon;
}
// Code files
if (['js', 'jsx', 'ts', 'tsx', 'html', 'css', 'scss', 'json', 'xml', 'py', 'java', 'c', 'cpp', 'rs', 'go'].includes(lowerType)) {
return FileCodeIcon;
}
// Archive files
if (['zip', 'rar', '7z', 'tar', 'gz', 'bz2', 'xz'].includes(lowerType)) {
return FileArchiveIcon;
}
return FileIcon;
};
export const FileTypeIcon = React.forwardRef<SVGSVGElement, FileTypeIconProps>(
({ type, className, ...props }, ref) => {
const Icon = getIconForType(type);
return (
<Icon
ref={ref}
className={cn("h-4 w-4", className)}
{...props}
/>
);
}
);
FileTypeIcon.displayName = 'FileTypeIcon';
export const DirectoryTypeIcon = React.forwardRef<SVGSVGElement, Omit<FileTypeIconProps, 'type'>>(
({ className, ...props }, ref) => {
return (
<FolderIcon
ref={ref}
className={cn("h-4 w-4", className)}
{...props}
/>
);
}
);
DirectoryTypeIcon.displayName = 'DirectoryTypeIcon';
-45
View File
@@ -1,45 +0,0 @@
// File icon map - placeholder for now
// TODO: Add actual file icons when SVG assets are available
export const fileIconMap: Record<string, string> = {
aep: '',
ai: '',
avi: '',
css: '',
csv: '',
dmg: '',
doc: '',
docx: '',
gif: '',
html: '',
jpeg: '',
jpg: '',
js: '',
json: '',
pdf: '',
png: '',
ppt: '',
pptx: '',
psd: '',
svg: '',
xls: '',
xlsx: '',
xml: '',
};
export const PaperClipIcon = ({ className }: { className?: string }) => (
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
strokeWidth={1.5}
stroke="currentColor"
className={className}
>
<path
strokeLinecap="round"
strokeLinejoin="round"
d="m18.375 12.739-7.693 7.693a4.5 4.5 0 0 1-6.364-6.364l10.94-10.94A3 3 0 1 1 19.5 7.372L8.552 18.32m.009-.01-.01.01m5.699-9.941-7.81 7.81a1.5 1.5 0 0 0 2.112 2.13"
/>
</svg>
);
File diff suppressed because one or more lines are too long
-9
View File
@@ -1,9 +0,0 @@
<svg viewBox="0 0 67 67" xmlns="http://www.w3.org/2000/svg">
<path d="M22.21 67V44.6369H0V67H22.21Z" fill="#ffffff"/>
<path d="M0 44.6369L22.21 46.8285V44.6369H0Z" fill="#DDDDDD"/>
<path d="M66.7038 22.3184H22.2534L0.0878906 44.6367H44.4634L66.7038 22.3184Z" fill="#ffffff"/>
<path d="M22.21 0H0V22.3184H22.21V0Z" fill="#ffffff"/>
<path d="M66.7198 0H44.5098V22.3184H66.7198V0Z" fill="#ffffff"/>
<path d="M66.6753 22.3185L44.5098 20.0822V22.3185H66.6753Z" fill="#DDDDDD"/>
<path d="M66.7198 67V44.6369H44.5098V67H66.7198Z" fill="#ffffff"/>
</svg>

Before

Width:  |  Height:  |  Size: 560 B

-15
View File
@@ -1,15 +0,0 @@
import React from 'react';
export const HanzoLogo = ({ className, ...props }: React.SVGProps<SVGSVGElement>) => (
<svg viewBox="0 0 67 67" xmlns="http://www.w3.org/2000/svg" className={className} {...props}>
<path d="M22.21 67V44.6369H0V67H22.21Z" fill="currentColor"/>
<path d="M0 44.6369L22.21 46.8285V44.6369H0Z" fill="currentColor" opacity="0.85"/>
<path d="M66.7038 22.3184H22.2534L0.0878906 44.6367H44.4634L66.7038 22.3184Z" fill="currentColor"/>
<path d="M22.21 0H0V22.3184H22.21V0Z" fill="currentColor"/>
<path d="M66.7198 0H44.5098V22.3184H66.7198V0Z" fill="currentColor"/>
<path d="M66.6753 22.3185L44.5098 20.0822V22.3185H66.6753Z" fill="currentColor" opacity="0.85"/>
<path d="M66.7198 67V44.6369H44.5098V67H66.7198Z" fill="currentColor"/>
</svg>
);
export const HanzoIcon = HanzoLogo; // Alias for compatibility
-122
View File
@@ -1,122 +0,0 @@
// Export all asset components and icons
// Note: Some files have overlapping exports, so we export them selectively
// File-related icons
export * from './file';
// File type icons (specific exports from file-type-icon to avoid conflicts with general)
export {
FileTypeIcon,
DirectoryTypeIcon,
type FileTypeIconProps
} from './file-type-icon';
// General icons (excluding duplicates: DirectoryTypeIcon, FileTypeIcon, HanzoIcon)
export {
HanzoLogoIcon,
HanzoLogoSoloIcon,
HanzoCombinationMarkIcon,
SendIcon,
ExportIcon,
QrIcon,
JobBubbleIcon,
ChatBubbleIcon,
AttachmentIcon,
DisconnectIcon,
AgentIcon,
AddAgentIcon,
InboxIcon,
ArchiveIcon,
ArchivedIcon,
ActiveIcon,
FilesIcon,
SharedFolderIcon,
AddNewFolderIcon,
UploadVectorResourceIcon,
GenerateDocIcon,
GenerateFromWebIcon,
CreateAIIcon,
FileEmptyStateIcon,
AiTasksIcon,
AISearchContentIcon,
BrowseSubscriptionIcon,
MySubscriptionsIcon,
PromptLibraryIcon,
NotificationIcon,
SheetFileIcon,
FormulaIcon,
SheetIcon,
ShortcutsIcon,
NetworkAgentIcon,
MCPIcon,
ToolsIcon,
ToolsDisabledIcon,
StoreIcon,
ScheduledTasksIcon,
AddCryptoWalletIcon,
CryptoWalletIcon,
ReactJsIcon,
AIAgentIcon,
AisIcon,
ToolAssetsIcon,
ScheduledTasksComingSoonIcon,
EmbeddingsGeneratedIcon,
UnknownLanguageIcon,
PythonIcon,
TypeScriptIcon,
MetadataIcon,
SaveIcon,
CloudModelIcon,
LocalModelIcon,
ArrowRightIcon,
ImportIcon,
WebSearchIcon,
WebSearchDisabledIcon,
ChatSettingsIcon,
PlusIcon,
ReasoningIcon,
HomeIcon,
TracingIcon,
DownloadIcon,
CategoryIcon,
PartyIcon,
// Exclude DirectoryTypeIcon and FileTypeIcon as they're exported above
// Exclude HanzoIcon as it's exported below from hanzo-logo
} from './general';
// Crypto icons
export * from './crypto';
// AI-related icons are already exported from general above
// Removed duplicate exports to avoid conflicts
// LLM provider icons (specific exports to avoid HanzoIcon conflict)
export {
MistralIcon,
GoogleIcon,
MetaIcon,
MicrosoftIcon,
OpenBMBIcon,
AnthropicIcon,
AzureIcon,
DeepSeekIcon,
GroqIcon,
LmStudioIcon,
OllamaIcon,
OpenAIIcon,
OpenRouterIcon,
PerplexityIcon,
QwenIcon,
ExoIcon,
GeminiIcon,
GrokIcon,
AyaCohereIcon,
// Exclude HanzoIcon as it's exported below from hanzo-logo
} from './llm-provider';
// Hanzo logo (primary export for HanzoIcon)
export {
HanzoLogo,
HanzoIcon, // Use this as the canonical HanzoIcon export
// HanzoLogoProps type not exported from hanzo-logo
} from './hanzo-logo';
-4
View File
@@ -1,4 +0,0 @@
export * from './general';
export * from './file';
export * from './crypto';
export * from './llm-provider';
File diff suppressed because one or more lines are too long
-100
View File
@@ -1,100 +0,0 @@
#!/usr/bin/env node
/**
* Main CLI entry point for @hanzo/ui
* Supports various commands including registry:mcp
*/
const { program } = require("commander");
// Define the version from the package.json
let version = "4.5.0";
try {
const packageJson = require("../package.json");
version = packageJson.version || version;
} catch (error) {
// Use default version if package.json can't be loaded
}
// Set up the program
program
.name("@hanzo/ui")
.description("Hanzo UI Component Library CLI")
.version(version);
// Add the MCP command (main command)
program
.command("mcp")
.description("Start the Hanzo UI MCP server for AI assistants")
.option(
"-r, --registry <url>",
"URL to the registry.json file",
process.env.REGISTRY_URL || "https://ui.hanzo.ai/registry/registry.json"
)
.option(
"-p, --port <port>",
"Port to listen on (for HTTP mode)",
"3333"
)
.option(
"--http",
"Run in HTTP mode instead of stdio mode",
false
)
.action(async (options) => {
// Delegate to the dedicated MCP binary
require("./mcp.js");
});
// Add the registry:mcp command (alias for compatibility)
program
.command("registry:mcp")
.description("Starts the registry MCP server (alias for 'mcp')")
.option(
"-r, --registry <url>",
"URL to the registry.json file",
process.env.REGISTRY_URL
)
.option(
"-p, --port <port>",
"Port to listen on (for HTTP mode)",
"3333"
)
.option(
"--http",
"Run in HTTP mode instead of stdio mode",
false
)
.action(async (options) => {
try {
// Set environment variables based on options
if (options.registry) {
process.env.REGISTRY_URL = options.registry;
}
// Set port if running in HTTP mode
if (options.http) {
process.env.MCP_HTTP_MODE = "true";
process.env.MCP_PORT = options.port;
}
// Show info about the server
console.error("Starting Hanzo UI MCP server...");
console.error(`Registry URL: ${process.env.REGISTRY_URL || "[Using default registry]"}`);
if (options.http) {
console.error(`Running in HTTP mode on port ${options.port}`);
} else {
console.error("Running in stdio mode");
}
// Load and run the MCP server script
require("./registry-mcp.js");
} catch (error) {
console.error("Error starting MCP server:", error);
process.exit(1);
}
});
// Parse command line arguments
program.parse();
-108
View File
@@ -1,108 +0,0 @@
#!/usr/bin/env node
/**
* Simple registry builder for @hanzo/ui
* Creates the necessary structure for the MCP registry
*/
const fs = require('fs');
const path = require('path');
// Configuration
const REGISTRY_DIR = path.resolve(__dirname, '../registry');
const PUBLIC_DIR = path.resolve(__dirname, '../public');
const OUTPUT_FILE = path.resolve(__dirname, '../registry.json');
const REGISTRY_STYLES = ['default', 'new-york'];
// Registry schema
const registrySchema = {
"$schema": "https://ui.hanzo.com/schema/registry.json",
"name": "hanzo",
"homepage": "https://ui.hanzo.ai",
"items": []
};
// Create the basic registry structure
function createRegistry() {
console.log('Creating registry structure...');
const items = [];
// Ensure registry directory exists
if (!fs.existsSync(REGISTRY_DIR)) {
fs.mkdirSync(REGISTRY_DIR, { recursive: true });
}
// Create style directories and add them to registry items
for (const style of REGISTRY_STYLES) {
const styleDir = path.join(REGISTRY_DIR, style);
if (!fs.existsSync(styleDir)) {
fs.mkdirSync(styleDir, { recursive: true });
// Create ui and block directories
fs.mkdirSync(path.join(styleDir, 'ui'), { recursive: true });
fs.mkdirSync(path.join(styleDir, 'block'), { recursive: true });
}
// Add style to registry
items.push({
name: style,
type: 'registry:style',
description: `The ${style} style for Hanzo UI components.`,
files: []
});
}
// Add primitive components as empty placeholders
const primitives = [
'accordion', 'alert', 'avatar', 'badge', 'button', 'card',
'checkbox', 'dialog', 'input', 'label', 'popover', 'select',
'table', 'tabs', 'toast'
];
for (const component of primitives) {
items.push({
name: component,
type: 'registry:component',
description: `A ${component} component for your UI.`,
files: [
{
path: `default/ui/${component}/${component}.tsx`,
type: 'registry:component'
}
]
});
}
// Update registry.json
registrySchema.items = items;
// Create output directory if it doesn't exist
const outputDir = path.dirname(OUTPUT_FILE);
if (!fs.existsSync(outputDir)) {
fs.mkdirSync(outputDir, { recursive: true });
}
// Write registry.json
fs.writeFileSync(OUTPUT_FILE, JSON.stringify(registrySchema, null, 2), 'utf8');
console.log(`Registry updated with ${items.length} items.`);
// Create public/r directory if it doesn't exist
const publicRDir = path.join(PUBLIC_DIR, 'r');
if (!fs.existsSync(publicRDir)) {
fs.mkdirSync(publicRDir, { recursive: true });
}
// Generate individual component JSON files
console.log('Generating individual component JSON files...');
for (const item of items) {
const componentFile = path.join(publicRDir, `${item.name}.json`);
fs.writeFileSync(componentFile, JSON.stringify(item, null, 2), 'utf8');
}
console.log(`Generated ${items.length} component JSON files.`);
console.log('Registry creation complete!');
}
// Run the registry creation
createRegistry();
-403
View File
@@ -1,403 +0,0 @@
#!/usr/bin/env node
/**
* Hanzo UI MCP Server
*
* This is the main entry point for the MCP server that can be run with:
* - npx @hanzo/ui mcp
* - npx @hanzo/ui@latest mcp
*
* The server provides AI assistants with tools to:
* - List and search components
* - Get component source code and demos
* - Access UI blocks and patterns
* - Generate usage examples
* - Create custom themes
*/
const { StdioServerTransport } = require("@modelcontextprotocol/sdk/server/stdio.js");
// Helper to load the server implementation
async function loadServer() {
try {
// Try loading from dist directory (published package)
const distPath = require.resolve("../dist/mcp/enhanced-server.js");
return require(distPath).default || require(distPath).server;
} catch (error) {
try {
// Fallback to original server if enhanced not available
const distPath = require.resolve("../dist/mcp/index.js");
return require(distPath).server;
} catch (innerError) {
try {
// Development mode - try to use ts-node if available
const tsNode = require("ts-node");
tsNode.register({
transpileOnly: true,
compilerOptions: {
module: "commonjs",
target: "es2020",
esModuleInterop: true,
allowSyntheticDefaultImports: true
}
});
// Now try to load the TypeScript file
try {
const enhancedPath = require.resolve("../mcp/enhanced-server.ts");
return require(enhancedPath).default || require(enhancedPath).server;
} catch (e) {
const srcPath = require.resolve("../mcp/index.ts");
return require(srcPath).server;
}
} catch (tsError) {
// If ts-node not available, create a basic server inline
console.error("Creating basic MCP server (ts-node not available for enhanced features)");
return createBasicServer();
}
}
}
}
// Create a basic MCP server if we can't load the TypeScript files
function createBasicServer() {
const { Server } = require("@modelcontextprotocol/sdk/server/index.js");
const { z } = require("zod");
const { zodToJsonSchema } = require("zod-to-json-schema");
const server = new Server(
{
name: "hanzo-ui",
version: "4.5.0",
},
{
capabilities: {
resources: {},
tools: {},
},
}
);
// Register basic tools
const { ListToolsRequestSchema, CallToolRequestSchema } = require("@modelcontextprotocol/sdk/types.js");
server.setRequestHandler(ListToolsRequestSchema, async () => {
return {
tools: [
{
name: "list_components",
description: "List all available Hanzo UI components",
inputSchema: zodToJsonSchema(z.object({})),
},
{
name: "get_component",
description: "Get information about a specific component",
inputSchema: zodToJsonSchema(z.object({
name: z.string().describe("Component name"),
})),
},
],
};
});
server.setRequestHandler(CallToolRequestSchema, async (request) => {
const args = request.params.arguments || {};
switch (request.params.name) {
case "list_components": {
return {
content: [{
type: "text",
text: `# Hanzo UI Components
The following components are available:
- accordion - Expandable accordion component
- alert - Alert message component
- avatar - User avatar display
- badge - Badge for labels and status
- button - Versatile button component
- card - Container card component
- checkbox - Checkbox input
- dialog - Modal dialog component
- drawer - Sliding drawer panel
- dropdown-menu - Dropdown menu component
- form - Form components and validation
- input - Text input field
- label - Form label component
- popover - Popover container
- radio-group - Radio button group
- select - Select dropdown
- separator - Visual separator
- sheet - Sheet modal component
- skeleton - Loading skeleton
- slider - Range slider input
- switch - Toggle switch
- table - Data table component
- tabs - Tab navigation
- textarea - Multiline text input
- toast - Toast notification
- tooltip - Tooltip component
Use 'get_component' to get more details about a specific component.`,
}],
};
}
case "get_component": {
const name = args.name;
return {
content: [{
type: "text",
text: `# ${name} Component
To install the ${name} component:
\`\`\`bash
npx @hanzo/ui@latest add ${name}
\`\`\`
This will add the component to your project and install any required dependencies.
For more information, visit https://ui.hanzo.ai/docs/components/${name}`,
}],
};
}
default:
throw new Error(`Unknown tool: ${request.params.name}`);
}
});
return server;
}
// Parse command line arguments
function parseArgs() {
const args = process.argv.slice(2);
const options = {
mode: 'stdio',
port: 3333,
registryUrl: process.env.REGISTRY_URL || 'https://ui.hanzo.ai/registry/registry.json',
help: false,
version: false,
};
for (let i = 0; i < args.length; i++) {
const arg = args[i];
if (arg === '--help' || arg === '-h') {
options.help = true;
} else if (arg === '--version' || arg === '-v') {
options.version = true;
} else if (arg === '--http') {
options.mode = 'http';
} else if (arg === '--port' && i + 1 < args.length) {
options.port = parseInt(args[++i], 10);
} else if (arg === '--registry' && i + 1 < args.length) {
options.registryUrl = args[++i];
}
}
return options;
}
// Display help message
function showHelp() {
console.log(`
Hanzo UI MCP Server
Usage:
npx @hanzo/ui mcp [options]
Options:
--help, -h Show this help message
--version, -v Show version information
--http Run in HTTP mode instead of stdio (experimental)
--port <port> Port for HTTP mode (default: 3333)
--registry <url> Custom registry URL
Examples:
# Run MCP server (stdio mode for AI clients)
npx @hanzo/ui mcp
# Run in HTTP mode for testing
npx @hanzo/ui mcp --http --port 3333
# Use custom registry
npx @hanzo/ui mcp --registry https://my-registry.com/registry.json
AI Client Configuration:
For Claude Desktop (.mcp.json):
{
"mcpServers": {
"hanzo-ui": {
"command": "npx",
"args": ["@hanzo/ui", "mcp"]
}
}
}
For Cursor (.cursor/mcp.json):
{
"mcpServers": {
"hanzo-ui": {
"command": "npx",
"args": ["@hanzo/ui", "mcp"]
}
}
}
For VS Code (.vscode/mcp.json):
{
"mcpServers": {
"hanzo-ui": {
"command": "npx",
"args": ["@hanzo/ui", "mcp"]
}
}
}
Available Tools:
- init Initialize a new project
- list_components List all available components
- get_component Get component details
- get_component_source Get component source code
- get_component_demo Get component demo code
- add_component Add component to project
- list_blocks List UI blocks/patterns
- get_block Get block details
- search_registry Search for components
- get_installation_guide Get installation guide
Available Resources:
- hanzo://components/list Complete component list
- hanzo://blocks/list UI blocks and patterns
- hanzo://installation/guide Installation guide
- hanzo://theming/guide Theming guide
Available Prompts:
- component_usage Generate usage examples
- build_page Build complete pages
- component_composition Create custom components
- accessibility_review Review accessibility
- theme_customization Generate custom themes
Learn more: https://ui.hanzo.ai/docs/mcp
`);
}
// Display version information
function showVersion() {
try {
const packageJson = require("../package.json");
console.log(`@hanzo/ui MCP Server v${packageJson.version}`);
} catch (error) {
console.log("@hanzo/ui MCP Server");
}
}
// Start HTTP server (experimental)
async function startHttpServer(server, port) {
try {
// Dynamically import HTTP transport
const { HttpServerTransport } = require("@modelcontextprotocol/sdk/server/http.js");
const transport = new HttpServerTransport({
port: port,
cors: {
origin: "*",
methods: ["GET", "POST"],
allowedHeaders: ["Content-Type"],
},
});
await server.connect(transport);
console.log(`Hanzo UI MCP HTTP server listening on port ${port}`);
console.log(`Access the server at http://localhost:${port}`);
console.log(`\nAvailable endpoints:`);
console.log(` GET http://localhost:${port}/health`);
console.log(` POST http://localhost:${port}/mcp/v1/list_tools`);
console.log(` POST http://localhost:${port}/mcp/v1/call_tool`);
console.log(` POST http://localhost:${port}/mcp/v1/list_resources`);
console.log(` POST http://localhost:${port}/mcp/v1/read_resource`);
console.log(` POST http://localhost:${port}/mcp/v1/list_prompts`);
console.log(` POST http://localhost:${port}/mcp/v1/get_prompt`);
console.log(`\nPress Ctrl+C to stop the server`);
return true;
} catch (error) {
console.error("Failed to start HTTP server:", error.message);
console.error("Falling back to stdio mode...");
return false;
}
}
// Main function
async function main() {
const options = parseArgs();
// Handle help and version flags
if (options.help) {
showHelp();
process.exit(0);
}
if (options.version) {
showVersion();
process.exit(0);
}
// Set registry URL in environment
process.env.REGISTRY_URL = options.registryUrl;
console.error(`Loading Hanzo UI MCP server...`);
console.error(`Registry: ${options.registryUrl}`);
try {
// Load the server implementation
const server = await loadServer();
if (options.mode === 'http') {
// Try HTTP mode
const httpStarted = await startHttpServer(server, options.port);
if (!httpStarted) {
// Fall back to stdio if HTTP fails
const transport = new StdioServerTransport();
await server.connect(transport);
console.error("MCP server running in stdio mode");
}
} else {
// Default stdio mode
const transport = new StdioServerTransport();
await server.connect(transport);
console.error("MCP server running in stdio mode");
console.error("Ready for AI client connections");
}
} catch (error) {
console.error("Error starting MCP server:", error);
console.error("\nTroubleshooting:");
console.error("1. Make sure @hanzo/ui is properly installed");
console.error("2. Try running: npm install @hanzo/ui@latest");
console.error("3. Check that all dependencies are installed");
process.exit(1);
}
}
// Handle process signals
process.on('SIGINT', () => {
console.error("\nShutting down MCP server...");
process.exit(0);
});
process.on('SIGTERM', () => {
console.error("\nShutting down MCP server...");
process.exit(0);
});
// Start the server
main().catch((error) => {
console.error("Fatal error:", error);
process.exit(1);
});
-15
View File
@@ -1,15 +0,0 @@
// @ts-check
#!/usr/bin/env node
const { program } = require("commander")
program
.name("npx @hanzo/ui registry:mcp")
.description("Run the Hanzo UI registry with MCP support")
.action(() => {
console.log("Starting Hanzo UI MCP server...")
// Execute the MCP server
require("../bin/registry-mcp.js")
})
program.parse()
-19
View File
@@ -1,19 +0,0 @@
#!/bin/bash
# Find the registry-mcp.js file in the node_modules directory
MCP_SCRIPT=$(find ./node_modules -path "*/@hanzo/ui/bin/registry-mcp.js" -type f | head -1)
if [ -z "$MCP_SCRIPT" ]; then
echo "Error: @hanzo/ui registry-mcp.js not found."
echo "Please make sure @hanzo/ui is installed or run 'npm install @hanzo/ui' first."
exit 1
fi
# Set the REGISTRY_URL environment variable if not already set
if [ -z "$REGISTRY_URL" ]; then
export REGISTRY_URL="https://ui.hanzo.ai/registry/registry.json"
echo "Using default registry URL: $REGISTRY_URL"
fi
# Execute the MCP script
node "$MCP_SCRIPT"
-100
View File
@@ -1,100 +0,0 @@
#!/usr/bin/env node
// This is the main entry point for the MCP server
// It should be runnable directly with npx @hanzo/ui registry:mcp
/**
* This script starts the MCP server for Hanzo UI
* It can be run with npx @hanzo/ui registry:mcp
*/
const { StdioServerTransport } = require("@modelcontextprotocol/sdk/server/stdio.js");
// Import the server dynamically based on where it's available
async function loadServer() {
try {
// Try loading from dist directory (published package)
return require("../dist/mcp/index.js").server;
} catch (error) {
try {
// Fallback to direct import (development)
return require("../mcp/index.js").server;
} catch (innerError) {
console.error("Failed to load the MCP server implementation:", innerError);
process.exit(1);
}
}
}
// Start HTTP server if requested
async function startHttpServer(server) {
try {
// Dynamically import the HTTP transport - we don't want to require
// this dependency if we're not using HTTP mode
const { HttpServerTransport } = require("@modelcontextprotocol/sdk/server/http.js");
// Get port from environment or use default
const port = process.env.MCP_PORT || 3333;
// Create HTTP transport
const transport = new HttpServerTransport({
port: parseInt(port, 10),
cors: {
origin: "*", // Allow any origin
methods: ["GET", "POST"],
allowedHeaders: ["Content-Type"],
},
});
// Connect the server to the transport
await server.connect(transport);
console.error(`MCP HTTP server listening on port ${port}`);
console.error(`You can access the server at http://localhost:${port}`);
} catch (error) {
console.error("Failed to start HTTP server:", error);
console.error("Falling back to STDIO mode...");
return false;
}
return true;
}
async function main() {
try {
console.error("Loading Hanzo UI MCP server...");
// Default registry URL if not set
if (!process.env.REGISTRY_URL) {
process.env.REGISTRY_URL = "https://ui.hanzo.ai/registry/registry.json";
console.error(`Using default registry URL: ${process.env.REGISTRY_URL}`);
console.error("You can set a custom registry URL with the REGISTRY_URL environment variable.");
}
// Load the server
const server = await loadServer();
// Check if we should use HTTP mode
const useHttp = process.env.MCP_HTTP_MODE === "true";
if (useHttp) {
// Try to start HTTP server, fall back to stdio if it fails
const httpStarted = await startHttpServer(server);
if (!httpStarted) {
// Fall back to stdio
const transport = new StdioServerTransport();
await server.connect(transport);
}
} else {
// Use stdio mode
const transport = new StdioServerTransport();
await server.connect(transport);
console.error("MCP server running in stdio mode");
}
} catch (error) {
console.error("Error starting MCP server:", error);
process.exit(1);
}
}
// Start the server
main();
-22
View File
@@ -1,22 +0,0 @@
#!/bin/bash
# Set the registry URL
export REGISTRY_URL=${REGISTRY_URL:-"https://ui.hanzo.ai/registry/registry.json"}
echo "Starting Hanzo UI MCP server with registry URL: $REGISTRY_URL"
echo "This server enables AI assistants to interact with the Hanzo UI component library."
echo "Use this as the URL for 'npx @hanzo/ui registry:mcp' in your MCP configuration."
echo ""
echo "Press Ctrl+C to stop the server."
# Find the CLI script
if [ -f "./bin/cli.js" ]; then
# In development directory structure
node ./bin/cli.js registry:mcp
elif [ -f "./node_modules/@hanzo/ui/bin/cli.js" ]; then
# Installed as dependency
node ./node_modules/@hanzo/ui/bin/cli.js registry:mcp
else
# Use npx as fallback
npx @hanzo/ui registry:mcp
fi
-52
View File
@@ -1,52 +0,0 @@
#!/bin/bash
# Test script for verifying the Hanzo UI MCP functionality
# Set a test registry URL
export REGISTRY_URL="https://ui.hanzo.com/registry/registry.json"
echo "Testing Hanzo UI MCP Server"
echo "============================"
echo ""
echo "This script will test the following functionalities:"
echo "1. Loading the server from NPX"
echo "2. Server startup with custom registry URL"
echo ""
# First test: Simple command check
echo "Test 1: Check if the NPX command is recognized"
# Just check if the command exists without running it
which npx > /dev/null 2>&1
if [ $? -eq 0 ]; then
echo "✅ NPX is available"
else
echo "❌ NPX is not installed"
exit 1
fi
# Second test: Server startup check
echo ""
echo "Test 2: Server startup (will exit automatically after 3 seconds)"
echo "Running: REGISTRY_URL=$REGISTRY_URL timeout 3 npx @hanzo/ui registry:mcp"
echo ""
echo "Expected output: You should see the server startup message"
echo ""
# Run with timeout to automatically kill after 3 seconds
# This works because the MCP server doesn't terminate on its own
timeout 3 npx @hanzo/ui registry:mcp 2>&1 | grep "Starting Hanzo UI MCP server"
if [ $? -eq 0 ]; then
echo ""
echo "✅ Server startup message detected"
else
echo ""
echo "❌ Server failed to start properly"
exit 1
fi
echo ""
echo "All tests completed successfully!"
echo ""
echo "The Hanzo UI MCP server is ready to be used in your MCP configuration with:"
echo "npx @hanzo/ui registry:mcp"
-196
View File
@@ -1,196 +0,0 @@
#!/usr/bin/env node
/**
* This script updates the registry.json file based on components
* found in the registry directory. It's similar to the approach used by hanzo/ui.
*/
const fs = require('fs');
const path = require('path');
const glob = require('glob');
// Configuration
const REGISTRY_DIR = path.resolve(__dirname, '../registry');
const PUBLIC_DIR = path.resolve(__dirname, '../public');
const OUTPUT_FILE = path.resolve(__dirname, '../registry.json');
const REGISTRY_STYLES = ['default', 'new-york']; // Add other styles as needed
// Schema for registry.json
const registrySchema = {
"$schema": "https://ui.hanzo.com/schema/registry.json",
"name": "hanzo",
"homepage": "https://ui.hanzo.ai",
"items": []
};
/**
* Main function to update the registry
*/
async function updateRegistry() {
try {
console.log('Updating registry.json...');
// Find all registry components
const items = [];
// Process each style directory
for (const style of REGISTRY_STYLES) {
const styleDir = path.join(REGISTRY_DIR, style);
// Skip if style directory doesn't exist
if (!fs.existsSync(styleDir)) {
console.log(`Style directory not found: ${style}`);
continue;
}
// Process UI components
const uiDir = path.join(styleDir, 'ui');
if (fs.existsSync(uiDir)) {
const uiComponents = glob.sync(`${uiDir}/**/!(*.test|*.spec).{ts,tsx,js,jsx}`);
for (const filePath of uiComponents) {
const relativePath = path.relative(REGISTRY_DIR, filePath);
const dirName = path.basename(path.dirname(filePath));
const fileName = path.basename(filePath);
// Skip index files and non-component files
if (fileName === 'index.ts' || fileName === 'index.tsx') continue;
// Component name is usually the directory name
const name = dirName;
// Check if component already exists in the registry
if (!items.find(item => item.name === name && item.type === 'registry:component')) {
const componentMeta = {
name,
type: 'registry:component',
description: `A ${name} component for your UI.`,
files: [
{
path: relativePath,
type: 'registry:component'
}
],
registryDependencies: []
};
// Try to parse dependencies from the file content
try {
const content = fs.readFileSync(filePath, 'utf8');
// Find import statements for other components
const dependencies = [];
const importRegex = /import.*from ['"]@\/components\/ui\/([^'"]+)['"]/g;
let match;
while ((match = importRegex.exec(content)) !== null) {
const dependency = match[1];
if (!dependencies.includes(dependency)) {
dependencies.push(dependency);
}
}
if (dependencies.length > 0) {
componentMeta.registryDependencies = dependencies;
}
} catch (error) {
console.warn(`Error parsing dependencies for ${name}:`, error.message);
}
items.push(componentMeta);
}
}
}
// Process other component types (blocks, etc.)
const blockDir = path.join(styleDir, 'block');
if (fs.existsSync(blockDir)) {
const blocks = glob.sync(`${blockDir}/**/!(*.test|*.spec).{ts,tsx,js,jsx}`);
for (const filePath of blocks) {
const relativePath = path.relative(REGISTRY_DIR, filePath);
const dirName = path.basename(path.dirname(filePath));
// Skip helper files
if (dirName === 'utils' || dirName === 'helpers') continue;
// Block name is usually the directory name
const name = dirName;
// Check if block already exists in the registry
if (!items.find(item => item.name === name && item.type === 'registry:block')) {
items.push({
name,
type: 'registry:block',
description: `A ${name} block for your UI.`,
files: [
{
path: relativePath,
type: 'registry:block'
}
]
});
}
}
}
}
// Add style definition
for (const style of REGISTRY_STYLES) {
items.push({
name: style,
type: 'registry:style',
description: `The ${style} style for Hanzo UI components.`,
files: []
});
}
// Update registry.json
registrySchema.items = items;
// Create output directory if it doesn't exist
const outputDir = path.dirname(OUTPUT_FILE);
if (!fs.existsSync(outputDir)) {
fs.mkdirSync(outputDir, { recursive: true });
}
// Write registry.json
fs.writeFileSync(OUTPUT_FILE, JSON.stringify(registrySchema, null, 2), 'utf8');
console.log(`Registry updated with ${items.length} items.`);
// Create public/r directory if it doesn't exist
const publicRDir = path.join(PUBLIC_DIR, 'r');
if (!fs.existsSync(publicRDir)) {
fs.mkdirSync(publicRDir, { recursive: true });
}
// Generate individual component JSON files
console.log('Generating individual component JSON files...');
for (const item of items) {
const componentFile = path.join(publicRDir, `${item.name}.json`);
// Add content to files if they exist
if (item.files && item.files.length > 0) {
for (const file of item.files) {
const filePath = path.join(REGISTRY_DIR, file.path);
if (fs.existsSync(filePath)) {
file.content = fs.readFileSync(filePath, 'utf8');
}
}
}
// Write component JSON file
fs.writeFileSync(componentFile, JSON.stringify(item, null, 2), 'utf8');
}
console.log(`Generated ${items.length} component JSON files.`);
console.log('Registry update complete!');
} catch (error) {
console.error('Error updating registry:', error);
process.exit(1);
}
}
// Run the update
updateRegistry();
-202
View File
@@ -1,202 +0,0 @@
# Block Tests - Quick Start Guide
## Test Status: ✅ ALL PASSING (53/53 tests)
This directory contains comprehensive validation tests for all migrated blocks in the @hanzo/ui package.
---
## Quick Test Commands
```bash
# Run all block tests
npm test -- blocks/__tests__/
# Run comprehensive validation only
npx jest blocks/__tests__/comprehensive-validation.test.js --verbose
# Run dependency checks only
npx jest blocks/__tests__/dependency-check.test.js --verbose
```
---
## Test Files
### 1. `comprehensive-validation.test.js` (29 tests)
Validates block structure, file counts, imports, and integrity.
**Tests:**
- ✅ Dashboard block structure (4 tests)
- ✅ Sidebar variants 1-16 (8 tests)
- ✅ Calendar files 1-32 (4 tests)
- ✅ Auth variants (login/signup/otp) (7 tests)
- ✅ Import paths (3 tests)
- ✅ File integrity (3 tests)
### 2. `dependency-check.test.js` (24 tests)
Validates all dependencies and import paths.
**Tests:**
- ✅ Dashboard dependencies (2 tests)
- ✅ Sidebar dependencies (2 tests)
- ✅ Calendar dependencies (3 tests)
- ✅ Auth dependencies (3 tests)
- ✅ Required primitives (10 tests)
- ✅ External dependencies (2 tests)
- ✅ Import consistency (2 tests)
### 3. `BLOCK_TEST_REPORT.md`
Detailed test report with findings and recommendations.
---
## What's Tested
### ✅ Block Structure
- All 64 blocks/components exist
- Correct directory structures
- Required files present (page.tsx, components/, etc.)
### ✅ Import Paths
- Local component imports (`./components`)
- Primitive imports (`../../primitives/`)
- All imports resolve correctly
### ✅ Dependencies
- All 10 required primitives available
- Component dependencies valid
- External packages importable
### ✅ File Integrity
- All TypeScript/React files valid
- Proper exports present
- Client directives where needed
---
## Block Inventory
### Dashboard (1 block)
- `dashboard-01/` - 11 components
### Sidebar (16 blocks)
- `sidebar-01/` through `sidebar-16/` - ~72 total files
### Calendar (32 files)
- `calendar-01.tsx` through `calendar-32.tsx`
### Auth (15 variants)
- `login/login-01/` through `login-05/` - 5 variants
- `signup/signup-01/` through `signup-05/` - 5 variants
- `otp/otp-01/` through `otp-05/` - 5 variants
**Total: 64 components/pages**
---
## Test Results Summary
```
Test Suites: 2 passed, 2 total
Tests: 53 passed, 53 total
Time: ~0.17s
```
### Breakdown by Category
- **Structure Validation:** 29/29 ✅
- **Dependency Validation:** 24/24 ✅
- **Total Coverage:** 100% ✅
---
## What's NOT Tested (Requires Runtime)
These require a browser/runtime environment:
- ⏳ Form submissions and validation
- ⏳ Calendar date selection interactions
- ⏳ Sidebar navigation clicks
- ⏳ User interaction flows
**Recommendation:** Set up Vitest + jsdom for runtime testing.
---
## Adding New Tests
When adding new blocks:
1. **Update comprehensive-validation.test.js:**
- Add block count to relevant section
- Add variant numbers if applicable
- Update total counts
2. **Update dependency-check.test.js:**
- Add new primitive dependencies
- Verify import paths
- Check component structure
3. **Run tests:**
```bash
npm test -- blocks/__tests__/
```
---
## Troubleshooting
### Tests fail with "Cannot find module"
- Check that primitives exist in `../../primitives/`
- Verify component imports use correct paths
- Ensure index files export correctly
### Tests fail with "ENOENT: no such file"
- Verify block directory structure
- Check file naming (page.tsx, components/)
- Ensure all variants exist
### Import path errors
- Blocks use relative imports: `../../primitives/`
- Components use local imports: `./components`
- No `@/` aliases in blocks
---
## CI/CD Integration
Add to your pipeline:
```yaml
# .github/workflows/test.yml
- name: Test Blocks
run: npm test -- blocks/__tests__/
```
---
## Related Documentation
- **BLOCK_TEST_REPORT.md** - Detailed test report
- **/blocks/dashboard/README.md** - Dashboard documentation
- **/blocks/sidebar/README.md** - Sidebar documentation
- **/blocks/calendar/README.md** - Calendar documentation
- **/blocks/auth/README.md** - Auth documentation
---
## Status Summary
| Category | Status | Count | Tests |
|----------|--------|-------|-------|
| Dashboard | ✅ Pass | 1 block | 4/4 |
| Sidebar | ✅ Pass | 16 blocks | 8/8 |
| Calendar | ✅ Pass | 32 files | 4/4 |
| Auth | ✅ Pass | 15 variants | 7/7 |
| Dependencies | ✅ Pass | 10 primitives | 24/24 |
| **Total** | **✅ Pass** | **64 components** | **53/53** |
---
**Last Updated:** 2025-10-05
**Test Coverage:** 100% (structural)
**Status:** ✅ READY FOR USE
@@ -1,275 +0,0 @@
/**
* Comprehensive Block Validation Tests
* Validates all migrated blocks structure and integrity
*/
const fs = require('fs');
const path = require('path');
const blocksDir = path.join(__dirname, '..');
describe('Migrated Blocks - Comprehensive Validation', () => {
describe('Dashboard Blocks (1 block, 11 components)', () => {
test('dashboard-01 exists with correct structure', () => {
const dashboard01 = path.join(blocksDir, 'dashboard/dashboard-01');
expect(fs.existsSync(dashboard01)).toBe(true);
expect(fs.existsSync(path.join(dashboard01, 'page.tsx'))).toBe(true);
expect(fs.existsSync(path.join(dashboard01, 'components'))).toBe(true);
expect(fs.existsSync(path.join(dashboard01, 'data.json'))).toBe(true);
});
test('dashboard-01 has 11 component files', () => {
const componentsDir = path.join(blocksDir, 'dashboard/dashboard-01/components');
const files = fs.readdirSync(componentsDir).filter(f => f.endsWith('.tsx'));
expect(files.length).toBeGreaterThanOrEqual(6); // At least major components
});
test('dashboard-01 page imports components correctly', () => {
const pagePath = path.join(blocksDir, 'dashboard/dashboard-01/page.tsx');
const content = fs.readFileSync(pagePath, 'utf8');
expect(content).toContain('./components');
});
test('dashboard index exports correctly', () => {
const indexPath = path.join(blocksDir, 'dashboard/index.ts');
expect(fs.existsSync(indexPath)).toBe(true);
const content = fs.readFileSync(indexPath, 'utf8');
expect(content).toContain('dashboard-01');
});
});
describe('Sidebar Blocks (16 variants, ~72 files)', () => {
test('all 16 sidebar variants exist', () => {
for (let i = 1; i <= 16; i++) {
const variant = String(i).padStart(2, '0');
const sidebarPath = path.join(blocksDir, `sidebar/sidebar-${variant}`);
expect(fs.existsSync(sidebarPath)).toBe(true);
expect(fs.existsSync(path.join(sidebarPath, 'page.tsx'))).toBe(true);
expect(fs.existsSync(path.join(sidebarPath, 'components'))).toBe(true);
}
});
test('each sidebar has components directory with files', () => {
for (let i = 1; i <= 16; i++) {
const variant = String(i).padStart(2, '0');
const componentsDir = path.join(blocksDir, `sidebar/sidebar-${variant}/components`);
const files = fs.readdirSync(componentsDir);
expect(files.length).toBeGreaterThan(0);
}
});
test('sidebar pages import from local components', () => {
for (let i = 1; i <= 16; i++) {
const variant = String(i).padStart(2, '0');
const pagePath = path.join(blocksDir, `sidebar/sidebar-${variant}/page.tsx`);
const content = fs.readFileSync(pagePath, 'utf8');
expect(content).toContain('./components');
}
});
test('sidebar index exports all variants', () => {
const indexPath = path.join(blocksDir, 'sidebar/index.ts');
expect(fs.existsSync(indexPath)).toBe(true);
const content = fs.readFileSync(indexPath, 'utf8');
for (let i = 1; i <= 16; i++) {
const variant = String(i).padStart(2, '0');
expect(content).toContain(`sidebar-${variant}`);
}
});
});
describe('Calendar Blocks (32 files)', () => {
test('all 32 calendar variants exist', () => {
for (let i = 1; i <= 32; i++) {
const variant = String(i).padStart(2, '0');
const calendarPath = path.join(blocksDir, `calendar/calendar-${variant}.tsx`);
expect(fs.existsSync(calendarPath)).toBe(true);
}
});
test('calendar files export default components', () => {
for (let i = 1; i <= 32; i++) {
const variant = String(i).padStart(2, '0');
const calendarPath = path.join(blocksDir, `calendar/calendar-${variant}.tsx`);
const content = fs.readFileSync(calendarPath, 'utf8');
expect(content).toContain('export default');
}
});
test('calendar files use primitives correctly', () => {
const calendar01 = path.join(blocksDir, 'calendar/calendar-01.tsx');
const content = fs.readFileSync(calendar01, 'utf8');
expect(content).toContain('primitives');
});
test('calendar index exports all variants', () => {
const indexPath = path.join(blocksDir, 'calendar/index.ts');
expect(fs.existsSync(indexPath)).toBe(true);
const content = fs.readFileSync(indexPath, 'utf8');
for (let i = 1; i <= 32; i++) {
const variant = String(i).padStart(2, '0');
expect(content).toContain(`calendar-${variant}`);
}
});
});
describe('Auth Blocks (15 variants: 5 login, 5 signup, 5 otp)', () => {
test('all 5 login variants exist', () => {
for (let i = 1; i <= 5; i++) {
const variant = String(i).padStart(2, '0');
const loginPath = path.join(blocksDir, `auth/login/login-${variant}`);
expect(fs.existsSync(loginPath)).toBe(true);
expect(fs.existsSync(path.join(loginPath, 'page.tsx'))).toBe(true);
expect(fs.existsSync(path.join(loginPath, 'components'))).toBe(true);
}
});
test('all 5 signup variants exist', () => {
for (let i = 1; i <= 5; i++) {
const variant = String(i).padStart(2, '0');
const signupPath = path.join(blocksDir, `auth/signup/signup-${variant}`);
expect(fs.existsSync(signupPath)).toBe(true);
expect(fs.existsSync(path.join(signupPath, 'page.tsx'))).toBe(true);
expect(fs.existsSync(path.join(signupPath, 'components'))).toBe(true);
}
});
test('all 5 otp variants exist', () => {
for (let i = 1; i <= 5; i++) {
const variant = String(i).padStart(2, '0');
const otpPath = path.join(blocksDir, `auth/otp/otp-${variant}`);
expect(fs.existsSync(otpPath)).toBe(true);
expect(fs.existsSync(path.join(otpPath, 'page.tsx'))).toBe(true);
expect(fs.existsSync(path.join(otpPath, 'components'))).toBe(true);
}
});
test('login variants have components', () => {
for (let i = 1; i <= 5; i++) {
const variant = String(i).padStart(2, '0');
const componentsDir = path.join(blocksDir, `auth/login/login-${variant}/components`);
const files = fs.readdirSync(componentsDir);
expect(files.length).toBeGreaterThan(0);
}
});
test('signup variants have components', () => {
for (let i = 1; i <= 5; i++) {
const variant = String(i).padStart(2, '0');
const componentsDir = path.join(blocksDir, `auth/signup/signup-${variant}/components`);
const files = fs.readdirSync(componentsDir);
expect(files.length).toBeGreaterThan(0);
}
});
test('otp variants have components', () => {
for (let i = 1; i <= 5; i++) {
const variant = String(i).padStart(2, '0');
const componentsDir = path.join(blocksDir, `auth/otp/otp-${variant}/components`);
const files = fs.readdirSync(componentsDir);
expect(files.length).toBeGreaterThan(0);
}
});
test('auth index exports all blocks', () => {
const indexPath = path.join(blocksDir, 'auth/index.ts');
expect(fs.existsSync(indexPath)).toBe(true);
const content = fs.readFileSync(indexPath, 'utf8');
expect(content).toContain('login');
expect(content).toContain('signup');
expect(content).toContain('otp');
});
});
describe('Import Path Validation', () => {
test('blocks use relative primitive imports', () => {
const calendar01 = path.join(blocksDir, 'calendar/calendar-01.tsx');
const content = fs.readFileSync(calendar01, 'utf8');
expect(content).toContain('../../primitives');
});
test('sidebar blocks import components correctly', () => {
const sidebar01 = path.join(blocksDir, 'sidebar/sidebar-01/page.tsx');
const content = fs.readFileSync(sidebar01, 'utf8');
expect(content).toContain('./components');
});
test('auth blocks import components correctly', () => {
const login01 = path.join(blocksDir, 'auth/login/login-01/page.tsx');
const content = fs.readFileSync(login01, 'utf8');
expect(content).toContain('./components');
});
});
describe('File Integrity', () => {
test('all calendar files are valid TSX', () => {
for (let i = 1; i <= 32; i++) {
const variant = String(i).padStart(2, '0');
const calendarPath = path.join(blocksDir, `calendar/calendar-${variant}.tsx`);
const content = fs.readFileSync(calendarPath, 'utf8');
expect(content.length).toBeGreaterThan(0);
expect(content).toContain('React');
}
});
test('all sidebar pages are valid TSX', () => {
for (let i = 1; i <= 16; i++) {
const variant = String(i).padStart(2, '0');
const pagePath = path.join(blocksDir, `sidebar/sidebar-${variant}/page.tsx`);
const content = fs.readFileSync(pagePath, 'utf8');
expect(content.length).toBeGreaterThan(0);
}
});
test('all auth pages are valid TSX', () => {
const authTypes = ['login', 'signup', 'otp'];
authTypes.forEach(type => {
for (let i = 1; i <= 5; i++) {
const variant = String(i).padStart(2, '0');
const pagePath = path.join(blocksDir, `auth/${type}/${type}-${variant}/page.tsx`);
const content = fs.readFileSync(pagePath, 'utf8');
expect(content.length).toBeGreaterThan(0);
}
});
});
});
describe('Total File Counts', () => {
test('dashboard has 1 block', () => {
const dashboardDir = path.join(blocksDir, 'dashboard');
const dirs = fs.readdirSync(dashboardDir).filter(f =>
fs.statSync(path.join(dashboardDir, f)).isDirectory()
);
expect(dirs.length).toBe(1);
});
test('sidebar has 16 blocks', () => {
const sidebarDir = path.join(blocksDir, 'sidebar');
const dirs = fs.readdirSync(sidebarDir).filter(f =>
fs.statSync(path.join(sidebarDir, f)).isDirectory()
);
expect(dirs.length).toBe(16);
});
test('calendar has 32 files', () => {
const calendarDir = path.join(blocksDir, 'calendar');
const files = fs.readdirSync(calendarDir).filter(f =>
f.startsWith('calendar-') && f.endsWith('.tsx')
);
expect(files.length).toBe(32);
});
test('auth has 15 total variants (5 each)', () => {
const authTypes = ['login', 'signup', 'otp'];
let totalCount = 0;
authTypes.forEach(type => {
const typeDir = path.join(blocksDir, `auth/${type}`);
const dirs = fs.readdirSync(typeDir).filter(f =>
fs.statSync(path.join(typeDir, f)).isDirectory()
);
totalCount += dirs.length;
});
expect(totalCount).toBe(15);
});
});
});
@@ -1,260 +0,0 @@
/**
* Dependency Check Tests
* Validates that all block dependencies are correctly resolved
*/
const fs = require('fs');
const path = require('path');
const blocksDir = path.join(__dirname, '..');
const primitivesDir = path.join(__dirname, '../../primitives');
// Extract imports from a file
function extractImports(content) {
const importRegex = /import\s+(?:{[^}]*}|[^from]+)\s+from\s+['"]([^'"]+)['"]/g;
const imports = [];
let match;
while ((match = importRegex.exec(content)) !== null) {
imports.push(match[1]);
}
return imports;
}
// Check if a primitive exists
function primitiveExists(primitiveName) {
const primitivePath = path.join(primitivesDir, `${primitiveName}.tsx`);
return fs.existsSync(primitivePath);
}
describe('Block Dependencies', () => {
describe('Dashboard Dependencies', () => {
test('dashboard-01 page imports are valid', () => {
const pagePath = path.join(blocksDir, 'dashboard/dashboard-01/page.tsx');
const content = fs.readFileSync(pagePath, 'utf8');
const imports = extractImports(content);
// Check local imports
const localImports = imports.filter(imp => imp.startsWith('./'));
localImports.forEach(imp => {
const importPath = path.join(blocksDir, 'dashboard/dashboard-01', imp);
expect(fs.existsSync(importPath) || fs.existsSync(`${importPath}.tsx`)).toBe(true);
});
});
test('dashboard-01 components use valid primitives', () => {
const componentsDir = path.join(blocksDir, 'dashboard/dashboard-01/components');
const files = fs.readdirSync(componentsDir).filter(f => f.endsWith('.tsx'));
files.forEach(file => {
const filePath = path.join(componentsDir, file);
const content = fs.readFileSync(filePath, 'utf8');
const imports = extractImports(content);
// Check primitive imports
const primitiveImports = imports.filter(imp => imp.includes('primitives/'));
primitiveImports.forEach(imp => {
const primitiveName = imp.split('/').pop();
expect(primitiveExists(primitiveName)).toBe(true);
});
});
});
});
describe('Sidebar Dependencies', () => {
test('sidebar variants import valid primitives', () => {
for (let i = 1; i <= 3; i++) { // Test first 3 variants
const variant = String(i).padStart(2, '0');
const pagePath = path.join(blocksDir, `sidebar/sidebar-${variant}/page.tsx`);
const content = fs.readFileSync(pagePath, 'utf8');
const imports = extractImports(content);
// Verify local imports exist
const localImports = imports.filter(imp => imp.startsWith('./'));
localImports.forEach(imp => {
const importPath = path.join(blocksDir, `sidebar/sidebar-${variant}`, imp);
expect(
fs.existsSync(importPath) ||
fs.existsSync(`${importPath}.tsx`) ||
fs.existsSync(`${importPath}/index.tsx`)
).toBe(true);
});
}
});
test('sidebar components use valid primitives', () => {
const componentsDir = path.join(blocksDir, 'sidebar/sidebar-01/components');
const files = fs.readdirSync(componentsDir).filter(f => f.endsWith('.tsx'));
files.forEach(file => {
const filePath = path.join(componentsDir, file);
const content = fs.readFileSync(filePath, 'utf8');
const imports = extractImports(content);
// Check primitive imports
const primitiveImports = imports.filter(imp => imp.includes('primitives/'));
primitiveImports.forEach(imp => {
const primitiveName = imp.split('/').pop();
expect(primitiveExists(primitiveName)).toBe(true);
});
});
});
});
describe('Calendar Dependencies', () => {
test('calendar variants import Calendar primitive', () => {
for (let i = 1; i <= 5; i++) { // Test first 5 variants
const variant = String(i).padStart(2, '0');
const calendarPath = path.join(blocksDir, `calendar/calendar-${variant}.tsx`);
const content = fs.readFileSync(calendarPath, 'utf8');
// Calendar should import from primitives
expect(content).toContain('primitives/calendar');
}
});
test('calendar primitives are available', () => {
expect(primitiveExists('calendar')).toBe(true);
});
test('calendar variants use valid imports', () => {
const calendar01 = path.join(blocksDir, 'calendar/calendar-01.tsx');
const content = fs.readFileSync(calendar01, 'utf8');
const imports = extractImports(content);
// All imports should be resolvable
imports.forEach(imp => {
if (imp.includes('primitives/')) {
const primitiveName = imp.split('/').pop();
expect(primitiveExists(primitiveName)).toBe(true);
}
});
});
});
describe('Auth Dependencies', () => {
test('login variants have valid component imports', () => {
for (let i = 1; i <= 3; i++) { // Test first 3 variants
const variant = String(i).padStart(2, '0');
const pagePath = path.join(blocksDir, `auth/login/login-${variant}/page.tsx`);
const content = fs.readFileSync(pagePath, 'utf8');
const imports = extractImports(content);
// Verify local imports
const localImports = imports.filter(imp => imp.startsWith('./'));
localImports.forEach(imp => {
const importPath = path.join(blocksDir, `auth/login/login-${variant}`, imp);
expect(
fs.existsSync(importPath) ||
fs.existsSync(`${importPath}.tsx`) ||
fs.existsSync(`${importPath}/index.tsx`)
).toBe(true);
});
}
});
test('signup variants have valid component imports', () => {
for (let i = 1; i <= 3; i++) { // Test first 3 variants
const variant = String(i).padStart(2, '0');
const pagePath = path.join(blocksDir, `auth/signup/signup-${variant}/page.tsx`);
const content = fs.readFileSync(pagePath, 'utf8');
const imports = extractImports(content);
// Verify local imports
const localImports = imports.filter(imp => imp.startsWith('./'));
localImports.forEach(imp => {
const importPath = path.join(blocksDir, `auth/signup/signup-${variant}`, imp);
expect(
fs.existsSync(importPath) ||
fs.existsSync(`${importPath}.tsx`) ||
fs.existsSync(`${importPath}/index.tsx`)
).toBe(true);
});
}
});
test('otp variants have valid component imports', () => {
for (let i = 1; i <= 3; i++) { // Test first 3 variants
const variant = String(i).padStart(2, '0');
const pagePath = path.join(blocksDir, `auth/otp/otp-${variant}/page.tsx`);
const content = fs.readFileSync(pagePath, 'utf8');
const imports = extractImports(content);
// Verify local imports
const localImports = imports.filter(imp => imp.startsWith('./'));
localImports.forEach(imp => {
const importPath = path.join(blocksDir, `auth/otp/otp-${variant}`, imp);
expect(
fs.existsSync(importPath) ||
fs.existsSync(`${importPath}.tsx`) ||
fs.existsSync(`${importPath}/index.tsx`)
).toBe(true);
});
}
});
});
describe('Required Primitives', () => {
const requiredPrimitives = [
'button',
'card',
'input',
'label',
'calendar',
'select',
'sidebar',
'form',
'checkbox',
'input-otp'
];
requiredPrimitives.forEach(primitive => {
test(`${primitive} primitive exists`, () => {
expect(primitiveExists(primitive)).toBe(true);
});
});
});
describe('External Dependencies', () => {
test('blocks can import React', () => {
const calendar01 = path.join(blocksDir, 'calendar/calendar-01.tsx');
const content = fs.readFileSync(calendar01, 'utf8');
expect(content).toContain('React');
});
test('blocks use client directive where needed', () => {
const calendar01 = path.join(blocksDir, 'calendar/calendar-01.tsx');
const content = fs.readFileSync(calendar01, 'utf8');
expect(content).toContain('"use client"');
});
});
});
describe('Import Path Consistency', () => {
test('all blocks use relative primitive imports', () => {
// Check calendar
const calendar01 = path.join(blocksDir, 'calendar/calendar-01.tsx');
const calContent = fs.readFileSync(calendar01, 'utf8');
const calImports = extractImports(calContent);
const calPrimitiveImports = calImports.filter(imp => imp.includes('primitives'));
calPrimitiveImports.forEach(imp => {
expect(imp).toContain('../');
});
});
test('all blocks use local component imports', () => {
// Check dashboard
const dashPage = path.join(blocksDir, 'dashboard/dashboard-01/page.tsx');
const dashContent = fs.readFileSync(dashPage, 'utf8');
expect(dashContent).toContain('./components');
// Check sidebar
const sidebarPage = path.join(blocksDir, 'sidebar/sidebar-01/page.tsx');
const sidebarContent = fs.readFileSync(sidebarPage, 'utf8');
expect(sidebarContent).toContain('./components');
// Check auth
const loginPage = path.join(blocksDir, 'auth/login/login-01/page.tsx');
const loginContent = fs.readFileSync(loginPage, 'utf8');
expect(loginContent).toContain('./components');
});
});
@@ -1,682 +0,0 @@
/**
* Comprehensive Auth Form Validation Tests
* Tests all auth forms: login (5 variants), signup (5 variants), OTP (5 variants)
*
* Test Coverage:
* - Form submission with valid data
* - Field validation (email, password, OTP)
* - User interactions (typing, clicking)
* - Password matching for signup forms
* - OTP input handling (6-digit code)
*/
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'
import { render, screen, waitFor } from '@testing-library/react'
import userEvent from '@testing-library/user-event'
// Mock console.log to verify form submissions
let consoleLogSpy: ReturnType<typeof vi.spyOn>
beforeEach(() => {
consoleLogSpy = vi.spyOn(console, 'log').mockImplementation(() => {})
})
afterEach(() => {
consoleLogSpy.mockRestore()
})
describe('Auth Forms - Comprehensive Validation', () => {
describe('Login Forms (5 variants)', () => {
it('login-01: submits form data correctly', async () => {
const { LoginForm } = await import('../login/login-01/components/login-form')
const user = userEvent.setup()
render(<LoginForm />)
// Find and fill email field
const emailInput = screen.getByLabelText(/email/i)
await user.type(emailInput, 'test@example.com')
// Find and fill password field
const passwordInput = screen.getByLabelText(/^password$/i)
await user.type(passwordInput, 'password123')
// Submit form
const submitButton = screen.getByRole('button', { name: /^login$/i })
await user.click(submitButton)
// Verify handleSubmit was called with correct data
await waitFor(() => {
expect(consoleLogSpy).toHaveBeenCalledWith(
'Form submitted:',
expect.objectContaining({
email: 'test@example.com',
password: 'password123'
})
)
})
})
it('login-01: validates required email field', async () => {
const { LoginForm } = await import('../login/login-01/components/login-form')
const user = userEvent.setup()
render(<LoginForm />)
const emailInput = screen.getByLabelText(/email/i) as HTMLInputElement
expect(emailInput).toHaveAttribute('required')
expect(emailInput).toHaveAttribute('type', 'email')
})
it('login-01: validates required password field', async () => {
const { LoginForm } = await import('../login/login-01/components/login-form')
render(<LoginForm />)
const passwordInput = screen.getByLabelText(/^password$/i) as HTMLInputElement
expect(passwordInput).toHaveAttribute('required')
expect(passwordInput).toHaveAttribute('type', 'password')
})
it('login-02: submits form data correctly', async () => {
const { LoginForm } = await import('../login/login-02/components/login-form')
const user = userEvent.setup()
render(<LoginForm />)
const emailInput = screen.getByLabelText(/email/i)
await user.type(emailInput, 'user@test.com')
const passwordInput = screen.getByLabelText(/^password$/i)
await user.type(passwordInput, 'securepass456')
const submitButton = screen.getByRole('button', { name: /^login$/i })
await user.click(submitButton)
await waitFor(() => {
expect(consoleLogSpy).toHaveBeenCalledWith(
'Form submitted:',
expect.objectContaining({
email: 'user@test.com',
password: 'securepass456'
})
)
})
})
it('login-03: submits form data correctly', async () => {
const { LoginForm } = await import('../login/login-03/components/login-form')
const user = userEvent.setup()
render(<LoginForm />)
const emailInput = screen.getByLabelText(/email/i)
await user.type(emailInput, 'admin@example.com')
const passwordInput = screen.getByLabelText(/^password$/i)
await user.type(passwordInput, 'admin789')
const submitButton = screen.getByRole('button', { name: /^login$/i })
await user.click(submitButton)
await waitFor(() => {
expect(consoleLogSpy).toHaveBeenCalledWith(
'Form submitted:',
expect.objectContaining({
email: 'admin@example.com',
password: 'admin789'
})
)
})
})
it('login-04: submits form data correctly', async () => {
const { LoginForm } = await import('../login/login-04/components/login-form')
const user = userEvent.setup()
render(<LoginForm />)
const emailInput = screen.getByLabelText(/email/i)
await user.type(emailInput, 'dev@hanzo.ai')
const passwordInput = screen.getByLabelText(/^password$/i)
await user.type(passwordInput, 'developer123')
const submitButton = screen.getByRole('button', { name: /^login$/i })
await user.click(submitButton)
await waitFor(() => {
expect(consoleLogSpy).toHaveBeenCalledWith(
'Form submitted:',
expect.objectContaining({
email: 'dev@hanzo.ai',
password: 'developer123'
})
)
})
})
it('login-05: submits form data correctly (passwordless)', async () => {
const { LoginForm } = await import('../login/login-05/components/login-form')
const user = userEvent.setup()
render(<LoginForm />)
const emailInput = screen.getByLabelText(/email/i)
await user.type(emailInput, 'tester@hanzo.ai')
// login-05 is a passwordless/magic link login, so no password field
const submitButton = screen.getByRole('button', { name: /^login$/i })
await user.click(submitButton)
await waitFor(() => {
expect(consoleLogSpy).toHaveBeenCalledWith(
'Form submitted:',
expect.objectContaining({
email: 'tester@hanzo.ai'
})
)
})
})
it('login forms 01-04 have email and password fields', async () => {
const forms = [
await import('../login/login-01/components/login-form'),
await import('../login/login-02/components/login-form'),
await import('../login/login-03/components/login-form'),
await import('../login/login-04/components/login-form'),
]
for (const { LoginForm } of forms) {
const { unmount } = render(<LoginForm />)
expect(screen.getByLabelText(/email/i)).toBeInTheDocument()
expect(screen.getByLabelText(/^password$/i)).toBeInTheDocument()
unmount()
}
})
it('login-05 is passwordless (email only)', async () => {
const { LoginForm } = await import('../login/login-05/components/login-form')
render(<LoginForm />)
expect(screen.getByLabelText(/email/i)).toBeInTheDocument()
expect(screen.queryByLabelText(/^password$/i)).not.toBeInTheDocument()
})
it('all login forms have submit buttons', async () => {
const forms = [
await import('../login/login-01/components/login-form'),
await import('../login/login-02/components/login-form'),
await import('../login/login-03/components/login-form'),
await import('../login/login-04/components/login-form'),
await import('../login/login-05/components/login-form'),
]
for (const { LoginForm } of forms) {
const { unmount } = render(<LoginForm />)
expect(screen.getByRole('button', { name: /^login$/i })).toBeInTheDocument()
unmount()
}
})
})
describe('Signup Forms (5 variants)', () => {
it('signup-01: submits complete form data', async () => {
const { SignupForm } = await import('../signup/signup-01/components/signup-form')
const user = userEvent.setup()
render(<SignupForm />)
const nameInput = screen.getByLabelText(/full name/i)
await user.type(nameInput, 'John Doe')
const emailInput = screen.getByLabelText(/email/i)
await user.type(emailInput, 'john@example.com')
const passwordInput = screen.getByLabelText(/^password$/i)
await user.type(passwordInput, 'password123')
const confirmPasswordInput = screen.getByLabelText(/confirm password/i)
await user.type(confirmPasswordInput, 'password123')
const submitButton = screen.getByRole('button', { name: /create account/i })
await user.click(submitButton)
await waitFor(() => {
expect(consoleLogSpy).toHaveBeenCalledWith(
'Form submitted:',
expect.objectContaining({
name: 'John Doe',
email: 'john@example.com',
password: 'password123',
'confirm-password': 'password123'
})
)
})
})
it('signup-01: validates password length requirement', async () => {
const { SignupForm } = await import('../signup/signup-01/components/signup-form')
render(<SignupForm />)
// Check that password description mentions 8 characters
expect(screen.getByText(/must be at least 8 characters/i)).toBeInTheDocument()
})
it('signup-01: has all required fields', async () => {
const { SignupForm } = await import('../signup/signup-01/components/signup-form')
render(<SignupForm />)
expect(screen.getByLabelText(/full name/i)).toHaveAttribute('required')
expect(screen.getByLabelText(/email/i)).toHaveAttribute('required')
expect(screen.getByLabelText(/^password$/i)).toHaveAttribute('required')
expect(screen.getByLabelText(/confirm password/i)).toHaveAttribute('required')
})
it('signup-02: submits complete form data', async () => {
const { SignupForm } = await import('../signup/signup-02/components/signup-form')
const user = userEvent.setup()
render(<SignupForm />)
const nameInput = screen.getByLabelText(/full name/i)
await user.type(nameInput, 'Jane Smith')
const emailInput = screen.getByLabelText(/email/i)
await user.type(emailInput, 'jane@test.com')
const passwordInput = screen.getByLabelText(/^password$/i)
await user.type(passwordInput, 'securepass')
const confirmPasswordInput = screen.getByLabelText(/confirm password/i)
await user.type(confirmPasswordInput, 'securepass')
const submitButton = screen.getByRole('button', { name: /create account/i })
await user.click(submitButton)
await waitFor(() => {
expect(consoleLogSpy).toHaveBeenCalled()
})
})
it('signup-03: submits complete form data', async () => {
const { SignupForm } = await import('../signup/signup-03/components/signup-form')
const user = userEvent.setup()
render(<SignupForm />)
const nameInput = screen.getByLabelText(/full name/i)
await user.type(nameInput, 'Bob Wilson')
const emailInput = screen.getByLabelText(/email/i)
await user.type(emailInput, 'bob@hanzo.ai')
const passwordInput = screen.getByLabelText(/^password$/i)
await user.type(passwordInput, 'bobpass123')
const confirmPasswordInput = screen.getByLabelText(/confirm password/i)
await user.type(confirmPasswordInput, 'bobpass123')
const submitButton = screen.getByRole('button', { name: /create account/i })
await user.click(submitButton)
await waitFor(() => {
expect(consoleLogSpy).toHaveBeenCalled()
})
})
it('signup-04: submits complete form data', async () => {
const { SignupForm } = await import('../signup/signup-04/components/signup-form')
const user = userEvent.setup()
render(<SignupForm />)
// signup-04 only has email, password, and confirm-password (no name field)
const emailInput = screen.getByLabelText(/email/i)
await user.type(emailInput, 'alice@example.com')
const passwordInput = screen.getByLabelText(/^password$/i)
await user.type(passwordInput, 'alice2024')
const confirmPasswordInput = screen.getByLabelText(/confirm password/i)
await user.type(confirmPasswordInput, 'alice2024')
const submitButton = screen.getByRole('button', { name: /create account/i })
await user.click(submitButton)
await waitFor(() => {
expect(consoleLogSpy).toHaveBeenCalledWith(
'Form submitted:',
expect.objectContaining({
email: 'alice@example.com',
password: 'alice2024',
'confirm-password': 'alice2024'
})
)
})
})
it('signup-05: submits complete form data (passwordless)', async () => {
const { SignupForm } = await import('../signup/signup-05/components/signup-form')
const user = userEvent.setup()
render(<SignupForm />)
// signup-05 is email-only (passwordless/magic link signup)
const emailInput = screen.getByLabelText(/email/i)
await user.type(emailInput, 'charlie@hanzo.ai')
const submitButton = screen.getByRole('button', { name: /create account/i })
await user.click(submitButton)
await waitFor(() => {
expect(consoleLogSpy).toHaveBeenCalledWith(
'Form submitted:',
expect.objectContaining({
email: 'charlie@hanzo.ai'
})
)
})
})
it('signup forms 01-03 have all fields (name, email, password, confirm)', async () => {
const forms = [
await import('../signup/signup-01/components/signup-form'),
await import('../signup/signup-02/components/signup-form'),
await import('../signup/signup-03/components/signup-form'),
]
for (const { SignupForm } of forms) {
const { unmount } = render(<SignupForm />)
expect(screen.getByLabelText(/full name/i)).toBeInTheDocument()
expect(screen.getByLabelText(/email/i)).toBeInTheDocument()
expect(screen.getByLabelText(/^password$/i)).toBeInTheDocument()
expect(screen.getByLabelText(/confirm password/i)).toBeInTheDocument()
unmount()
}
})
it('signup-04 has email, password, and confirm password (no name)', async () => {
const { SignupForm } = await import('../signup/signup-04/components/signup-form')
render(<SignupForm />)
expect(screen.queryByLabelText(/full name/i)).not.toBeInTheDocument()
expect(screen.getByLabelText(/email/i)).toBeInTheDocument()
expect(screen.getByLabelText(/^password$/i)).toBeInTheDocument()
expect(screen.getByLabelText(/confirm password/i)).toBeInTheDocument()
})
it('signup-05 is email-only (passwordless)', async () => {
const { SignupForm } = await import('../signup/signup-05/components/signup-form')
render(<SignupForm />)
expect(screen.queryByLabelText(/full name/i)).not.toBeInTheDocument()
expect(screen.getByLabelText(/email/i)).toBeInTheDocument()
expect(screen.queryByLabelText(/^password$/i)).not.toBeInTheDocument()
expect(screen.queryByLabelText(/confirm password/i)).not.toBeInTheDocument()
})
it('all signup forms have create account buttons', async () => {
const forms = [
await import('../signup/signup-01/components/signup-form'),
await import('../signup/signup-02/components/signup-form'),
await import('../signup/signup-03/components/signup-form'),
await import('../signup/signup-04/components/signup-form'),
await import('../signup/signup-05/components/signup-form'),
]
for (const { SignupForm } of forms) {
const { unmount } = render(<SignupForm />)
expect(screen.getByRole('button', { name: /create account/i })).toBeInTheDocument()
unmount()
}
})
})
describe('OTP Forms (5 variants)', () => {
it('otp-01: submits 6-digit OTP correctly', async () => {
const { OTPForm } = await import('../otp/otp-01/components/otp-form')
const user = userEvent.setup()
render(<OTPForm />)
// OTP uses InputOTP component with individual slots
// Find the OTP input container
const otpInput = screen.getByLabelText(/verification code/i)
// Type the 6-digit code
await user.type(otpInput, '123456')
const submitButton = screen.getByRole('button', { name: /verify/i })
await user.click(submitButton)
await waitFor(() => {
expect(consoleLogSpy).toHaveBeenCalledWith(
'Form submitted:',
expect.objectContaining({
otp: '123456'
})
)
})
})
it('otp-01: validates OTP field is required', async () => {
const { OTPForm } = await import('../otp/otp-01/components/otp-form')
render(<OTPForm />)
const otpInput = screen.getByLabelText(/verification code/i)
expect(otpInput).toHaveAttribute('required')
})
it('otp-01: has description about 6-digit code', async () => {
const { OTPForm } = await import('../otp/otp-01/components/otp-form')
render(<OTPForm />)
expect(screen.getByText(/enter the 6-digit code/i)).toBeInTheDocument()
})
it('otp-02: submits 6-digit OTP correctly', async () => {
const { OTPForm } = await import('../otp/otp-02/components/otp-form')
const user = userEvent.setup()
render(<OTPForm />)
const otpInput = screen.getByLabelText(/verification code/i)
await user.type(otpInput, '654321')
const submitButton = screen.getByRole('button', { name: /verify/i })
await user.click(submitButton)
await waitFor(() => {
expect(consoleLogSpy).toHaveBeenCalled()
})
})
it('otp-03: submits 6-digit OTP correctly', async () => {
const { OTPForm } = await import('../otp/otp-03/components/otp-form')
const user = userEvent.setup()
render(<OTPForm />)
const otpInput = screen.getByLabelText(/verification code/i)
await user.type(otpInput, '789012')
const submitButton = screen.getByRole('button', { name: /verify/i })
await user.click(submitButton)
await waitFor(() => {
expect(consoleLogSpy).toHaveBeenCalled()
})
})
it('otp-04: submits 6-digit OTP correctly', async () => {
const { OTPForm } = await import('../otp/otp-04/components/otp-form')
const user = userEvent.setup()
render(<OTPForm />)
const otpInput = screen.getByLabelText(/verification code/i)
await user.type(otpInput, '345678')
const submitButton = screen.getByRole('button', { name: /verify/i })
await user.click(submitButton)
await waitFor(() => {
expect(consoleLogSpy).toHaveBeenCalled()
})
})
it('otp-05: submits 6-digit OTP correctly', async () => {
const { OTPForm } = await import('../otp/otp-05/components/otp-form')
const user = userEvent.setup()
render(<OTPForm />)
const otpInput = screen.getByLabelText(/verification code/i)
await user.type(otpInput, '901234')
const submitButton = screen.getByRole('button', { name: /verify/i })
await user.click(submitButton)
await waitFor(() => {
expect(consoleLogSpy).toHaveBeenCalled()
})
})
it('all OTP forms have verification code input', async () => {
const forms = [
await import('../otp/otp-01/components/otp-form'),
await import('../otp/otp-02/components/otp-form'),
await import('../otp/otp-03/components/otp-form'),
await import('../otp/otp-04/components/otp-form'),
await import('../otp/otp-05/components/otp-form'),
]
for (const { OTPForm } of forms) {
const { unmount } = render(<OTPForm />)
expect(screen.getByLabelText(/verification code/i)).toBeInTheDocument()
unmount()
}
})
it('all OTP forms have verify buttons', async () => {
const forms = [
await import('../otp/otp-01/components/otp-form'),
await import('../otp/otp-02/components/otp-form'),
await import('../otp/otp-03/components/otp-form'),
await import('../otp/otp-04/components/otp-form'),
await import('../otp/otp-05/components/otp-form'),
]
for (const { OTPForm } of forms) {
const { unmount } = render(<OTPForm />)
expect(screen.getByRole('button', { name: /verify/i })).toBeInTheDocument()
unmount()
}
})
})
describe('Form Interaction Tests', () => {
it('login form: clears input after typing and clearing', async () => {
const { LoginForm } = await import('../login/login-01/components/login-form')
const user = userEvent.setup()
render(<LoginForm />)
const emailInput = screen.getByLabelText(/email/i) as HTMLInputElement
await user.type(emailInput, 'test@example.com')
expect(emailInput.value).toBe('test@example.com')
await user.clear(emailInput)
expect(emailInput.value).toBe('')
})
it('signup form: handles multiple field updates', async () => {
const { SignupForm } = await import('../signup/signup-01/components/signup-form')
const user = userEvent.setup()
render(<SignupForm />)
const nameInput = screen.getByLabelText(/full name/i) as HTMLInputElement
const emailInput = screen.getByLabelText(/email/i) as HTMLInputElement
await user.type(nameInput, 'Test User')
await user.type(emailInput, 'test@example.com')
expect(nameInput.value).toBe('Test User')
expect(emailInput.value).toBe('test@example.com')
})
it('OTP form: accepts numeric input only', async () => {
const { OTPForm } = await import('../otp/otp-01/components/otp-form')
const user = userEvent.setup()
render(<OTPForm />)
const otpInput = screen.getByLabelText(/verification code/i)
// Type only numbers
await user.type(otpInput, '123456')
// Should have accepted the numeric input
expect(otpInput).toHaveValue('123456')
})
})
describe('Console Log Verification', () => {
it('verifies console.log is called on form submission', async () => {
const { LoginForm } = await import('../login/login-01/components/login-form')
const user = userEvent.setup()
render(<LoginForm />)
await user.type(screen.getByLabelText(/email/i), 'verify@test.com')
await user.type(screen.getByLabelText(/^password$/i), 'verify123')
await user.click(screen.getByRole('button', { name: /^login$/i }))
await waitFor(() => {
expect(consoleLogSpy).toHaveBeenCalled()
expect(consoleLogSpy.mock.calls[0][0]).toBe('Form submitted:')
})
})
it('verifies form data structure in console.log', async () => {
const { SignupForm } = await import('../signup/signup-01/components/signup-form')
const user = userEvent.setup()
render(<SignupForm />)
await user.type(screen.getByLabelText(/full name/i), 'Data Test')
await user.type(screen.getByLabelText(/email/i), 'data@test.com')
await user.type(screen.getByLabelText(/^password$/i), 'data123')
await user.type(screen.getByLabelText(/confirm password/i), 'data123')
await user.click(screen.getByRole('button', { name: /create account/i }))
await waitFor(() => {
const loggedData = consoleLogSpy.mock.calls[0][1]
expect(loggedData).toHaveProperty('name')
expect(loggedData).toHaveProperty('email')
expect(loggedData).toHaveProperty('password')
})
})
})
})
-4
View File
@@ -1,4 +0,0 @@
// Auth Blocks
export * from './login'
export * from './signup'
export * from './otp'
-6
View File
@@ -1,6 +0,0 @@
// Login Blocks
export { default as Login01 } from './login-01/page'
export { default as Login02 } from './login-02/page'
export { default as Login03 } from './login-03/page'
export { default as Login04 } from './login-04/page'
export { default as Login05 } from './login-05/page'
@@ -1,79 +0,0 @@
import { cn } from "../../../../../util"
import { Button } from "../../../../../primitives/button"
import {
Card,
CardContent,
CardDescription,
CardHeader,
CardTitle,
} from "../../../../../primitives/card"
import { Input } from "../../../../../primitives/input"
import { Label } from "../../../../../primitives/label"
export function LoginForm({
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&apos;t have an account?{" "}
<a href="#" className="underline underline-offset-4">
Sign up
</a>
</div>
</form>
</CardContent>
</Card>
</div>
)
}
@@ -1,11 +0,0 @@
import { LoginForm } from "./components/login-form"
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">
<LoginForm />
</div>
</div>
)
}
@@ -1,71 +0,0 @@
import { cn } from "../../../../../util"
import { Button } from "../../../../../primitives/button"
import { Input } from "../../../../../primitives/input"
import { Label } from "../../../../../primitives/label"
export function LoginForm({
className,
...props
}: React.ComponentPropsWithoutRef<"form">) {
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 (
<form className={cn("flex flex-col gap-6", className)} {...props} onSubmit={handleSubmit}>
<div className="flex flex-col items-center gap-2 text-center">
<h1 className="text-2xl font-bold">Login to your account</h1>
<p className="text-balance text-sm text-muted-foreground">
Enter your email below to login to your account
</p>
</div>
<div className="grid 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 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>
<div className="relative text-center text-sm after:absolute after:inset-0 after:top-1/2 after:z-0 after:flex after:items-center after:border-t after:border-border">
<span className="relative z-10 bg-background px-2 text-muted-foreground">
Or continue with
</span>
</div>
<Button variant="outline" className="w-full">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
<path
d="M12 .297c-6.63 0-12 5.373-12 12 0 5.303 3.438 9.8 8.205 11.385.6.113.82-.258.82-.577 0-.285-.01-1.04-.015-2.04-3.338.724-4.042-1.61-4.042-1.61C4.422 18.07 3.633 17.7 3.633 17.7c-1.087-.744.084-.729.084-.729 1.205.084 1.838 1.236 1.838 1.236 1.07 1.835 2.809 1.305 3.495.998.108-.776.417-1.305.76-1.605-2.665-.3-5.466-1.332-5.466-5.93 0-1.31.465-2.38 1.235-3.22-.135-.303-.54-1.523.105-3.176 0 0 1.005-.322 3.3 1.23.96-.267 1.98-.399 3-.405 1.02.006 2.04.138 3 .405 2.28-1.552 3.285-1.23 3.285-1.23.645 1.653.24 2.873.12 3.176.765.84 1.23 1.91 1.23 3.22 0 4.61-2.805 5.625-5.475 5.92.42.36.81 1.096.81 2.22 0 1.606-.015 2.896-.015 3.286 0 .315.21.69.825.57C20.565 22.092 24 17.592 24 12.297c0-6.627-5.373-12-12-12"
fill="currentColor"
/>
</svg>
Login with GitHub
</Button>
</div>
<div className="text-center text-sm">
Don&apos;t have an account?{" "}
<a href="#" className="underline underline-offset-4">
Sign up
</a>
</div>
</form>
)
}
@@ -1,32 +0,0 @@
import { GalleryVerticalEnd } from "lucide-react"
import { LoginForm } from "./components/login-form"
export default function LoginPage() {
return (
<div className="grid min-h-svh lg:grid-cols-2">
<div className="flex flex-col gap-4 p-6 md:p-10">
<div className="flex justify-center gap-2 md:justify-start">
<a href="#" className="flex items-center gap-2 font-medium">
<div className="flex h-6 w-6 items-center justify-center rounded-md bg-primary text-primary-foreground">
<GalleryVerticalEnd className="size-4" />
</div>
Acme Inc.
</a>
</div>
<div className="flex flex-1 items-center justify-center">
<div className="w-full max-w-xs">
<LoginForm />
</div>
</div>
</div>
<div className="relative hidden bg-muted lg:block">
<img
src="/placeholder.svg"
alt="Image"
className="absolute inset-0 h-full w-full object-cover dark:brightness-[0.2] dark:grayscale"
/>
</div>
</div>
)
}
@@ -1,107 +0,0 @@
import { cn } from "../../../../../util"
import { Button } from "../../../../../primitives/button"
import {
Card,
CardContent,
CardDescription,
CardHeader,
CardTitle,
} from "../../../../../primitives/card"
import { Input } from "../../../../../primitives/input"
import { Label } from "../../../../../primitives/label"
export function LoginForm({
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 className="text-center">
<CardTitle className="text-xl">Welcome back</CardTitle>
<CardDescription>
Login with your Apple or Google account
</CardDescription>
</CardHeader>
<CardContent>
<form onSubmit={handleSubmit}>
<div className="grid gap-6">
<div className="flex flex-col gap-4">
<Button variant="outline" className="w-full">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
<path
d="M12.152 6.896c-.948 0-2.415-1.078-3.96-1.04-2.04.027-3.91 1.183-4.961 3.014-2.117 3.675-.546 9.103 1.519 12.09 1.013 1.454 2.208 3.09 3.792 3.039 1.52-.065 2.09-.987 3.935-.987 1.831 0 2.35.987 3.96.948 1.637-.026 2.676-1.48 3.676-2.948 1.156-1.688 1.636-3.325 1.662-3.415-.039-.013-3.182-1.221-3.22-4.857-.026-3.04 2.48-4.494 2.597-4.559-1.429-2.09-3.623-2.324-4.39-2.376-2-.156-3.675 1.09-4.61 1.09zM15.53 3.83c.843-1.012 1.4-2.427 1.245-3.83-1.207.052-2.662.805-3.532 1.818-.78.896-1.454 2.338-1.273 3.714 1.338.104 2.715-.688 3.559-1.701"
fill="currentColor"
/>
</svg>
Login with Apple
</Button>
<Button variant="outline" className="w-full">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
<path
d="M12.48 10.92v3.28h7.84c-.24 1.84-.853 3.187-1.787 4.133-1.147 1.147-2.933 2.4-6.053 2.4-4.827 0-8.6-3.893-8.6-8.72s3.773-8.72 8.6-8.72c2.6 0 4.507 1.027 5.907 2.347l2.307-2.307C18.747 1.44 16.133 0 12.48 0 5.867 0 .307 5.387.307 12s5.56 12 12.173 12c3.573 0 6.267-1.173 8.373-3.36 2.16-2.16 2.84-5.213 2.84-7.667 0-.76-.053-1.467-.173-2.053H12.48z"
fill="currentColor"
/>
</svg>
Login with Google
</Button>
</div>
<div className="relative text-center text-sm after:absolute after:inset-0 after:top-1/2 after:z-0 after:flex after:items-center after:border-t after:border-border">
<span className="relative z-10 bg-background px-2 text-muted-foreground">
Or continue with
</span>
</div>
<div className="grid 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 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>
</div>
<div className="text-center text-sm">
Don&apos;t have an account?{" "}
<a href="#" className="underline underline-offset-4">
Sign up
</a>
</div>
</div>
</form>
</CardContent>
</Card>
<div className="text-balance text-center text-xs text-muted-foreground [&_a]:underline [&_a]:underline-offset-4 [&_a]:hover:text-primary ">
By clicking continue, you agree to our <a href="#">Terms of Service</a>{" "}
and <a href="#">Privacy Policy</a>.
</div>
</div>
)
}
@@ -1,19 +0,0 @@
import { GalleryVerticalEnd } from "lucide-react"
import { LoginForm } from "./components/login-form"
export default function LoginPage() {
return (
<div className="flex min-h-svh flex-col items-center justify-center gap-6 bg-muted p-6 md:p-10">
<div className="flex w-full max-w-sm flex-col gap-6">
<a href="#" className="flex items-center gap-2 self-center font-medium">
<div className="flex h-6 w-6 items-center justify-center rounded-md bg-primary text-primary-foreground">
<GalleryVerticalEnd className="size-4" />
</div>
Acme Inc.
</a>
<LoginForm />
</div>
</div>
)
}
@@ -1,115 +0,0 @@
import { cn } from "../../../../../util"
import { Button } from "../../../../../primitives/button"
import { Card, CardContent } from "../../../../../primitives/card"
import { Input } from "../../../../../primitives/input"
import { Label } from "../../../../../primitives/label"
export function LoginForm({
className,
...props
}: React.ComponentProps<"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 className="overflow-hidden">
<CardContent className="grid p-0 md:grid-cols-2">
<form className="p-6 md:p-8" onSubmit={handleSubmit}>
<div className="flex flex-col gap-6">
<div className="flex flex-col items-center text-center">
<h1 className="text-2xl font-bold">Welcome back</h1>
<p className="text-balance text-muted-foreground">
Login to your Acme Inc account
</p>
</div>
<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 text-sm underline-offset-2 hover:underline"
>
Forgot your password?
</a>
</div>
<Input id="password" name="password" type="password" required />
</div>
<Button type="submit" className="w-full">
Login
</Button>
<div className="relative text-center text-sm after:absolute after:inset-0 after:top-1/2 after:z-0 after:flex after:items-center after:border-t after:border-border">
<span className="relative z-10 bg-background px-2 text-muted-foreground">
Or continue with
</span>
</div>
<div className="grid grid-cols-3 gap-4">
<Button variant="outline" className="w-full">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
<path
d="M12.152 6.896c-.948 0-2.415-1.078-3.96-1.04-2.04.027-3.91 1.183-4.961 3.014-2.117 3.675-.546 9.103 1.519 12.09 1.013 1.454 2.208 3.09 3.792 3.039 1.52-.065 2.09-.987 3.935-.987 1.831 0 2.35.987 3.96.948 1.637-.026 2.676-1.48 3.676-2.948 1.156-1.688 1.636-3.325 1.662-3.415-.039-.013-3.182-1.221-3.22-4.857-.026-3.04 2.48-4.494 2.597-4.559-1.429-2.09-3.623-2.324-4.39-2.376-2-.156-3.675 1.09-4.61 1.09zM15.53 3.83c.843-1.012 1.4-2.427 1.245-3.83-1.207.052-2.662.805-3.532 1.818-.78.896-1.454 2.338-1.273 3.714 1.338.104 2.715-.688 3.559-1.701"
fill="currentColor"
/>
</svg>
<span className="sr-only">Login with Apple</span>
</Button>
<Button variant="outline" className="w-full">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
<path
d="M12.48 10.92v3.28h7.84c-.24 1.84-.853 3.187-1.787 4.133-1.147 1.147-2.933 2.4-6.053 2.4-4.827 0-8.6-3.893-8.6-8.72s3.773-8.72 8.6-8.72c2.6 0 4.507 1.027 5.907 2.347l2.307-2.307C18.747 1.44 16.133 0 12.48 0 5.867 0 .307 5.387.307 12s5.56 12 12.173 12c3.573 0 6.267-1.173 8.373-3.36 2.16-2.16 2.84-5.213 2.84-7.667 0-.76-.053-1.467-.173-2.053H12.48z"
fill="currentColor"
/>
</svg>
<span className="sr-only">Login with Google</span>
</Button>
<Button variant="outline" className="w-full">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
<path
d="M6.915 4.03c-1.968 0-3.683 1.28-4.871 3.113C.704 9.208 0 11.883 0 14.449c0 .706.07 1.369.21 1.973a6.624 6.624 0 0 0 .265.86 5.297 5.297 0 0 0 .371.761c.696 1.159 1.818 1.927 3.593 1.927 1.497 0 2.633-.671 3.965-2.444.76-1.012 1.144-1.626 2.663-4.32l.756-1.339.186-.325c.061.1.121.196.183.3l2.152 3.595c.724 1.21 1.665 2.556 2.47 3.314 1.046.987 1.992 1.22 3.06 1.22 1.075 0 1.876-.355 2.455-.843a3.743 3.743 0 0 0 .81-.973c.542-.939.861-2.127.861-3.745 0-2.72-.681-5.357-2.084-7.45-1.282-1.912-2.957-2.93-4.716-2.93-1.047 0-2.088.467-3.053 1.308-.652.57-1.257 1.29-1.82 2.05-.69-.875-1.335-1.547-1.958-2.056-1.182-.966-2.315-1.303-3.454-1.303zm10.16 2.053c1.147 0 2.188.758 2.992 1.999 1.132 1.748 1.647 4.195 1.647 6.4 0 1.548-.368 2.9-1.839 2.9-.58 0-1.027-.23-1.664-1.004-.496-.601-1.343-1.878-2.832-4.358l-.617-1.028a44.908 44.908 0 0 0-1.255-1.98c.07-.109.141-.224.211-.327 1.12-1.667 2.118-2.602 3.358-2.602zm-10.201.553c1.265 0 2.058.791 2.675 1.446.307.327.737.871 1.234 1.579l-1.02 1.566c-.757 1.163-1.882 3.017-2.837 4.338-1.191 1.649-1.81 1.817-2.486 1.817-.524 0-1.038-.237-1.383-.794-.263-.426-.464-1.13-.464-2.046 0-2.221.63-4.535 1.66-6.088.454-.687.964-1.226 1.533-1.533a2.264 2.264 0 0 1 1.088-.285z"
fill="currentColor"
/>
</svg>
<span className="sr-only">Login with Meta</span>
</Button>
</div>
<div className="text-center text-sm">
Don&apos;t have an account?{" "}
<a href="#" className="underline underline-offset-4">
Sign up
</a>
</div>
</div>
</form>
<div className="relative hidden bg-muted md:block">
<img
src="/placeholder.svg"
alt="Image"
className="absolute inset-0 h-full w-full object-cover dark:brightness-[0.2] dark:grayscale"
/>
</div>
</CardContent>
</Card>
<div className="text-balance text-center text-xs text-muted-foreground [&_a]:underline [&_a]:underline-offset-4 hover:[&_a]:text-primary">
By clicking continue, you agree to our <a href="#">Terms of Service</a>{" "}
and <a href="#">Privacy Policy</a>.
</div>
</div>
)
}
@@ -1,11 +0,0 @@
import { LoginForm } from "./components/login-form"
export default function LoginPage() {
return (
<div className="flex min-h-svh flex-col items-center justify-center bg-muted p-6 md:p-10">
<div className="w-full max-w-sm md:max-w-3xl">
<LoginForm />
</div>
</div>
)
}
@@ -1,92 +0,0 @@
import { GalleryVerticalEnd } from "lucide-react"
import { cn } from "../../../../../util"
import { Button } from "../../../../../primitives/button"
import { Input } from "../../../../../primitives/input"
import { Label } from "../../../../../primitives/label"
export function LoginForm({
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}>
<form onSubmit={handleSubmit}>
<div className="flex flex-col gap-6">
<div className="flex flex-col items-center gap-2">
<a
href="#"
className="flex flex-col items-center gap-2 font-medium"
>
<div className="flex h-8 w-8 items-center justify-center rounded-md">
<GalleryVerticalEnd className="size-6" />
</div>
<span className="sr-only">Acme Inc.</span>
</a>
<h1 className="text-xl font-bold">Welcome to Acme Inc.</h1>
<div className="text-center text-sm">
Don&apos;t have an account?{" "}
<a href="#" className="underline underline-offset-4">
Sign up
</a>
</div>
</div>
<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>
<Button type="submit" className="w-full">
Login
</Button>
</div>
<div className="relative text-center text-sm after:absolute after:inset-0 after:top-1/2 after:z-0 after:flex after:items-center after:border-t after:border-border">
<span className="relative z-10 bg-background px-2 text-muted-foreground">
Or
</span>
</div>
<div className="grid gap-4 sm:grid-cols-2">
<Button variant="outline" className="w-full">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
<path
d="M12.152 6.896c-.948 0-2.415-1.078-3.96-1.04-2.04.027-3.91 1.183-4.961 3.014-2.117 3.675-.546 9.103 1.519 12.09 1.013 1.454 2.208 3.09 3.792 3.039 1.52-.065 2.09-.987 3.935-.987 1.831 0 2.35.987 3.96.948 1.637-.026 2.676-1.48 3.676-2.948 1.156-1.688 1.636-3.325 1.662-3.415-.039-.013-3.182-1.221-3.22-4.857-.026-3.04 2.48-4.494 2.597-4.559-1.429-2.09-3.623-2.324-4.39-2.376-2-.156-3.675 1.09-4.61 1.09zM15.53 3.83c.843-1.012 1.4-2.427 1.245-3.83-1.207.052-2.662.805-3.532 1.818-.78.896-1.454 2.338-1.273 3.714 1.338.104 2.715-.688 3.559-1.701"
fill="currentColor"
/>
</svg>
Continue with Apple
</Button>
<Button variant="outline" className="w-full">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
<path
d="M12.48 10.92v3.28h7.84c-.24 1.84-.853 3.187-1.787 4.133-1.147 1.147-2.933 2.4-6.053 2.4-4.827 0-8.6-3.893-8.6-8.72s3.773-8.72 8.6-8.72c2.6 0 4.507 1.027 5.907 2.347l2.307-2.307C18.747 1.44 16.133 0 12.48 0 5.867 0 .307 5.387.307 12s5.56 12 12.173 12c3.573 0 6.267-1.173 8.373-3.36 2.16-2.16 2.84-5.213 2.84-7.667 0-.76-.053-1.467-.173-2.053H12.48z"
fill="currentColor"
/>
</svg>
Continue with Google
</Button>
</div>
</div>
</form>
<div className="text-balance text-center text-xs text-muted-foreground [&_a]:underline [&_a]:underline-offset-4 hover:[&_a]:text-primary ">
By clicking continue, you agree to our <a href="#">Terms of Service</a>{" "}
and <a href="#">Privacy Policy</a>.
</div>
</div>
)
}
@@ -1,11 +0,0 @@
import { LoginForm } from "./components/login-form"
export default function LoginPage() {
return (
<div className="flex min-h-svh flex-col items-center justify-center gap-6 bg-background p-6 md:p-10">
<div className="w-full max-w-sm">
<LoginForm />
</div>
</div>
)
}
-6
View File
@@ -1,6 +0,0 @@
// OTP Blocks
export { default as Otp01 } from './otp-01/page'
export { default as Otp02 } from './otp-02/page'
export { default as Otp03 } from './otp-03/page'
export { default as Otp04 } from './otp-04/page'
export { default as Otp05 } from './otp-05/page'
@@ -1,68 +0,0 @@
import { Button } from "../../../../../primitives/button"
import {
Card,
CardContent,
CardDescription,
CardHeader,
CardTitle,
} from "../../../../../primitives/card"
import {
Field,
FieldDescription,
FieldGroup,
FieldLabel,
} from "../../../../../primitives/field"
import {
InputOTP,
InputOTPGroup,
InputOTPSlot,
} from "../../../../../primitives/input-otp"
export function OTPForm({ ...props }: React.ComponentProps<typeof Card>) {
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 (
<Card {...props}>
<CardHeader>
<CardTitle>Enter verification code</CardTitle>
<CardDescription>We sent a 6-digit code to your email.</CardDescription>
</CardHeader>
<CardContent>
<form onSubmit={handleSubmit}>
<FieldGroup>
<Field>
<FieldLabel htmlFor="otp">Verification code</FieldLabel>
<InputOTP maxLength={6} id="otp" name="otp" required>
<InputOTPGroup className="gap-2.5 *:data-[slot=input-otp-slot]:rounded-md *:data-[slot=input-otp-slot]:border">
<InputOTPSlot index={0} />
<InputOTPSlot index={1} />
<InputOTPSlot index={2} />
<InputOTPSlot index={3} />
<InputOTPSlot index={4} />
<InputOTPSlot index={5} />
</InputOTPGroup>
</InputOTP>
<FieldDescription>
Enter the 6-digit code sent to your email.
</FieldDescription>
</Field>
<FieldGroup>
<Button type="submit">Verify</Button>
<FieldDescription className="text-center">
Didn&apos;t receive the code? <a href="#">Resend</a>
</FieldDescription>
</FieldGroup>
</FieldGroup>
</form>
</CardContent>
</Card>
)
}
-11
View File
@@ -1,11 +0,0 @@
import { OTPForm } from "./components/otp-form"
export default function OTPPage() {
return (
<div className="flex min-h-svh w-full items-center justify-center p-6 md:p-10">
<div className="w-full max-w-xs">
<OTPForm />
</div>
</div>
)
}
@@ -1,69 +0,0 @@
import { cn } from "../../../../../util"
import { Button } from "../../../../../primitives/button"
import {
Field,
FieldDescription,
FieldGroup,
FieldLabel,
} from "../../../../../primitives/field"
import {
InputOTP,
InputOTPGroup,
InputOTPSeparator,
InputOTPSlot,
} from "../../../../../primitives/input-otp"
export function OTPForm({ className, ...props }: React.ComponentProps<"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}>
<form onSubmit={handleSubmit}>
<FieldGroup>
<div className="flex flex-col items-center gap-1 text-center">
<h1 className="text-2xl font-bold">Enter verification code</h1>
<p className="text-muted-foreground text-sm text-balance">
We sent a 6-digit code to your email.
</p>
</div>
<Field>
<FieldLabel htmlFor="otp" className="sr-only">
Verification code
</FieldLabel>
<InputOTP maxLength={6} id="otp" name="otp" required>
<InputOTPGroup className="gap-2 *:data-[slot=input-otp-slot]:rounded-md *:data-[slot=input-otp-slot]:border">
<InputOTPSlot index={0} />
<InputOTPSlot index={1} />
</InputOTPGroup>
<InputOTPSeparator />
<InputOTPGroup className="gap-2 *:data-[slot=input-otp-slot]:rounded-md *:data-[slot=input-otp-slot]:border">
<InputOTPSlot index={2} />
<InputOTPSlot index={3} />
</InputOTPGroup>
<InputOTPSeparator />
<InputOTPGroup className="gap-2 *:data-[slot=input-otp-slot]:rounded-md *:data-[slot=input-otp-slot]:border">
<InputOTPSlot index={4} />
<InputOTPSlot index={5} />
</InputOTPGroup>
</InputOTP>
<FieldDescription className="text-center">
Enter the 6-digit code sent to your email.
</FieldDescription>
</Field>
<Button type="submit">Verify</Button>
<FieldDescription className="text-center">
Didn&apos;t receive the code? <a href="#">Resend</a>
</FieldDescription>
</FieldGroup>
</form>
</div>
)
}
-22
View File
@@ -1,22 +0,0 @@
import { OTPForm } from "./components/otp-form"
export default function OTPPage() {
return (
<div className="flex min-h-svh w-full">
<div className="flex w-full items-center justify-center p-6 lg:w-1/2">
<div className="w-full max-w-xs">
<OTPForm />
</div>
</div>
<div className="relative hidden w-1/2 lg:block">
<img
alt="Authentication"
className="absolute inset-0 h-full w-full object-cover"
height={1080}
src="/placeholder.svg"
width={1920}
/>
</div>
</div>
)
}
@@ -1,68 +0,0 @@
import { Button } from "../../../../../primitives/button"
import {
Card,
CardContent,
CardDescription,
CardHeader,
CardTitle,
} from "../../../../../primitives/card"
import {
Field,
FieldDescription,
FieldGroup,
FieldLabel,
} from "../../../../../primitives/field"
import {
InputOTP,
InputOTPGroup,
InputOTPSlot,
} from "../../../../../primitives/input-otp"
export function OTPForm({ ...props }: React.ComponentProps<typeof Card>) {
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 (
<Card {...props}>
<CardHeader className="text-center">
<CardTitle className="text-xl">Enter verification code</CardTitle>
<CardDescription>We sent a 6-digit code to your email.</CardDescription>
</CardHeader>
<CardContent>
<form onSubmit={handleSubmit}>
<FieldGroup>
<Field>
<FieldLabel htmlFor="otp" className="sr-only">
Verification code
</FieldLabel>
<InputOTP maxLength={6} id="otp" name="otp" required>
<InputOTPGroup className="gap-2.5 *:data-[slot=input-otp-slot]:rounded-md *:data-[slot=input-otp-slot]:border">
<InputOTPSlot index={0} />
<InputOTPSlot index={1} />
<InputOTPSlot index={2} />
<InputOTPSlot index={3} />
<InputOTPSlot index={4} />
<InputOTPSlot index={5} />
</InputOTPGroup>
</InputOTP>
<FieldDescription className="text-center">
Enter the 6-digit code sent to your email.
</FieldDescription>
</Field>
<Button type="submit">Verify</Button>
<FieldDescription className="text-center">
Didn&apos;t receive the code? <a href="#">Resend</a>
</FieldDescription>
</FieldGroup>
</form>
</CardContent>
</Card>
)
}
-19
View File
@@ -1,19 +0,0 @@
import { GalleryVerticalEnd } from "lucide-react"
import { OTPForm } from "./components/otp-form"
export default function OTPPage() {
return (
<div className="bg-muted flex min-h-svh flex-col items-center justify-center gap-6 p-6 md:p-10">
<div className="flex w-full max-w-xs flex-col gap-6">
<a href="#" className="flex items-center gap-2 self-center font-medium">
<div className="bg-primary text-primary-foreground flex size-6 items-center justify-center rounded-md">
<GalleryVerticalEnd className="size-4" />
</div>
Acme Inc.
</a>
<OTPForm />
</div>
</div>
)
}
@@ -1,93 +0,0 @@
import { cn } from "../../../../../util"
import { Button } from "../../../../../primitives/button"
import { Card, CardContent } from "../../../../../primitives/card"
import {
Field,
FieldDescription,
FieldGroup,
FieldLabel,
} from "../../../../../primitives/field"
import {
InputOTP,
InputOTPGroup,
InputOTPSeparator,
InputOTPSlot,
} from "../../../../../primitives/input-otp"
export function OTPForm({ className, ...props }: React.ComponentProps<"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 md:min-h-[450px]", className)}
{...props}
>
<Card className="flex-1 overflow-hidden p-0">
<CardContent className="grid flex-1 p-0 md:grid-cols-2">
<form className="flex flex-col items-center justify-center p-6 md:p-8" onSubmit={handleSubmit}>
<FieldGroup>
<Field className="items-center text-center">
<h1 className="text-2xl font-bold">Enter verification code</h1>
<p className="text-muted-foreground text-sm text-balance">
We sent a 6-digit code to your email
</p>
</Field>
<Field>
<FieldLabel htmlFor="otp" className="sr-only">
Verification code
</FieldLabel>
<InputOTP
maxLength={6}
id="otp"
name="otp"
required
containerClassName="gap-4"
>
<InputOTPGroup>
<InputOTPSlot index={0} />
<InputOTPSlot index={1} />
<InputOTPSlot index={2} />
</InputOTPGroup>
<InputOTPSeparator />
<InputOTPGroup>
<InputOTPSlot index={3} />
<InputOTPSlot index={4} />
<InputOTPSlot index={5} />
</InputOTPGroup>
</InputOTP>
<FieldDescription className="text-center">
Enter the 6-digit code sent to your email.
</FieldDescription>
</Field>
<Field>
<Button type="submit">Verify</Button>
<FieldDescription className="text-center">
Didn&apos;t receive the code? <a href="#">Resend</a>
</FieldDescription>
</Field>
</FieldGroup>
</form>
<div className="bg-muted relative hidden md:block">
<img
src="/placeholder.svg"
alt="Image"
className="absolute inset-0 h-full w-full object-cover dark:brightness-[0.2] dark:grayscale"
/>
</div>
</CardContent>
</Card>
<FieldDescription className="text-center">
By clicking continue, you agree to our <a href="#">Terms of Service</a>{" "}
and <a href="#">Privacy Policy</a>.
</FieldDescription>
</div>
)
}
-11
View File
@@ -1,11 +0,0 @@
import { OTPForm } from "./components/otp-form"
export default function OTPPage() {
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 md:max-w-3xl">
<OTPForm />
</div>
</div>
)
}
@@ -1,86 +0,0 @@
import { GalleryVerticalEnd } from "lucide-react"
import { cn } from "../../../../../util"
import { Button } from "../../../../../primitives/button"
import {
Field,
FieldDescription,
FieldGroup,
FieldLabel,
} from "../../../../../primitives/field"
import {
InputOTP,
InputOTPGroup,
InputOTPSeparator,
InputOTPSlot,
} from "../../../../../primitives/input-otp"
export function OTPForm({ className, ...props }: React.ComponentProps<"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}>
<form onSubmit={handleSubmit}>
<FieldGroup>
<div className="flex flex-col items-center gap-2 text-center">
<a
href="#"
className="flex flex-col items-center gap-2 font-medium"
>
<div className="flex size-8 items-center justify-center rounded-md">
<GalleryVerticalEnd className="size-6" />
</div>
<span className="sr-only">Acme Inc.</span>
</a>
<h1 className="text-xl font-bold">Enter verification code</h1>
<FieldDescription>
We sent a 6-digit code to your email address
</FieldDescription>
</div>
<Field>
<FieldLabel htmlFor="otp" className="sr-only">
Verification code
</FieldLabel>
<InputOTP
maxLength={6}
id="otp"
name="otp"
required
containerClassName="gap-4"
>
<InputOTPGroup className="gap-2.5 *:data-[slot=input-otp-slot]:h-16 *:data-[slot=input-otp-slot]:w-12 *:data-[slot=input-otp-slot]:rounded-md *:data-[slot=input-otp-slot]:border *:data-[slot=input-otp-slot]:text-xl">
<InputOTPSlot index={0} />
<InputOTPSlot index={1} />
<InputOTPSlot index={2} />
</InputOTPGroup>
<InputOTPSeparator />
<InputOTPGroup className="gap-2.5 *:data-[slot=input-otp-slot]:h-16 *:data-[slot=input-otp-slot]:w-12 *:data-[slot=input-otp-slot]:rounded-md *:data-[slot=input-otp-slot]:border *:data-[slot=input-otp-slot]:text-xl">
<InputOTPSlot index={3} />
<InputOTPSlot index={4} />
<InputOTPSlot index={5} />
</InputOTPGroup>
</InputOTP>
<FieldDescription className="text-center">
Didn&apos;t receive the code? <a href="#">Resend</a>
</FieldDescription>
</Field>
<Field>
<Button type="submit">Verify</Button>
</Field>
</FieldGroup>
</form>
<FieldDescription className="px-6 text-center">
By clicking continue, you agree to our <a href="#">Terms of Service</a>{" "}
and <a href="#">Privacy Policy</a>.
</FieldDescription>
</div>
)
}
-11
View File
@@ -1,11 +0,0 @@
import { OTPForm } from "./components/otp-form"
export default function OTPPage() {
return (
<div className="bg-background flex min-h-svh flex-col items-center justify-center gap-6 p-6 md:p-10">
<div className="w-full max-w-sm">
<OTPForm />
</div>
</div>
)
}
-6
View File
@@ -1,6 +0,0 @@
// Signup Blocks
export { default as Signup01 } from './signup-01/page'
export { default as Signup02 } from './signup-02/page'
export { default as Signup03 } from './signup-03/page'
export { default as Signup04 } from './signup-04/page'
export { default as Signup05 } from './signup-05/page'
@@ -1,87 +0,0 @@
import { Button } from "../../../../../primitives/button"
import {
Card,
CardContent,
CardDescription,
CardHeader,
CardTitle,
} from "../../../../../primitives/card"
import {
Field,
FieldDescription,
FieldGroup,
FieldLabel,
} from "../../../../../primitives/field"
import { Input } from "../../../../../primitives/input"
export function SignupForm({ ...props }: React.ComponentProps<typeof Card>) {
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 (
<Card {...props}>
<CardHeader>
<CardTitle>Create an account</CardTitle>
<CardDescription>
Enter your information below to create your account
</CardDescription>
</CardHeader>
<CardContent>
<form onSubmit={handleSubmit}>
<FieldGroup>
<Field>
<FieldLabel htmlFor="name">Full Name</FieldLabel>
<Input id="name" name="name" type="text" placeholder="John Doe" required />
</Field>
<Field>
<FieldLabel htmlFor="email">Email</FieldLabel>
<Input
id="email"
name="email"
type="email"
placeholder="m@example.com"
required
/>
<FieldDescription>
We&apos;ll use this to contact you. We will not share your email
with anyone else.
</FieldDescription>
</Field>
<Field>
<FieldLabel htmlFor="password">Password</FieldLabel>
<Input id="password" name="password" type="password" required />
<FieldDescription>
Must be at least 8 characters long.
</FieldDescription>
</Field>
<Field>
<FieldLabel htmlFor="confirm-password">
Confirm Password
</FieldLabel>
<Input id="confirm-password" name="confirm-password" type="password" required />
<FieldDescription>Please confirm your password.</FieldDescription>
</Field>
<FieldGroup>
<Field>
<Button type="submit">Create Account</Button>
<Button variant="outline" type="button">
Sign up with Google
</Button>
<FieldDescription className="px-6 text-center">
Already have an account? <a href="#">Sign in</a>
</FieldDescription>
</Field>
</FieldGroup>
</FieldGroup>
</form>
</CardContent>
</Card>
)
}
@@ -1,11 +0,0 @@
import { SignupForm } from "./components/signup-form"
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">
<SignupForm />
</div>
</div>
)
}
@@ -1,80 +0,0 @@
import { cn } from "../../../../../util"
import { Button } from "../../../../../primitives/button"
import {
Field,
FieldDescription,
FieldGroup,
FieldLabel,
FieldSeparator,
} from "../../../../../primitives/field"
import { Input } from "../../../../../primitives/input"
export function SignupForm({
className,
...props
}: React.ComponentProps<"form">) {
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 (
<form className={cn("flex flex-col gap-6", className)} {...props} onSubmit={handleSubmit}>
<FieldGroup>
<div className="flex flex-col items-center gap-1 text-center">
<h1 className="text-2xl font-bold">Create your account</h1>
<p className="text-muted-foreground text-sm text-balance">
Fill in the form below to create your account
</p>
</div>
<Field>
<FieldLabel htmlFor="name">Full Name</FieldLabel>
<Input id="name" name="name" type="text" placeholder="John Doe" required />
</Field>
<Field>
<FieldLabel htmlFor="email">Email</FieldLabel>
<Input id="email" name="email" type="email" placeholder="m@example.com" required />
<FieldDescription>
We&apos;ll use this to contact you. We will not share your email
with anyone else.
</FieldDescription>
</Field>
<Field>
<FieldLabel htmlFor="password">Password</FieldLabel>
<Input id="password" name="password" type="password" required />
<FieldDescription>
Must be at least 8 characters long.
</FieldDescription>
</Field>
<Field>
<FieldLabel htmlFor="confirm-password">Confirm Password</FieldLabel>
<Input id="confirm-password" name="confirm-password" type="password" required />
<FieldDescription>Please confirm your password.</FieldDescription>
</Field>
<Field>
<Button type="submit">Create Account</Button>
</Field>
<FieldSeparator>Or continue with</FieldSeparator>
<Field>
<Button variant="outline" type="button">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
<path
d="M12 .297c-6.63 0-12 5.373-12 12 0 5.303 3.438 9.8 8.205 11.385.6.113.82-.258.82-.577 0-.285-.01-1.04-.015-2.04-3.338.724-4.042-1.61-4.042-1.61C4.422 18.07 3.633 17.7 3.633 17.7c-1.087-.744.084-.729.084-.729 1.205.084 1.838 1.236 1.838 1.236 1.07 1.835 2.809 1.305 3.495.998.108-.776.417-1.305.76-1.605-2.665-.3-5.466-1.332-5.466-5.93 0-1.31.465-2.38 1.235-3.22-.135-.303-.54-1.523.105-3.176 0 0 1.005-.322 3.3 1.23.96-.267 1.98-.399 3-.405 1.02.006 2.04.138 3 .405 2.28-1.552 3.285-1.23 3.285-1.23.645 1.653.24 2.873.12 3.176.765.84 1.23 1.91 1.23 3.22 0 4.61-2.805 5.625-5.475 5.92.42.36.81 1.096.81 2.22 0 1.606-.015 2.896-.015 3.286 0 .315.21.69.825.57C20.565 22.092 24 17.592 24 12.297c0-6.627-5.373-12-12-12"
fill="currentColor"
/>
</svg>
Sign up with GitHub
</Button>
<FieldDescription className="px-6 text-center">
Already have an account? <a href="#">Sign in</a>
</FieldDescription>
</Field>
</FieldGroup>
</form>
)
}
@@ -1,32 +0,0 @@
import { GalleryVerticalEnd } from "lucide-react"
import { SignupForm } from "./components/signup-form"
export default function SignupPage() {
return (
<div className="grid min-h-svh lg:grid-cols-2">
<div className="flex flex-col gap-4 p-6 md:p-10">
<div className="flex justify-center gap-2 md:justify-start">
<a href="#" className="flex items-center gap-2 font-medium">
<div className="bg-primary text-primary-foreground flex size-6 items-center justify-center rounded-md">
<GalleryVerticalEnd className="size-4" />
</div>
Acme Inc.
</a>
</div>
<div className="flex flex-1 items-center justify-center">
<div className="w-full max-w-xs">
<SignupForm />
</div>
</div>
</div>
<div className="bg-muted relative hidden lg:block">
<img
src="/placeholder.svg"
alt="Image"
className="absolute inset-0 h-full w-full object-cover dark:brightness-[0.2] dark:grayscale"
/>
</div>
</div>
)
}
@@ -1,91 +0,0 @@
import { cn } from "../../../../../util"
import { Button } from "../../../../../primitives/button"
import {
Card,
CardContent,
CardDescription,
CardHeader,
CardTitle,
} from "../../../../../primitives/card"
import {
Field,
FieldDescription,
FieldGroup,
FieldLabel,
} from "../../../../../primitives/field"
import { Input } from "../../../../../primitives/input"
export function SignupForm({
className,
...props
}: React.ComponentProps<"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 className="text-center">
<CardTitle className="text-xl">Create your account</CardTitle>
<CardDescription>
Enter your email below to create your account
</CardDescription>
</CardHeader>
<CardContent>
<form onSubmit={handleSubmit}>
<FieldGroup>
<Field>
<FieldLabel htmlFor="name">Full Name</FieldLabel>
<Input id="name" name="name" type="text" placeholder="John Doe" required />
</Field>
<Field>
<FieldLabel htmlFor="email">Email</FieldLabel>
<Input
id="email"
name="email"
type="email"
placeholder="m@example.com"
required
/>
</Field>
<Field>
<Field className="grid grid-cols-2 gap-4">
<Field>
<FieldLabel htmlFor="password">Password</FieldLabel>
<Input id="password" name="password" type="password" required />
</Field>
<Field>
<FieldLabel htmlFor="confirm-password">
Confirm Password
</FieldLabel>
<Input id="confirm-password" name="confirm-password" type="password" required />
</Field>
</Field>
<FieldDescription>
Must be at least 8 characters long.
</FieldDescription>
</Field>
<Field>
<Button type="submit">Create Account</Button>
<FieldDescription className="text-center">
Already have an account? <a href="#">Sign in</a>
</FieldDescription>
</Field>
</FieldGroup>
</form>
</CardContent>
</Card>
<FieldDescription className="px-6 text-center">
By clicking continue, you agree to our <a href="#">Terms of Service</a>{" "}
and <a href="#">Privacy Policy</a>.
</FieldDescription>
</div>
)
}
@@ -1,19 +0,0 @@
import { GalleryVerticalEnd } from "lucide-react"
import { SignupForm } from "./components/signup-form"
export default function SignupPage() {
return (
<div className="bg-muted flex min-h-svh flex-col items-center justify-center gap-6 p-6 md:p-10">
<div className="flex w-full max-w-sm flex-col gap-6">
<a href="#" className="flex items-center gap-2 self-center font-medium">
<div className="bg-primary text-primary-foreground flex size-6 items-center justify-center rounded-md">
<GalleryVerticalEnd className="size-4" />
</div>
Acme Inc.
</a>
<SignupForm />
</div>
</div>
)
}
@@ -1,125 +0,0 @@
import { cn } from "../../../../../util"
import { Button } from "../../../../../primitives/button"
import { Card, CardContent } from "../../../../../primitives/card"
import {
Field,
FieldDescription,
FieldGroup,
FieldLabel,
FieldSeparator,
} from "../../../../../primitives/field"
import { Input } from "../../../../../primitives/input"
export function SignupForm({
className,
...props
}: React.ComponentProps<"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 className="overflow-hidden p-0">
<CardContent className="grid p-0 md:grid-cols-2">
<form className="p-6 md:p-8" onSubmit={handleSubmit}>
<FieldGroup>
<div className="flex flex-col items-center gap-2 text-center">
<h1 className="text-2xl font-bold">Create your account</h1>
<p className="text-muted-foreground text-sm text-balance">
Enter your email below to create your account
</p>
</div>
<Field>
<FieldLabel htmlFor="email">Email</FieldLabel>
<Input
id="email"
name="email"
type="email"
placeholder="m@example.com"
required
/>
<FieldDescription>
We&apos;ll use this to contact you. We will not share your
email with anyone else.
</FieldDescription>
</Field>
<Field>
<Field className="grid grid-cols-2 gap-4">
<Field>
<FieldLabel htmlFor="password">Password</FieldLabel>
<Input id="password" name="password" type="password" required />
</Field>
<Field>
<FieldLabel htmlFor="confirm-password">
Confirm Password
</FieldLabel>
<Input id="confirm-password" name="confirm-password" type="password" required />
</Field>
</Field>
<FieldDescription>
Must be at least 8 characters long.
</FieldDescription>
</Field>
<Field>
<Button type="submit">Create Account</Button>
</Field>
<FieldSeparator className="*:data-[slot=field-separator-content]:bg-card">
Or continue with
</FieldSeparator>
<Field className="grid grid-cols-3 gap-4">
<Button variant="outline" type="button">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
<path
d="M12.152 6.896c-.948 0-2.415-1.078-3.96-1.04-2.04.027-3.91 1.183-4.961 3.014-2.117 3.675-.546 9.103 1.519 12.09 1.013 1.454 2.208 3.09 3.792 3.039 1.52-.065 2.09-.987 3.935-.987 1.831 0 2.35.987 3.96.948 1.637-.026 2.676-1.48 3.676-2.948 1.156-1.688 1.636-3.325 1.662-3.415-.039-.013-3.182-1.221-3.22-4.857-.026-3.04 2.48-4.494 2.597-4.559-1.429-2.09-3.623-2.324-4.39-2.376-2-.156-3.675 1.09-4.61 1.09zM15.53 3.83c.843-1.012 1.4-2.427 1.245-3.83-1.207.052-2.662.805-3.532 1.818-.78.896-1.454 2.338-1.273 3.714 1.338.104 2.715-.688 3.559-1.701"
fill="currentColor"
/>
</svg>
<span className="sr-only">Sign up with Apple</span>
</Button>
<Button variant="outline" type="button">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
<path
d="M12.48 10.92v3.28h7.84c-.24 1.84-.853 3.187-1.787 4.133-1.147 1.147-2.933 2.4-6.053 2.4-4.827 0-8.6-3.893-8.6-8.72s3.773-8.72 8.6-8.72c2.6 0 4.507 1.027 5.907 2.347l2.307-2.307C18.747 1.44 16.133 0 12.48 0 5.867 0 .307 5.387.307 12s5.56 12 12.173 12c3.573 0 6.267-1.173 8.373-3.36 2.16-2.16 2.84-5.213 2.84-7.667 0-.76-.053-1.467-.173-2.053H12.48z"
fill="currentColor"
/>
</svg>
<span className="sr-only">Sign up with Google</span>
</Button>
<Button variant="outline" type="button">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
<path
d="M6.915 4.03c-1.968 0-3.683 1.28-4.871 3.113C.704 9.208 0 11.883 0 14.449c0 .706.07 1.369.21 1.973a6.624 6.624 0 0 0 .265.86 5.297 5.297 0 0 0 .371.761c.696 1.159 1.818 1.927 3.593 1.927 1.497 0 2.633-.671 3.965-2.444.76-1.012 1.144-1.626 2.663-4.32l.756-1.339.186-.325c.061.1.121.196.183.3l2.152 3.595c.724 1.21 1.665 2.556 2.47 3.314 1.046.987 1.992 1.22 3.06 1.22 1.075 0 1.876-.355 2.455-.843a3.743 3.743 0 0 0 .81-.973c.542-.939.861-2.127.861-3.745 0-2.72-.681-5.357-2.084-7.45-1.282-1.912-2.957-2.93-4.716-2.93-1.047 0-2.088.467-3.053 1.308-.652.57-1.257 1.29-1.82 2.05-.69-.875-1.335-1.547-1.958-2.056-1.182-.966-2.315-1.303-3.454-1.303zm10.16 2.053c1.147 0 2.188.758 2.992 1.999 1.132 1.748 1.647 4.195 1.647 6.4 0 1.548-.368 2.9-1.839 2.9-.58 0-1.027-.23-1.664-1.004-.496-.601-1.343-1.878-2.832-4.358l-.617-1.028a44.908 44.908 0 0 0-1.255-1.98c.07-.109.141-.224.211-.327 1.12-1.667 2.118-2.602 3.358-2.602zm-10.201.553c1.265 0 2.058.791 2.675 1.446.307.327.737.871 1.234 1.579l-1.02 1.566c-.757 1.163-1.882 3.017-2.837 4.338-1.191 1.649-1.81 1.817-2.486 1.817-.524 0-1.038-.237-1.383-.794-.263-.426-.464-1.13-.464-2.046 0-2.221.63-4.535 1.66-6.088.454-.687.964-1.226 1.533-1.533a2.264 2.264 0 0 1 1.088-.285z"
fill="currentColor"
/>
</svg>
<span className="sr-only">Sign up with Meta</span>
</Button>
</Field>
<FieldDescription className="text-center">
Already have an account? <a href="#">Sign in</a>
</FieldDescription>
</FieldGroup>
</form>
<div className="bg-muted relative hidden md:block">
<img
src="/placeholder.svg"
alt="Image"
className="absolute inset-0 h-full w-full object-cover dark:brightness-[0.2] dark:grayscale"
/>
</div>
</CardContent>
</Card>
<FieldDescription className="px-6 text-center">
By clicking continue, you agree to our <a href="#">Terms of Service</a>{" "}
and <a href="#">Privacy Policy</a>.
</FieldDescription>
</div>
)
}
@@ -1,11 +0,0 @@
import { SignupForm } from "./components/signup-form"
export default function SignupPage() {
return (
<div className="bg-muted flex min-h-svh flex-col items-center justify-center p-6 md:p-10">
<div className="w-full max-w-sm md:max-w-4xl">
<SignupForm />
</div>
</div>
)
}
@@ -1,89 +0,0 @@
import { GalleryVerticalEnd } from "lucide-react"
import { cn } from "../../../../../util"
import { Button } from "../../../../../primitives/button"
import {
Field,
FieldDescription,
FieldGroup,
FieldLabel,
FieldSeparator,
} from "../../../../../primitives/field"
import { Input } from "../../../../../primitives/input"
export function SignupForm({
className,
...props
}: React.ComponentProps<"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}>
<form onSubmit={handleSubmit}>
<FieldGroup>
<div className="flex flex-col items-center gap-2 text-center">
<a
href="#"
className="flex flex-col items-center gap-2 font-medium"
>
<div className="flex size-8 items-center justify-center rounded-md">
<GalleryVerticalEnd className="size-6" />
</div>
<span className="sr-only">Acme Inc.</span>
</a>
<h1 className="text-xl font-bold">Welcome to Acme Inc.</h1>
<FieldDescription>
Already have an account? <a href="#">Sign in</a>
</FieldDescription>
</div>
<Field>
<FieldLabel htmlFor="email">Email</FieldLabel>
<Input
id="email"
name="email"
type="email"
placeholder="m@example.com"
required
/>
</Field>
<Field>
<Button type="submit">Create Account</Button>
</Field>
<FieldSeparator>Or</FieldSeparator>
<Field className="grid gap-4 sm:grid-cols-2">
<Button variant="outline" type="button">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
<path
d="M12.152 6.896c-.948 0-2.415-1.078-3.96-1.04-2.04.027-3.91 1.183-4.961 3.014-2.117 3.675-.546 9.103 1.519 12.09 1.013 1.454 2.208 3.09 3.792 3.039 1.52-.065 2.09-.987 3.935-.987 1.831 0 2.35.987 3.96.948 1.637-.026 2.676-1.48 3.676-2.948 1.156-1.688 1.636-3.325 1.662-3.415-.039-.013-3.182-1.221-3.22-4.857-.026-3.04 2.48-4.494 2.597-4.559-1.429-2.09-3.623-2.324-4.39-2.376-2-.156-3.675 1.09-4.61 1.09zM15.53 3.83c.843-1.012 1.4-2.427 1.245-3.83-1.207.052-2.662.805-3.532 1.818-.78.896-1.454 2.338-1.273 3.714 1.338.104 2.715-.688 3.559-1.701"
fill="currentColor"
/>
</svg>
Continue with Apple
</Button>
<Button variant="outline" type="button">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
<path
d="M12.48 10.92v3.28h7.84c-.24 1.84-.853 3.187-1.787 4.133-1.147 1.147-2.933 2.4-6.053 2.4-4.827 0-8.6-3.893-8.6-8.72s3.773-8.72 8.6-8.72c2.6 0 4.507 1.027 5.907 2.347l2.307-2.307C18.747 1.44 16.133 0 12.48 0 5.867 0 .307 5.387.307 12s5.56 12 12.173 12c3.573 0 6.267-1.173 8.373-3.36 2.16-2.16 2.84-5.213 2.84-7.667 0-.76-.053-1.467-.173-2.053H12.48z"
fill="currentColor"
/>
</svg>
Continue with Google
</Button>
</Field>
</FieldGroup>
</form>
<FieldDescription className="px-6 text-center">
By clicking continue, you agree to our <a href="#">Terms of Service</a>{" "}
and <a href="#">Privacy Policy</a>.
</FieldDescription>
</div>
)
}
@@ -1,11 +0,0 @@
import { SignupForm } from "./components/signup-form"
export default function SignupPage() {
return (
<div className="bg-background flex min-h-svh flex-col items-center justify-center gap-6 p-6 md:p-10">
<div className="w-full max-w-sm">
<SignupForm />
</div>
</div>
)
}
-21
View File
@@ -1,21 +0,0 @@
"use client"
import * as React from "react"
import Calendar from "../../primitives/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"
/>
)
}
-22
View File
@@ -1,22 +0,0 @@
"use client"
import * as React from "react"
import Calendar from "../../primitives/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"
/>
)
}
-25
View File
@@ -1,25 +0,0 @@
"use client"
import * as React from "react"
import Calendar from "../../primitives/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"
/>
)
}
-23
View File
@@ -1,23 +0,0 @@
"use client"
import * as React from "react"
import { type DateRange } from "react-day-picker"
import Calendar from "../../primitives/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"
/>
)
}
-24
View File
@@ -1,24 +0,0 @@
"use client"
import * as React from "react"
import { type DateRange } from "react-day-picker"
import Calendar from "../../primitives/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"
/>
)
}
-30
View File
@@ -1,30 +0,0 @@
"use client"
import * as React from "react"
import { type DateRange } from "react-day-picker"
import Calendar from "../../primitives/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>
)
}
-31
View File
@@ -1,31 +0,0 @@
"use client"
import * as React from "react"
import { type DateRange } from "react-day-picker"
import Calendar from "../../primitives/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>
)
}
-24
View File
@@ -1,24 +0,0 @@
"use client"
import * as React from "react"
import Calendar from "../../primitives/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"
/>
)
}
-26
View File
@@ -1,26 +0,0 @@
"use client"
import * as React from "react"
import { type DateRange } from "react-day-picker"
import Calendar from "../../primitives/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
/>
)
}
-50
View File
@@ -1,50 +0,0 @@
"use client"
import * as React from "react"
import { Button } from "../../primitives/button"
import Calendar from "../../primitives/calendar"
import {
Card,
CardContent,
CardDescription,
CardHeader,
CardTitle,
} from "../../primitives/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>
)
}
-31
View File
@@ -1,31 +0,0 @@
"use client"
import * as React from "react"
import { type DateRange } from "react-day-picker"
import Calendar from "../../primitives/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>
)
}
-77
View File
@@ -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 "../../primitives/calendar"
import {
Card,
CardContent,
CardDescription,
CardHeader,
CardTitle,
} from "../../primitives/card"
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "../../primitives/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>
)
}
-58
View File
@@ -1,58 +0,0 @@
"use client"
import * as React from "react"
import Calendar from "../../primitives/calendar"
import { Label } from "../../primitives/label"
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "../../primitives/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>
)
}
-32
View File
@@ -1,32 +0,0 @@
"use client"
import * as React from "react"
import Calendar from "../../primitives/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"
/>
)
}
-22
View File
@@ -1,22 +0,0 @@
"use client"
import * as React from "react"
import Calendar from "../../primitives/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
/>
)
}
-56
View File
@@ -1,56 +0,0 @@
"use client"
import * as React from "react"
import { Clock2Icon } from "lucide-react"
import Calendar from "../../primitives/calendar"
import { Card, CardContent, CardFooter } from "../../primitives/card"
import { Input } from "../../primitives/input"
import { Label } from "../../primitives/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>
)
}
-54
View File
@@ -1,54 +0,0 @@
"use client"
import * as React from "react"
import Calendar from "../../primitives/calendar"
import { Card, CardContent, CardFooter } from "../../primitives/card"
import { Input } from "../../primitives/input"
import { Label } from "../../primitives/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>
)
}
-20
View File
@@ -1,20 +0,0 @@
"use client"
import * as React from "react"
import Calendar from "../../primitives/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]"
/>
)
}
-50
View File
@@ -1,50 +0,0 @@
"use client"
import * as React from "react"
import { addDays } from "date-fns"
import { Button } from "../../primitives/button"
import Calendar from "../../primitives/calendar"
import { Card, CardContent, CardFooter } from "../../primitives/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>
)
}
-97
View File
@@ -1,97 +0,0 @@
"use client"
import * as React from "react"
import { Button } from "../../primitives/button"
import Calendar from "../../primitives/calendar"
import { Card, CardContent, CardFooter } from "../../primitives/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>
)
}
-42
View File
@@ -1,42 +0,0 @@
"use client"
import * as React from "react"
import { DateRange } from "react-day-picker"
import { Calendar, CalendarDayButton } from "../../primitives/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 (
<CalendarDayButton day={day} modifiers={modifiers} {...props}>
{children}
{!modifiers.outside && <span>{isWeekend ? "$220" : "$100"}</span>}
</CalendarDayButton>
)
},
}}
/>
)
}
-49
View File
@@ -1,49 +0,0 @@
"use client"
import * as React from "react"
import { ChevronDownIcon } from "lucide-react"
import { Button } from "../../primitives/button"
import Calendar from "../../primitives/calendar"
import { Label } from "../../primitives/label"
import {
Popover,
PopoverContent,
PopoverTrigger,
} from "../../primitives/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>
)
}
-50
View File
@@ -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 "../../primitives/button"
import Calendar from "../../primitives/calendar"
import { Label } from "../../primitives/label"
import {
Popover,
PopoverContent,
PopoverTrigger,
} from "../../primitives/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>
)
}
-64
View File
@@ -1,64 +0,0 @@
"use client"
import * as React from "react"
import { ChevronDownIcon } from "lucide-react"
import { Button } from "../../primitives/button"
import Calendar from "../../primitives/calendar"
import { Input } from "../../primitives/input"
import { Label } from "../../primitives/label"
import {
Popover,
PopoverContent,
PopoverTrigger,
} from "../../primitives/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>
)
}
-78
View File
@@ -1,78 +0,0 @@
"use client"
import * as React from "react"
import { ChevronDownIcon } from "lucide-react"
import { Button } from "../../primitives/button"
import Calendar from "../../primitives/calendar"
import { Input } from "../../primitives/input"
import { Label } from "../../primitives/label"
import {
Popover,
PopoverContent,
PopoverTrigger,
} from "../../primitives/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>
)
}
-133
View File
@@ -1,133 +0,0 @@
"use client"
import * as React from "react"
import { ChevronDownIcon } from "lucide-react"
import { Button } from "../../primitives/button"
import Calendar from "../../primitives/calendar"
import { Input } from "../../primitives/input"
import { Label } from "../../primitives/label"
import {
Popover,
PopoverContent,
PopoverTrigger,
} from "../../primitives/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>
)
}
-177
View File
@@ -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 "../../primitives/button"
import Calendar from "../../primitives/calendar"
import {
Card,
CardContent,
CardDescription,
CardFooter,
CardHeader,
CardTitle,
} from "../../primitives/card"
import {
ChartConfig,
ChartContainer,
ChartTooltip,
ChartTooltipContent,
} from "../../primitives/chart"
import {
Popover,
PopoverContent,
PopoverTrigger,
} from "../../primitives/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>
)
}
-104
View File
@@ -1,104 +0,0 @@
"use client"
import * as React from "react"
import { CalendarIcon } from "lucide-react"
import { Button } from "../../primitives/button"
import Calendar from "../../primitives/calendar"
import { Input } from "../../primitives/input"
import { Label } from "../../primitives/label"
import {
Popover,
PopoverContent,
PopoverTrigger,
} from "../../primitives/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>
)
}
-96
View File
@@ -1,96 +0,0 @@
"use client"
import * as React from "react"
import { parseDate } from "chrono-node"
import { CalendarIcon } from "lucide-react"
import { Button } from "../../primitives/button"
import Calendar from "../../primitives/calendar"
import { Input } from "../../primitives/input"
import { Label } from "../../primitives/label"
import {
Popover,
PopoverContent,
PopoverTrigger,
} from "../../primitives/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>(
parseDate(value) || undefined
)
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 = parseDate(e.target.value)
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>
)
}
-56
View File
@@ -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 "../../primitives/button"
import Calendar from "../../primitives/calendar"
import { Label } from "../../primitives/label"
import {
Popover,
PopoverContent,
PopoverTrigger,
} from "../../primitives/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>
)
}
-80
View File
@@ -1,80 +0,0 @@
"use client"
import * as React from "react"
import { formatDateRange } from "little-date"
import { PlusIcon } from "lucide-react"
import { Button } from "../../primitives/button"
import Calendar from "../../primitives/calendar"
import { Card, CardContent, CardFooter } from "../../primitives/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>
)
}
-60
View File
@@ -1,60 +0,0 @@
"use client"
import * as React from "react"
import { CalendarPlusIcon } from "lucide-react"
import { Button } from "../../primitives/button"
import Calendar from "../../primitives/calendar"
import {
Drawer,
DrawerContent,
DrawerDescription,
DrawerHeader,
DrawerTitle,
DrawerTrigger,
} from "../../primitives/drawer"
import { Label } from "../../primitives/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>
)
}

Some files were not shown because too many files have changed in this diff Show More