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:
+1
-1
@@ -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",
|
||||
|
||||
@@ -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 +0,0 @@
|
||||
.npmrc
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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).
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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');
|
||||
@@ -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';
|
||||
@@ -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>
|
||||
);
|
||||
@@ -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';
|
||||
@@ -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
@@ -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 |
@@ -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
|
||||
@@ -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';
|
||||
@@ -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
@@ -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();
|
||||
@@ -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();
|
||||
@@ -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);
|
||||
});
|
||||
@@ -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()
|
||||
@@ -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"
|
||||
@@ -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();
|
||||
@@ -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
|
||||
@@ -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"
|
||||
@@ -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();
|
||||
@@ -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')
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -1,4 +0,0 @@
|
||||
// Auth Blocks
|
||||
export * from './login'
|
||||
export * from './signup'
|
||||
export * from './otp'
|
||||
@@ -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'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'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'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'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'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>
|
||||
)
|
||||
}
|
||||
@@ -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't receive the code? <a href="#">Resend</a>
|
||||
</FieldDescription>
|
||||
</FieldGroup>
|
||||
</FieldGroup>
|
||||
</form>
|
||||
</CardContent>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
@@ -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't receive the code? <a href="#">Resend</a>
|
||||
</FieldDescription>
|
||||
</FieldGroup>
|
||||
</form>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -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't receive the code? <a href="#">Resend</a>
|
||||
</FieldDescription>
|
||||
</FieldGroup>
|
||||
</form>
|
||||
</CardContent>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
@@ -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'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>
|
||||
)
|
||||
}
|
||||
@@ -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'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>
|
||||
)
|
||||
}
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
@@ -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'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'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'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>
|
||||
)
|
||||
}
|
||||
@@ -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"
|
||||
/>
|
||||
)
|
||||
}
|
||||
@@ -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"
|
||||
/>
|
||||
)
|
||||
}
|
||||
@@ -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"
|
||||
/>
|
||||
)
|
||||
}
|
||||
@@ -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"
|
||||
/>
|
||||
)
|
||||
}
|
||||
@@ -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"
|
||||
/>
|
||||
)
|
||||
}
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
@@ -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"
|
||||
/>
|
||||
)
|
||||
}
|
||||
@@ -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
|
||||
/>
|
||||
)
|
||||
}
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
@@ -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"
|
||||
/>
|
||||
)
|
||||
}
|
||||
@@ -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
|
||||
/>
|
||||
)
|
||||
}
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
@@ -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]"
|
||||
/>
|
||||
)
|
||||
}
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
@@ -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>
|
||||
)
|
||||
},
|
||||
}}
|
||||
/>
|
||||
)
|
||||
}
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
@@ -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
Reference in New Issue
Block a user