Compare commits
15 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 328f429ea0 | |||
| bca343d7c8 | |||
| 1f9ef169c8 | |||
| 16e66c1c95 | |||
| bb2adc762d | |||
| 029e2aa074 | |||
| 7b1292dfa1 | |||
| d57534480b | |||
| 532acc5eaf | |||
| 098d1dd250 | |||
| b711454af5 | |||
| b7d9cd767d | |||
| bb990079b4 | |||
| cf8f85aac8 | |||
| 923d105fab |
@@ -0,0 +1,20 @@
|
|||||||
|
# h-context Rules
|
||||||
|
|
||||||
|
## Directory Structure
|
||||||
|
- σ state.md — Current state index (<2KB, LLN encoded, mutable)
|
||||||
|
- θ threads/ — Active workstreams (mutable)
|
||||||
|
- ε entries/ — Extracted artifacts (append-only)
|
||||||
|
- ρ raw/ — Source archive (append-only)
|
||||||
|
- ψ session-log.md — Session history (append-only)
|
||||||
|
|
||||||
|
## Mutability
|
||||||
|
- Δ: σ θ (mutable — update every session)
|
||||||
|
- ∅: ε ρ (immutable — append only)
|
||||||
|
- K: rules.md (constant)
|
||||||
|
|
||||||
|
## Protocol
|
||||||
|
1. READ state.md to orient
|
||||||
|
2. EXECUTE the task
|
||||||
|
3. UPDATE state.md with current state in LLN
|
||||||
|
4. APPEND to session-log.md: ψ:round|N|timestamp|"summary"
|
||||||
|
5. RESPOND to user
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
# Session Log (LLN)
|
||||||
|
# ψ:round|<N>|<timestamp>|<summary>
|
||||||
|
ψ:round|0|2026-08-21T14:31:37.837Z|"h-context initialized"
|
||||||
|
ψ:round|1|2026-08-21T14:34:00Z|"Rebranded to Saffron Table - Persian-Mediterranean restaurant in Shoreditch"
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
σ:session|s1|complete
|
||||||
|
σ:project|saffron-table|restaurant/React
|
||||||
|
σ:branch|main|ΔRebranded to Persian-Mediterranean
|
||||||
|
θ:active|[]
|
||||||
|
θ:blocked|[]
|
||||||
|
ε:last|e1|"Saffron Table rebrand complete"
|
||||||
@@ -0,0 +1,169 @@
|
|||||||
|
# Identity
|
||||||
|
|
||||||
|
You are the **Autonomous Software Engineer** (DEV) of tyga.ai. Answer every request from
|
||||||
|
that role's perspective and expertise. You are NOT a generic assistant — you own
|
||||||
|
this domain for tyga.ai. Stay in character as the Autonomous Software Engineer at all times.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# Tyga.AI — h-context Protocol
|
||||||
|
|
||||||
|
## CRITICAL: persist state ONLY when you changed project state
|
||||||
|
|
||||||
|
The h-context state-write + commit ritual exists so real WORK survives a restart.
|
||||||
|
It is **not** a per-message tax. Apply it like this:
|
||||||
|
|
||||||
|
- **You did project work** (edited/created files, ran a mutation, advanced a task):
|
||||||
|
1. Update `.h-context/state.md` with current project state in LLN
|
||||||
|
2. Append a round entry to `.h-context/session-log.md`
|
||||||
|
3. Commit: `git add .h-context/ && git commit -m "hctx: <brief>"`
|
||||||
|
- **You only answered a question / gave a read-only or conversational reply**:
|
||||||
|
do NONE of the above. There is nothing to persist — just answer.
|
||||||
|
|
||||||
|
**Never narrate this protocol to the user.** Do not mention state-writes, commits,
|
||||||
|
rounds, or "rituals" in your reply. Persist silently when warranted; otherwise stay
|
||||||
|
quiet about it. The user sees your answer, not your bookkeeping.
|
||||||
|
|
||||||
|
## A. h-context Directory
|
||||||
|
|
||||||
|
```
|
||||||
|
.h-context/
|
||||||
|
├── state.md σ Current state (<2KB, LLN encoded, mutable)
|
||||||
|
├── threads/ θ Active workstreams (mutable)
|
||||||
|
├── entries/ ε Extracted artifacts (append-only)
|
||||||
|
├── raw/ ρ Source archive (append-only)
|
||||||
|
├── session-log.md ψ Round history (append-only)
|
||||||
|
└── rules.md ω Protocol rules (constant)
|
||||||
|
```
|
||||||
|
|
||||||
|
When a `[H-CONTEXT]` block is in the message, use it as your state orientation. Do NOT re-read state.md — the server provides the latest.
|
||||||
|
|
||||||
|
If there is NO `[H-CONTEXT]` block, read `.h-context/state.md` yourself before responding.
|
||||||
|
|
||||||
|
## B. LLN Notation
|
||||||
|
|
||||||
|
State and logs MUST use LLN encoding — NOT prose. This minimises token usage.
|
||||||
|
|
||||||
|
**Format:** `prefix:key|value|metadata`
|
||||||
|
|
||||||
|
**state.md MUST contain:**
|
||||||
|
```
|
||||||
|
σ:session|s<N>|<status>
|
||||||
|
σ:project|<name>|<type/stack>
|
||||||
|
σ:branch|<name>|Δ<summary>
|
||||||
|
θ:active|[<thread1>;<thread2>]
|
||||||
|
θ:blocked|[<thread>→<reason>]
|
||||||
|
ε:last|e<N>|"<summary>"
|
||||||
|
```
|
||||||
|
|
||||||
|
**session-log.md — APPEND after every response:**
|
||||||
|
```
|
||||||
|
ψ:round|<N>|<ISO timestamp>|"<brief summary>"
|
||||||
|
```
|
||||||
|
|
||||||
|
**Symbols:** σ=state, θ=threads, ε=entries, ψ=log, Δ=change, ¬=not, →=flow, true/false/null
|
||||||
|
|
||||||
|
Keep state.md under 2KB.
|
||||||
|
|
||||||
|
## C. Round Protocol (h-conductor)
|
||||||
|
|
||||||
|
You are a **scoped expert** for this project directory.
|
||||||
|
|
||||||
|
**Each response follows this sequence:**
|
||||||
|
1. ORIENT — read `[H-CONTEXT]` block or `.h-context/state.md`
|
||||||
|
2. EXECUTE — do the requested work
|
||||||
|
3. RESPOND — answer the user
|
||||||
|
|
||||||
|
Steps 4–6 apply **only if step 2 changed project state** (files, mutations, task
|
||||||
|
progress). For a read-only or conversational answer, skip them entirely:
|
||||||
|
4. SAVE STATE — update `.h-context/state.md` with LLN
|
||||||
|
5. LOG ROUND — append `ψ:round|N|timestamp|"summary"` to session-log.md
|
||||||
|
6. COMMIT — `git add .h-context/ && git commit -m "hctx: <brief>"`
|
||||||
|
|
||||||
|
Do these silently — never narrate them in your reply.
|
||||||
|
|
||||||
|
## C2. Buggazi Board — your living todo list (work-tracking source of truth)
|
||||||
|
|
||||||
|
You have your OWN dedicated buggazi board, reachable via the `bgz` CLI (already
|
||||||
|
authenticated for this stack). It is the SINGLE SOURCE OF TRUTH for your work —
|
||||||
|
sprints, bugs, features, roadmap — and it SURVIVES session restarts, so you do
|
||||||
|
NOT rely on the LLM context window to remember outstanding work.
|
||||||
|
|
||||||
|
- **"the board" ALWAYS means your buggazi board.** If asked "what is on the
|
||||||
|
board?" / "what should I work on?", run `bgz` and report it — do not guess.
|
||||||
|
- **On session start**, orient with `bgz status` (τ-tree) or `bgz dashboard`.
|
||||||
|
- **Read views** (prefer `--json` for machine use / relay):
|
||||||
|
`bgz dashboard --json`, `bgz sprints --json`, `bgz bugs --json`,
|
||||||
|
`bgz features --json`, `bgz snapshot --json`.
|
||||||
|
- **Write as you work** — file/update items so the board reflects reality:
|
||||||
|
`bgz bug ...`, `bgz feature ...`, `bgz sprint add SPRINT-ID FEAT-ID`,
|
||||||
|
update statuses as you progress. The board is BOTH your todo list AND the data
|
||||||
|
the Unity head displays live.
|
||||||
|
- `bgz init --agent-schema` returns the full machine-readable command list.
|
||||||
|
|
||||||
|
## C3. SecretCarousel — how you mint, fetch & share secrets (`sc` CLI)
|
||||||
|
|
||||||
|
You have the `sc` (SecretCarousel) CLI (already authenticated for this stack). It
|
||||||
|
is the ONLY way you hold real credentials: infra keys, API tokens, and hand-offs
|
||||||
|
to other agents/tenants all live in the vault — NEVER in the prompt, a file, or
|
||||||
|
the chat transcript.
|
||||||
|
|
||||||
|
- **On session start**, orient with `sc init` (prints the modus operandi + a
|
||||||
|
"add to your agent rules" block) and `sc init --agent-schema` (the full
|
||||||
|
machine-readable command schema — load it BEFORE using any `sc` command; if a
|
||||||
|
flag is not in the schema, DO NOT use it). This mirrors `bgz init`.
|
||||||
|
- **Modus operandi (non-negotiable):**
|
||||||
|
1. NEVER print, echo, or repeat a secret value in your replies. Refer to
|
||||||
|
secrets by **id and name**, never by value.
|
||||||
|
2. Fetch a value with `sc secret show <id>` ONLY at the moment of use (writing
|
||||||
|
a config, calling an API) — use it, do not display it.
|
||||||
|
3. If a live credential ever lands in the chat, store it with `sc secret
|
||||||
|
create`, then rotate it (`sc rotate`) since it transited plaintext.
|
||||||
|
4. To hand a secret to another agent/tenant, mint a **claim token**
|
||||||
|
(`sc claim "<VAL>" --to <tenant> --contract <ctr>`) — never paste the value
|
||||||
|
across sessions.
|
||||||
|
- **Common commands** (see `sc init --agent-schema` for the authoritative set):
|
||||||
|
`sc secrets` / `sc secret show <id>` (read), `sc secret create` (store),
|
||||||
|
`sc rotate <id>` (rotate), `sc share` / `sc shares` (grant/list),
|
||||||
|
`sc claim` / `sc claim redeem` (cross-tenant hand-off),
|
||||||
|
`sc env pull` / `sc env promote` (materialise env from the vault),
|
||||||
|
`sc enroll ...` (E2E vault), `sc audit` (access log).
|
||||||
|
|
||||||
|
## D. Git Workflow
|
||||||
|
|
||||||
|
- Git credentials are pre-configured for Gitea (`git.core.tyga.ai`)
|
||||||
|
- After completing a feature or significant work:
|
||||||
|
1. Commit code changes: `git add -A && git commit -m "descriptive message"`
|
||||||
|
2. Push: `git push -u origin <branch>`
|
||||||
|
3. Create PR: `gitea-pr.sh "PR title" --body "description"`
|
||||||
|
- Do NOT use `gh` — it will not work. Use `gitea-pr.sh` for all PRs.
|
||||||
|
- When the user asks to "submit", "create a PR", or "push" — do all three steps.
|
||||||
|
|
||||||
|
## E. LLN Response Mode
|
||||||
|
|
||||||
|
When tools are defined in LLN notation (→tool|name|desc|params), respond using LLN:
|
||||||
|
|
||||||
|
**Tool calls:**
|
||||||
|
```
|
||||||
|
→tool|send_email|{"to":"joe@tyga.agency","subject":"Hello","body":"Hi"}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Multiple tool calls:**
|
||||||
|
```
|
||||||
|
→tool|web_search|{"query":"lovable AI"}
|
||||||
|
→tool|web_fetch|{"url":"https://lovable.dev"}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Status/factual responses** — use LLN where concise:
|
||||||
|
```
|
||||||
|
σ:answer|4
|
||||||
|
σ:status|complete|Δ3files
|
||||||
|
```
|
||||||
|
|
||||||
|
**Conversational responses** — use normal prose (don't force LLN on chat).
|
||||||
|
|
||||||
|
## F. Environment
|
||||||
|
|
||||||
|
- Docker container: Debian, Node.js 20, Python 3
|
||||||
|
- Tools: git, curl, node, npm, python3, bash
|
||||||
|
- All tool calls auto-approved (`--dangerously-skip-permissions`)
|
||||||
@@ -1,3 +1,33 @@
|
|||||||
# template-restaurant
|
# saffron-table
|
||||||
|
|
||||||
Template: restaurant — Aurum
|
Website for Saffron Table — a modern Persian–Mediterranean restaurant in Shoreditch, London.
|
||||||
|
|
||||||
|
Charcoal-grilled mezze and saffron cocktails.
|
||||||
|
|
||||||
|
## Tech Stack
|
||||||
|
|
||||||
|
- React with TypeScript
|
||||||
|
- TailwindCSS for styling
|
||||||
|
- React Router for navigation
|
||||||
|
- Lucide React for icons
|
||||||
|
|
||||||
|
## Pages
|
||||||
|
|
||||||
|
- **Home**: Hero with tagline, signature dishes, gallery, testimonials
|
||||||
|
- **Menu**: Full menu with mezze, mains, desserts, and saffron cocktails
|
||||||
|
- **Reservations**: OpenTable-style booking form
|
||||||
|
- **About**: Story, chef bio, awards, and philosophy
|
||||||
|
- **Events**: Private dining and special events
|
||||||
|
|
||||||
|
## Development
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm install
|
||||||
|
npm run dev
|
||||||
|
```
|
||||||
|
|
||||||
|
## Build
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm run build
|
||||||
|
```
|
||||||
+1
-1
@@ -4,7 +4,7 @@
|
|||||||
<meta charset="UTF-8" />
|
<meta charset="UTF-8" />
|
||||||
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
|
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
<title>Urestaurant — vibeasite template</title>
|
<title>Saffron Table — Persian–Mediterranean, Shoreditch</title>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div id="root"></div>
|
<div id="root"></div>
|
||||||
|
|||||||
+8
-13
@@ -1,5 +1,5 @@
|
|||||||
{
|
{
|
||||||
"name": "template-restaurant",
|
"name": "vite-react-app",
|
||||||
"private": true,
|
"private": true,
|
||||||
"version": "0.0.0",
|
"version": "0.0.0",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
@@ -9,7 +9,12 @@
|
|||||||
"preview": "vite preview"
|
"preview": "vite preview"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@hookform/resolvers": "^3.10.0",
|
"react": "^18.3.1",
|
||||||
|
"react-dom": "^18.3.1",
|
||||||
|
"react-router-dom": "^6.30.1",
|
||||||
|
"lucide-react": "^0.462.0",
|
||||||
|
"clsx": "^2.1.1",
|
||||||
|
"tailwind-merge": "^2.6.0",
|
||||||
"@radix-ui/react-accordion": "^1.2.11",
|
"@radix-ui/react-accordion": "^1.2.11",
|
||||||
"@radix-ui/react-alert-dialog": "^1.1.14",
|
"@radix-ui/react-alert-dialog": "^1.1.14",
|
||||||
"@radix-ui/react-aspect-ratio": "^1.1.7",
|
"@radix-ui/react-aspect-ratio": "^1.1.7",
|
||||||
@@ -37,30 +42,20 @@
|
|||||||
"@radix-ui/react-toggle": "^1.1.9",
|
"@radix-ui/react-toggle": "^1.1.9",
|
||||||
"@radix-ui/react-toggle-group": "^1.1.10",
|
"@radix-ui/react-toggle-group": "^1.1.10",
|
||||||
"@radix-ui/react-tooltip": "^1.2.7",
|
"@radix-ui/react-tooltip": "^1.2.7",
|
||||||
"@tanstack/react-query": "^5.83.0",
|
|
||||||
"class-variance-authority": "^0.7.1",
|
"class-variance-authority": "^0.7.1",
|
||||||
"clsx": "^2.1.1",
|
|
||||||
"cmdk": "^1.1.1",
|
"cmdk": "^1.1.1",
|
||||||
"date-fns": "^3.6.0",
|
"date-fns": "^3.6.0",
|
||||||
"embla-carousel-react": "^8.6.0",
|
"embla-carousel-react": "^8.6.0",
|
||||||
"input-otp": "^1.4.2",
|
"input-otp": "^1.4.2",
|
||||||
"lucide-react": "^0.462.0",
|
|
||||||
"next-themes": "^0.3.0",
|
"next-themes": "^0.3.0",
|
||||||
"react": "^18.3.1",
|
|
||||||
"react-day-picker": "^8.10.1",
|
"react-day-picker": "^8.10.1",
|
||||||
"react-dom": "^18.3.1",
|
|
||||||
"react-hook-form": "^7.61.1",
|
|
||||||
"react-resizable-panels": "^2.1.9",
|
"react-resizable-panels": "^2.1.9",
|
||||||
"react-router-dom": "^6.30.1",
|
|
||||||
"recharts": "^2.15.4",
|
"recharts": "^2.15.4",
|
||||||
"sonner": "^1.7.4",
|
"sonner": "^1.7.4",
|
||||||
"tailwind-merge": "^2.6.0",
|
|
||||||
"tailwindcss-animate": "^1.0.7",
|
"tailwindcss-animate": "^1.0.7",
|
||||||
"vaul": "^0.9.9",
|
"vaul": "^0.9.9"
|
||||||
"zod": "^3.25.76"
|
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/node": "^22.16.5",
|
|
||||||
"@types/react": "^18.3.23",
|
"@types/react": "^18.3.23",
|
||||||
"@types/react-dom": "^18.3.7",
|
"@types/react-dom": "^18.3.7",
|
||||||
"@vitejs/plugin-react-swc": "^3.11.0",
|
"@vitejs/plugin-react-swc": "^3.11.0",
|
||||||
|
|||||||
@@ -1,195 +0,0 @@
|
|||||||
// ---------------------------------------------------------------------------
|
|
||||||
// DataForm — pre-built form component wired to AnyDB insertOne
|
|
||||||
// ---------------------------------------------------------------------------
|
|
||||||
// PRE-INSTALLED. CodeWriterAgent just uses:
|
|
||||||
// import { DataForm } from "@/components/DataForm"
|
|
||||||
// <DataForm
|
|
||||||
// collection="contact_messages"
|
|
||||||
// fields={[
|
|
||||||
// { name: 'name', label: 'Full Name', required: true },
|
|
||||||
// { name: 'email', label: 'Email', type: 'email', required: true },
|
|
||||||
// { name: 'message', label: 'Message', type: 'textarea', rows: 4 },
|
|
||||||
// ]}
|
|
||||||
// submitLabel="Send Message"
|
|
||||||
// successMessage="Message sent! We'll get back to you soon."
|
|
||||||
// />
|
|
||||||
// ---------------------------------------------------------------------------
|
|
||||||
|
|
||||||
import { useState } from "react";
|
|
||||||
import { useMutation } from "@/hooks/use-anydb";
|
|
||||||
import { Input } from "@/components/ui/input";
|
|
||||||
import { Textarea } from "@/components/ui/textarea";
|
|
||||||
import { Button } from "@/components/ui/button";
|
|
||||||
import { Label } from "@/components/ui/label";
|
|
||||||
import { useToast } from "@/hooks/use-toast";
|
|
||||||
import { Loader2, CheckCircle } from "lucide-react";
|
|
||||||
|
|
||||||
interface FormField {
|
|
||||||
name: string;
|
|
||||||
label: string;
|
|
||||||
type?: "text" | "email" | "tel" | "number" | "date" | "time" | "url" | "textarea" | "select";
|
|
||||||
placeholder?: string;
|
|
||||||
required?: boolean;
|
|
||||||
options?: { value: string; label: string }[]; // For select type
|
|
||||||
rows?: number; // For textarea
|
|
||||||
min?: number;
|
|
||||||
max?: number;
|
|
||||||
defaultValue?: string | number;
|
|
||||||
className?: string;
|
|
||||||
gridCol?: number; // 1 or 2 — for grid layout
|
|
||||||
}
|
|
||||||
|
|
||||||
interface DataFormProps {
|
|
||||||
collection: string;
|
|
||||||
fields: FormField[];
|
|
||||||
submitLabel?: string;
|
|
||||||
successMessage?: string;
|
|
||||||
onSuccess?: (data: any) => void;
|
|
||||||
className?: string;
|
|
||||||
layout?: "stack" | "grid"; // stack = vertical, grid = 2-col
|
|
||||||
}
|
|
||||||
|
|
||||||
export function DataForm({
|
|
||||||
collection,
|
|
||||||
fields,
|
|
||||||
submitLabel = "Submit",
|
|
||||||
successMessage = "Saved successfully!",
|
|
||||||
onSuccess,
|
|
||||||
className = "",
|
|
||||||
layout = "stack",
|
|
||||||
}: DataFormProps) {
|
|
||||||
const { toast } = useToast();
|
|
||||||
const { mutate, loading } = useMutation(collection);
|
|
||||||
const [submitted, setSubmitted] = useState(false);
|
|
||||||
|
|
||||||
// Build initial form state from fields
|
|
||||||
const initialState: Record<string, any> = {};
|
|
||||||
fields.forEach(f => { initialState[f.name] = f.defaultValue || ""; });
|
|
||||||
const [formData, setFormData] = useState(initialState);
|
|
||||||
|
|
||||||
const handleChange = (name: string, value: any) => {
|
|
||||||
setFormData(prev => ({ ...prev, [name]: value }));
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleSubmit = async (e: React.FormEvent) => {
|
|
||||||
e.preventDefault();
|
|
||||||
|
|
||||||
// Basic validation
|
|
||||||
for (const field of fields) {
|
|
||||||
if (field.required && !formData[field.name]) {
|
|
||||||
toast({
|
|
||||||
title: "Required field",
|
|
||||||
description: `Please fill in ${field.label}`,
|
|
||||||
variant: "destructive",
|
|
||||||
});
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (field.type === "email" && formData[field.name] && !formData[field.name].includes("@")) {
|
|
||||||
toast({
|
|
||||||
title: "Invalid email",
|
|
||||||
description: "Please enter a valid email address",
|
|
||||||
variant: "destructive",
|
|
||||||
});
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
const result = await mutate(formData);
|
|
||||||
setSubmitted(true);
|
|
||||||
toast({ title: "Success", description: successMessage });
|
|
||||||
setFormData(initialState);
|
|
||||||
onSuccess?.(result);
|
|
||||||
|
|
||||||
// Reset submitted state after 3 seconds
|
|
||||||
setTimeout(() => setSubmitted(false), 3000);
|
|
||||||
} catch (err) {
|
|
||||||
toast({
|
|
||||||
title: "Error",
|
|
||||||
description: "Something went wrong. Please try again.",
|
|
||||||
variant: "destructive",
|
|
||||||
});
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const renderField = (field: FormField) => {
|
|
||||||
const id = `form-${collection}-${field.name}`;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div key={field.name} className={field.gridCol === 2 ? "col-span-2" : ""}>
|
|
||||||
<Label htmlFor={id} className="text-sm font-medium mb-1.5 block">
|
|
||||||
{field.label}
|
|
||||||
{field.required && <span className="text-destructive ml-1">*</span>}
|
|
||||||
</Label>
|
|
||||||
|
|
||||||
{field.type === "textarea" ? (
|
|
||||||
<Textarea
|
|
||||||
id={id}
|
|
||||||
placeholder={field.placeholder || `Enter ${field.label.toLowerCase()}...`}
|
|
||||||
value={formData[field.name]}
|
|
||||||
onChange={(e) => handleChange(field.name, e.target.value)}
|
|
||||||
rows={field.rows || 4}
|
|
||||||
className="resize-none focus-visible:ring-2 focus-visible:ring-primary"
|
|
||||||
required={field.required}
|
|
||||||
disabled={loading}
|
|
||||||
/>
|
|
||||||
) : field.type === "select" ? (
|
|
||||||
<select
|
|
||||||
id={id}
|
|
||||||
value={formData[field.name]}
|
|
||||||
onChange={(e) => handleChange(field.name, e.target.value)}
|
|
||||||
className="flex h-12 w-full rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary focus-visible:ring-offset-2"
|
|
||||||
required={field.required}
|
|
||||||
disabled={loading}
|
|
||||||
>
|
|
||||||
<option value="">{field.placeholder || `Select ${field.label.toLowerCase()}`}</option>
|
|
||||||
{field.options?.map(opt => (
|
|
||||||
<option key={opt.value} value={opt.value}>{opt.label}</option>
|
|
||||||
))}
|
|
||||||
</select>
|
|
||||||
) : (
|
|
||||||
<Input
|
|
||||||
id={id}
|
|
||||||
type={field.type || "text"}
|
|
||||||
placeholder={field.placeholder || `Enter ${field.label.toLowerCase()}`}
|
|
||||||
value={formData[field.name]}
|
|
||||||
onChange={(e) => handleChange(field.name, field.type === "number" ? Number(e.target.value) : e.target.value)}
|
|
||||||
className="h-12 focus-visible:ring-2 focus-visible:ring-primary"
|
|
||||||
required={field.required}
|
|
||||||
min={field.min}
|
|
||||||
max={field.max}
|
|
||||||
disabled={loading}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<form onSubmit={handleSubmit} className={className} aria-busy={loading}>
|
|
||||||
<div className={layout === "grid" ? "grid grid-cols-2 gap-4" : "space-y-4"}>
|
|
||||||
{fields.map(renderField)}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<Button
|
|
||||||
type="submit"
|
|
||||||
className="w-full h-12 text-base mt-6 transition-all duration-150 focus-visible:ring-2 focus-visible:ring-primary focus-visible:ring-offset-2"
|
|
||||||
disabled={loading}
|
|
||||||
>
|
|
||||||
{loading ? (
|
|
||||||
<>
|
|
||||||
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
|
|
||||||
Saving...
|
|
||||||
</>
|
|
||||||
) : submitted ? (
|
|
||||||
<>
|
|
||||||
<CheckCircle className="mr-2 h-4 w-4" />
|
|
||||||
Sent!
|
|
||||||
</>
|
|
||||||
) : (
|
|
||||||
submitLabel
|
|
||||||
)}
|
|
||||||
</Button>
|
|
||||||
</form>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,214 +0,0 @@
|
|||||||
// ---------------------------------------------------------------------------
|
|
||||||
// DataTable — pre-built sortable, paginated table wired to AnyDB
|
|
||||||
// ---------------------------------------------------------------------------
|
|
||||||
// PRE-INSTALLED. CodeWriterAgent just uses:
|
|
||||||
// import { DataTable } from "@/components/DataTable"
|
|
||||||
// <DataTable
|
|
||||||
// collection="orders"
|
|
||||||
// columns={[
|
|
||||||
// { key: 'customer', label: 'Customer' },
|
|
||||||
// { key: 'total_cents', label: 'Amount', render: (v) => `$${(v/100).toFixed(2)}` },
|
|
||||||
// { key: 'status', label: 'Status', render: (v) => <Badge>{v}</Badge> },
|
|
||||||
// { key: 'created_at', label: 'Date', render: (v) => new Date(v).toLocaleDateString() },
|
|
||||||
// ]}
|
|
||||||
// filter={{ status: 'active' }}
|
|
||||||
// pageSize={10}
|
|
||||||
// searchField="customer"
|
|
||||||
// />
|
|
||||||
// ---------------------------------------------------------------------------
|
|
||||||
|
|
||||||
import { useState, useMemo } from "react";
|
|
||||||
import { useCollection } from "@/hooks/use-anydb";
|
|
||||||
import { Input } from "@/components/ui/input";
|
|
||||||
import { Button } from "@/components/ui/button";
|
|
||||||
import { Skeleton } from "@/components/ui/skeleton";
|
|
||||||
import { ChevronUp, ChevronDown, Search, ChevronLeft, ChevronRight } from "lucide-react";
|
|
||||||
|
|
||||||
interface Column<T = any> {
|
|
||||||
key: string;
|
|
||||||
label: string;
|
|
||||||
sortable?: boolean;
|
|
||||||
render?: (value: any, row: T) => React.ReactNode;
|
|
||||||
className?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface DataTableProps<T = any> {
|
|
||||||
collection: string;
|
|
||||||
columns: Column<T>[];
|
|
||||||
filter?: Record<string, any>;
|
|
||||||
pageSize?: number;
|
|
||||||
searchField?: string;
|
|
||||||
searchPlaceholder?: string;
|
|
||||||
onRowClick?: (row: T) => void;
|
|
||||||
emptyMessage?: string;
|
|
||||||
className?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function DataTable<T extends Record<string, any> = any>({
|
|
||||||
collection,
|
|
||||||
columns,
|
|
||||||
filter = {},
|
|
||||||
pageSize = 10,
|
|
||||||
searchField,
|
|
||||||
searchPlaceholder = "Search...",
|
|
||||||
onRowClick,
|
|
||||||
emptyMessage = "No data found",
|
|
||||||
className = "",
|
|
||||||
}: DataTableProps<T>) {
|
|
||||||
const [page, setPage] = useState(0);
|
|
||||||
const [sortKey, setSortKey] = useState<string | null>(null);
|
|
||||||
const [sortDir, setSortDir] = useState<1 | -1>(-1);
|
|
||||||
const [search, setSearch] = useState("");
|
|
||||||
|
|
||||||
const queryFilter = useMemo(() => {
|
|
||||||
const f = { ...filter };
|
|
||||||
if (searchField && search.trim()) {
|
|
||||||
f[searchField] = { $regex: search.trim(), $options: "i" };
|
|
||||||
}
|
|
||||||
return f;
|
|
||||||
}, [filter, searchField, search]);
|
|
||||||
|
|
||||||
const sort = sortKey ? { [sortKey]: sortDir } : { created_at: -1 as const };
|
|
||||||
|
|
||||||
const { data, total, loading, error } = useCollection<T>(collection, {
|
|
||||||
filter: queryFilter,
|
|
||||||
sort,
|
|
||||||
limit: pageSize,
|
|
||||||
skip: page * pageSize,
|
|
||||||
});
|
|
||||||
|
|
||||||
const totalPages = Math.ceil(total / pageSize);
|
|
||||||
|
|
||||||
const handleSort = (key: string) => {
|
|
||||||
if (sortKey === key) {
|
|
||||||
setSortDir(d => (d === 1 ? -1 : 1));
|
|
||||||
} else {
|
|
||||||
setSortKey(key);
|
|
||||||
setSortDir(-1);
|
|
||||||
}
|
|
||||||
setPage(0);
|
|
||||||
};
|
|
||||||
|
|
||||||
if (error) {
|
|
||||||
return (
|
|
||||||
<div className="text-center py-8 text-destructive">
|
|
||||||
<p>Failed to load data: {error}</p>
|
|
||||||
<Button variant="outline" size="sm" className="mt-2" onClick={() => window.location.reload()}>
|
|
||||||
Retry
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className={`rounded-lg border bg-card ${className}`}>
|
|
||||||
{/* Search bar */}
|
|
||||||
{searchField && (
|
|
||||||
<div className="p-4 border-b">
|
|
||||||
<div className="relative max-w-sm">
|
|
||||||
<Search className="absolute left-3 top-1/2 -translate-y-1/2 h-4 w-4 text-muted-foreground" />
|
|
||||||
<Input
|
|
||||||
placeholder={searchPlaceholder}
|
|
||||||
value={search}
|
|
||||||
onChange={(e) => { setSearch(e.target.value); setPage(0); }}
|
|
||||||
className="pl-9 h-10"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* Table */}
|
|
||||||
<div className="overflow-x-auto">
|
|
||||||
<table className="w-full text-sm">
|
|
||||||
<thead>
|
|
||||||
<tr className="border-b bg-muted/50">
|
|
||||||
{columns.map((col) => (
|
|
||||||
<th
|
|
||||||
key={col.key}
|
|
||||||
className={`px-4 py-3 text-left font-medium text-muted-foreground ${
|
|
||||||
col.sortable !== false ? "cursor-pointer select-none hover:text-foreground transition-colors" : ""
|
|
||||||
} ${col.className || ""}`}
|
|
||||||
onClick={() => col.sortable !== false && handleSort(col.key)}
|
|
||||||
>
|
|
||||||
<div className="flex items-center gap-1">
|
|
||||||
{col.label}
|
|
||||||
{col.sortable !== false && sortKey === col.key && (
|
|
||||||
sortDir === 1 ? <ChevronUp className="h-3 w-3" /> : <ChevronDown className="h-3 w-3" />
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</th>
|
|
||||||
))}
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody>
|
|
||||||
{loading ? (
|
|
||||||
Array.from({ length: pageSize }).map((_, i) => (
|
|
||||||
<tr key={i} className="border-b">
|
|
||||||
{columns.map((col) => (
|
|
||||||
<td key={col.key} className="px-4 py-3">
|
|
||||||
<Skeleton className="h-5 w-full" />
|
|
||||||
</td>
|
|
||||||
))}
|
|
||||||
</tr>
|
|
||||||
))
|
|
||||||
) : data.length === 0 ? (
|
|
||||||
<tr>
|
|
||||||
<td colSpan={columns.length} className="px-4 py-12 text-center text-muted-foreground">
|
|
||||||
{emptyMessage}
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
) : (
|
|
||||||
data.map((row, i) => (
|
|
||||||
<tr
|
|
||||||
key={row._id || i}
|
|
||||||
className={`border-b transition-colors hover:bg-muted/30 ${
|
|
||||||
onRowClick ? "cursor-pointer" : ""
|
|
||||||
}`}
|
|
||||||
onClick={() => onRowClick?.(row)}
|
|
||||||
>
|
|
||||||
{columns.map((col) => (
|
|
||||||
<td key={col.key} className={`px-4 py-3 ${col.className || ""}`}>
|
|
||||||
{col.render ? col.render(row[col.key], row) : String(row[col.key] ?? "")}
|
|
||||||
</td>
|
|
||||||
))}
|
|
||||||
</tr>
|
|
||||||
))
|
|
||||||
)}
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Pagination */}
|
|
||||||
{totalPages > 1 && (
|
|
||||||
<div className="flex items-center justify-between px-4 py-3 border-t">
|
|
||||||
<p className="text-sm text-muted-foreground">
|
|
||||||
Showing {page * pageSize + 1}–{Math.min((page + 1) * pageSize, total)} of {total}
|
|
||||||
</p>
|
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
<Button
|
|
||||||
variant="outline"
|
|
||||||
size="sm"
|
|
||||||
disabled={page === 0}
|
|
||||||
onClick={() => setPage(p => p - 1)}
|
|
||||||
className="h-8"
|
|
||||||
>
|
|
||||||
<ChevronLeft className="h-4 w-4" />
|
|
||||||
</Button>
|
|
||||||
<span className="text-sm tabular-nums">
|
|
||||||
{page + 1} / {totalPages}
|
|
||||||
</span>
|
|
||||||
<Button
|
|
||||||
variant="outline"
|
|
||||||
size="sm"
|
|
||||||
disabled={page >= totalPages - 1}
|
|
||||||
onClick={() => setPage(p => p + 1)}
|
|
||||||
className="h-8"
|
|
||||||
>
|
|
||||||
<ChevronRight className="h-4 w-4" />
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -8,12 +8,12 @@ export default function Footer() {
|
|||||||
<div className="mx-auto max-w-7xl px-6">
|
<div className="mx-auto max-w-7xl px-6">
|
||||||
<div className="grid md:grid-cols-2 gap-12">
|
<div className="grid md:grid-cols-2 gap-12">
|
||||||
<div>
|
<div>
|
||||||
<div className="text-xl font-semibold tracking-widest uppercase text-white mb-6">Aurum</div>
|
<div className="text-xl font-semibold tracking-widest uppercase text-white mb-6">Saffron Table</div>
|
||||||
<div className="space-y-3 text-sm">
|
<div className="space-y-3 text-sm">
|
||||||
<div className="flex items-start gap-3"><MapPin className="h-4 w-4 mt-0.5 text-amber-500" /><span>42 Golden Lane, Mayfair, London W1K 3AB</span></div>
|
<div className="flex items-start gap-3"><MapPin className="h-4 w-4 mt-0.5 text-amber-500" /><span>127 Shoreditch High Street, London E1 6JE</span></div>
|
||||||
<div className="flex items-center gap-3"><Clock className="h-4 w-4 text-amber-500" /><span>Tue–Sat 6:00 PM – 11:00 PM</span></div>
|
<div className="flex items-center gap-3"><Clock className="h-4 w-4 text-amber-500" /><span>Tue–Sun 5:00 PM – 11:00 PM</span></div>
|
||||||
<div className="flex items-center gap-3"><Phone className="h-4 w-4 text-amber-500" /><span>+44 20 7946 0123</span></div>
|
<div className="flex items-center gap-3"><Phone className="h-4 w-4 text-amber-500" /><span>+44 20 7490 8956</span></div>
|
||||||
<div className="flex items-center gap-3"><Mail className="h-4 w-4 text-amber-500" /><span>reservations@aurum.com</span></div>
|
<div className="flex items-center gap-3"><Mail className="h-4 w-4 text-amber-500" /><span>reservations@saffrontable.co.uk</span></div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex flex-col items-start md:items-end justify-between">
|
<div className="flex flex-col items-start md:items-end justify-between">
|
||||||
@@ -27,7 +27,7 @@ export default function Footer() {
|
|||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="mt-12 pt-8 border-t border-stone-800 text-center text-sm text-stone-500">© 2026 Aurum. All rights reserved.</div>
|
<div className="mt-12 pt-8 border-t border-stone-800 text-center text-sm text-stone-500">© 2026 Saffron Table. All rights reserved.</div>
|
||||||
</div>
|
</div>
|
||||||
</footer>
|
</footer>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ export default function Header() {
|
|||||||
return (
|
return (
|
||||||
<header className={`fixed top-0 inset-x-0 z-50 transition-all duration-300 ${scrolled ? "bg-stone-950/95 backdrop-blur-lg border-b border-stone-800 shadow-sm" : "bg-transparent"}`}>
|
<header className={`fixed top-0 inset-x-0 z-50 transition-all duration-300 ${scrolled ? "bg-stone-950/95 backdrop-blur-lg border-b border-stone-800 shadow-sm" : "bg-transparent"}`}>
|
||||||
<div className="mx-auto max-w-7xl flex items-center justify-between px-6 py-4">
|
<div className="mx-auto max-w-7xl flex items-center justify-between px-6 py-4">
|
||||||
<Link to="/" className="text-lg font-semibold tracking-widest uppercase text-white">Aurum</Link>
|
<Link to="/" className="text-lg font-semibold tracking-widest uppercase text-white">Saffron Table</Link>
|
||||||
<nav className="hidden md:flex items-center gap-8">
|
<nav className="hidden md:flex items-center gap-8">
|
||||||
{links.map((l) => (
|
{links.map((l) => (
|
||||||
<Link key={l.to} to={l.to} className="text-sm text-stone-300 hover:text-amber-400 transition-colors tracking-wide uppercase">{l.label}</Link>
|
<Link key={l.to} to={l.to} className="text-sm text-stone-300 hover:text-amber-400 transition-colors tracking-wide uppercase">{l.label}</Link>
|
||||||
|
|||||||
@@ -1,124 +0,0 @@
|
|||||||
// ---------------------------------------------------------------------------
|
|
||||||
// KpiCard — pre-built KPI metric card wired to AnyDB aggregate
|
|
||||||
// ---------------------------------------------------------------------------
|
|
||||||
// PRE-INSTALLED. CodeWriterAgent just uses:
|
|
||||||
// import { KpiCard } from "@/components/KpiCard"
|
|
||||||
// <KpiCard collection="orders" metric="count" label="Total Orders" icon={ShoppingBag} />
|
|
||||||
// <KpiCard collection="orders" metric="sum" field="total_cents" label="Revenue" format="currency" icon={DollarSign} />
|
|
||||||
// <KpiCard collection="users" metric="count" filter={{ plan: 'pro' }} label="Pro Users" icon={Users} />
|
|
||||||
// ---------------------------------------------------------------------------
|
|
||||||
|
|
||||||
import { useMemo } from "react";
|
|
||||||
import { useAggregate, useCollection } from "@/hooks/use-anydb";
|
|
||||||
import { Card, CardContent } from "@/components/ui/card";
|
|
||||||
import { Skeleton } from "@/components/ui/skeleton";
|
|
||||||
import { LucideIcon } from "lucide-react";
|
|
||||||
|
|
||||||
interface KpiCardProps {
|
|
||||||
collection: string;
|
|
||||||
metric: "count" | "sum" | "avg" | "min" | "max";
|
|
||||||
field?: string;
|
|
||||||
filter?: Record<string, any>;
|
|
||||||
label: string;
|
|
||||||
icon?: LucideIcon;
|
|
||||||
format?: "number" | "currency" | "percent";
|
|
||||||
prefix?: string;
|
|
||||||
suffix?: string;
|
|
||||||
trend?: number; // e.g. 12.5 for +12.5%
|
|
||||||
className?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function KpiCard({
|
|
||||||
collection,
|
|
||||||
metric,
|
|
||||||
field,
|
|
||||||
filter = {},
|
|
||||||
label,
|
|
||||||
icon: Icon,
|
|
||||||
format = "number",
|
|
||||||
prefix = "",
|
|
||||||
suffix = "",
|
|
||||||
trend,
|
|
||||||
className = "",
|
|
||||||
}: KpiCardProps) {
|
|
||||||
const pipeline = useMemo(() => {
|
|
||||||
const stages: any[] = [];
|
|
||||||
if (Object.keys(filter).length > 0) {
|
|
||||||
stages.push({ $match: filter });
|
|
||||||
}
|
|
||||||
|
|
||||||
switch (metric) {
|
|
||||||
case "count":
|
|
||||||
stages.push({ $count: "value" });
|
|
||||||
break;
|
|
||||||
case "sum":
|
|
||||||
stages.push({ $group: { _id: null, value: { $sum: `$${field}` } } });
|
|
||||||
break;
|
|
||||||
case "avg":
|
|
||||||
stages.push({ $group: { _id: null, value: { $avg: `$${field}` } } });
|
|
||||||
break;
|
|
||||||
case "min":
|
|
||||||
stages.push({ $group: { _id: null, value: { $min: `$${field}` } } });
|
|
||||||
break;
|
|
||||||
case "max":
|
|
||||||
stages.push({ $group: { _id: null, value: { $max: `$${field}` } } });
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
return stages;
|
|
||||||
}, [metric, field, JSON.stringify(filter)]);
|
|
||||||
|
|
||||||
// For count, we can use the simpler useCollection total
|
|
||||||
const useSimpleCount = metric === "count" && Object.keys(filter).length === 0;
|
|
||||||
const { total: simpleTotal, loading: simpleLoading } = useCollection(collection, {
|
|
||||||
limit: 1,
|
|
||||||
autoFetch: useSimpleCount,
|
|
||||||
});
|
|
||||||
|
|
||||||
const { data: aggData, loading: aggLoading } = useAggregate(collection, pipeline);
|
|
||||||
|
|
||||||
const loading = useSimpleCount ? simpleLoading : aggLoading;
|
|
||||||
const rawValue = useSimpleCount
|
|
||||||
? simpleTotal
|
|
||||||
: aggData?.[0]?.value ?? 0;
|
|
||||||
|
|
||||||
const formatValue = (val: number): string => {
|
|
||||||
switch (format) {
|
|
||||||
case "currency":
|
|
||||||
return `$${(val / 100).toLocaleString("en-US", { minimumFractionDigits: 0, maximumFractionDigits: 0 })}`;
|
|
||||||
case "percent":
|
|
||||||
return `${val.toFixed(1)}%`;
|
|
||||||
default:
|
|
||||||
return val >= 1000 ? `${(val / 1000).toFixed(1)}K` : val.toLocaleString();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Card className={`${className}`}>
|
|
||||||
<CardContent className="p-6">
|
|
||||||
<div className="flex items-center justify-between">
|
|
||||||
<div className="space-y-1">
|
|
||||||
<p className="text-sm text-muted-foreground">{label}</p>
|
|
||||||
{loading ? (
|
|
||||||
<Skeleton className="h-8 w-24" />
|
|
||||||
) : (
|
|
||||||
<p className="text-2xl font-bold tabular-nums">
|
|
||||||
{prefix}{formatValue(rawValue)}{suffix}
|
|
||||||
</p>
|
|
||||||
)}
|
|
||||||
{trend !== undefined && !loading && (
|
|
||||||
<p className={`text-xs font-medium ${trend >= 0 ? "text-emerald-600" : "text-red-500"}`}>
|
|
||||||
{trend >= 0 ? "↑" : "↓"} {Math.abs(trend)}%
|
|
||||||
</p>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
{Icon && (
|
|
||||||
<div className="h-12 w-12 rounded-xl bg-primary/10 flex items-center justify-center shrink-0">
|
|
||||||
<Icon className="h-6 w-6 text-primary" />
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,125 +0,0 @@
|
|||||||
// ---------------------------------------------------------------------------
|
|
||||||
// ScrollHeader — pre-built responsive header for single-page scroll sites
|
|
||||||
// ---------------------------------------------------------------------------
|
|
||||||
// PRE-INSTALLED in every single-page React project. App.tsx imports:
|
|
||||||
// import ScrollHeader from "@/components/ScrollHeader"
|
|
||||||
// <ScrollHeader brand="Site Name" links={[{href:"#about",label:"About"},{href:"#contact",label:"Contact"}]} />
|
|
||||||
// ---------------------------------------------------------------------------
|
|
||||||
|
|
||||||
import { useState, useEffect } from "react";
|
|
||||||
|
|
||||||
interface NavLink {
|
|
||||||
href: string;
|
|
||||||
label: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface ScrollHeaderProps {
|
|
||||||
brand: string;
|
|
||||||
links: NavLink[];
|
|
||||||
ctaLabel?: string;
|
|
||||||
ctaHref?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export default function ScrollHeader({ brand, links, ctaLabel, ctaHref }: ScrollHeaderProps) {
|
|
||||||
const [open, setOpen] = useState(false);
|
|
||||||
const [scrolled, setScrolled] = useState(false);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
const onScroll = () => setScrolled(window.scrollY > 40);
|
|
||||||
window.addEventListener("scroll", onScroll, { passive: true });
|
|
||||||
return () => window.removeEventListener("scroll", onScroll);
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const handleClick = (href: string) => {
|
|
||||||
setOpen(false);
|
|
||||||
const id = href.replace("#", "");
|
|
||||||
const el = document.getElementById(id);
|
|
||||||
if (el) {
|
|
||||||
el.scrollIntoView({ behavior: "smooth" });
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<header
|
|
||||||
className={`fixed top-0 inset-x-0 z-50 transition-all duration-300 ${
|
|
||||||
scrolled
|
|
||||||
? "bg-background/95 backdrop-blur-md border-b border-border shadow-sm"
|
|
||||||
: "bg-transparent"
|
|
||||||
}`}
|
|
||||||
>
|
|
||||||
<div className="mx-auto max-w-7xl flex items-center justify-between px-6 h-16 md:h-20">
|
|
||||||
{/* Brand */}
|
|
||||||
<a
|
|
||||||
href="#"
|
|
||||||
onClick={(e) => { e.preventDefault(); window.scrollTo({ top: 0, behavior: "smooth" }); }}
|
|
||||||
className="text-lg font-bold tracking-tight text-foreground hover:text-primary transition-colors duration-150 focus-visible:ring-2 focus-visible:ring-primary focus-visible:ring-offset-2 rounded"
|
|
||||||
>
|
|
||||||
{brand}
|
|
||||||
</a>
|
|
||||||
|
|
||||||
{/* Desktop nav */}
|
|
||||||
<nav className="hidden md:flex items-center gap-6">
|
|
||||||
{links.map((link) => (
|
|
||||||
<a
|
|
||||||
key={link.href}
|
|
||||||
href={link.href}
|
|
||||||
onClick={(e) => { e.preventDefault(); handleClick(link.href); }}
|
|
||||||
className="text-sm font-medium text-muted-foreground hover:text-foreground transition-colors duration-150 py-2 focus-visible:ring-2 focus-visible:ring-primary rounded"
|
|
||||||
>
|
|
||||||
{link.label}
|
|
||||||
</a>
|
|
||||||
))}
|
|
||||||
{ctaLabel && (
|
|
||||||
<a
|
|
||||||
href={ctaHref || "#"}
|
|
||||||
onClick={(e) => { if (ctaHref?.startsWith("#")) { e.preventDefault(); handleClick(ctaHref); } }}
|
|
||||||
className="inline-flex items-center justify-center h-10 px-6 text-sm font-medium rounded-md bg-primary text-primary-foreground hover:bg-primary/90 active:scale-[0.98] transition-all duration-150 focus-visible:ring-2 focus-visible:ring-primary focus-visible:ring-offset-2"
|
|
||||||
>
|
|
||||||
{ctaLabel}
|
|
||||||
</a>
|
|
||||||
)}
|
|
||||||
</nav>
|
|
||||||
|
|
||||||
{/* Mobile hamburger */}
|
|
||||||
<button
|
|
||||||
onClick={() => setOpen(!open)}
|
|
||||||
className="md:hidden inline-flex items-center justify-center w-10 h-10 rounded-md text-foreground hover:bg-muted transition-colors duration-150 focus-visible:ring-2 focus-visible:ring-primary"
|
|
||||||
aria-label={open ? "Close menu" : "Open menu"}
|
|
||||||
>
|
|
||||||
{open ? (
|
|
||||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round"><path d="M18 6L6 18M6 6l12 12" /></svg>
|
|
||||||
) : (
|
|
||||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round"><path d="M4 6h16M4 12h16M4 18h16" /></svg>
|
|
||||||
)}
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Mobile nav overlay */}
|
|
||||||
{open && (
|
|
||||||
<div className="md:hidden absolute inset-x-0 top-full bg-background/98 backdrop-blur-lg border-b border-border shadow-lg">
|
|
||||||
<nav className="flex flex-col px-6 py-4 gap-1">
|
|
||||||
{links.map((link) => (
|
|
||||||
<a
|
|
||||||
key={link.href}
|
|
||||||
href={link.href}
|
|
||||||
onClick={(e) => { e.preventDefault(); handleClick(link.href); }}
|
|
||||||
className="py-3 text-base font-medium text-foreground hover:text-primary transition-colors duration-150 focus-visible:ring-2 focus-visible:ring-primary rounded px-2"
|
|
||||||
>
|
|
||||||
{link.label}
|
|
||||||
</a>
|
|
||||||
))}
|
|
||||||
{ctaLabel && (
|
|
||||||
<a
|
|
||||||
href={ctaHref || "#"}
|
|
||||||
onClick={(e) => { if (ctaHref?.startsWith("#")) { e.preventDefault(); handleClick(ctaHref); } }}
|
|
||||||
className="mt-2 inline-flex items-center justify-center h-12 px-6 text-base font-medium rounded-md bg-primary text-primary-foreground hover:bg-primary/90 active:scale-[0.98] transition-all duration-150"
|
|
||||||
>
|
|
||||||
{ctaLabel}
|
|
||||||
</a>
|
|
||||||
)}
|
|
||||||
</nav>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</header>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,19 +0,0 @@
|
|||||||
import { useTheme } from "next-themes";
|
|
||||||
import { Button } from "@/components/ui/button";
|
|
||||||
import { Sun, Moon } from "lucide-react";
|
|
||||||
|
|
||||||
export function ThemeToggle() {
|
|
||||||
const { theme, setTheme } = useTheme();
|
|
||||||
return (
|
|
||||||
<Button
|
|
||||||
variant="ghost"
|
|
||||||
size="icon"
|
|
||||||
className="h-9 w-9"
|
|
||||||
onClick={() => setTheme(theme === "dark" ? "light" : "dark")}
|
|
||||||
aria-label="Toggle theme"
|
|
||||||
>
|
|
||||||
<Sun className="h-4 w-4 rotate-0 scale-100 transition-all dark:-rotate-90 dark:scale-0" />
|
|
||||||
<Moon className="absolute h-4 w-4 rotate-90 scale-0 transition-all dark:rotate-0 dark:scale-100" />
|
|
||||||
</Button>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,52 +0,0 @@
|
|||||||
import * as React from "react";
|
|
||||||
import * as AccordionPrimitive from "@radix-ui/react-accordion";
|
|
||||||
import { ChevronDown } from "lucide-react";
|
|
||||||
|
|
||||||
import { cn } from "@/lib/utils";
|
|
||||||
|
|
||||||
const Accordion = AccordionPrimitive.Root;
|
|
||||||
|
|
||||||
const AccordionItem = React.forwardRef<
|
|
||||||
React.ElementRef<typeof AccordionPrimitive.Item>,
|
|
||||||
React.ComponentPropsWithoutRef<typeof AccordionPrimitive.Item>
|
|
||||||
>(({ className, ...props }, ref) => (
|
|
||||||
<AccordionPrimitive.Item ref={ref} className={cn("border-b", className)} {...props} />
|
|
||||||
));
|
|
||||||
AccordionItem.displayName = "AccordionItem";
|
|
||||||
|
|
||||||
const AccordionTrigger = React.forwardRef<
|
|
||||||
React.ElementRef<typeof AccordionPrimitive.Trigger>,
|
|
||||||
React.ComponentPropsWithoutRef<typeof AccordionPrimitive.Trigger>
|
|
||||||
>(({ className, children, ...props }, ref) => (
|
|
||||||
<AccordionPrimitive.Header className="flex">
|
|
||||||
<AccordionPrimitive.Trigger
|
|
||||||
ref={ref}
|
|
||||||
className={cn(
|
|
||||||
"flex flex-1 items-center justify-between py-4 font-medium transition-all hover:underline [&[data-state=open]>svg]:rotate-180",
|
|
||||||
className,
|
|
||||||
)}
|
|
||||||
{...props}
|
|
||||||
>
|
|
||||||
{children}
|
|
||||||
<ChevronDown className="h-4 w-4 shrink-0 transition-transform duration-200" />
|
|
||||||
</AccordionPrimitive.Trigger>
|
|
||||||
</AccordionPrimitive.Header>
|
|
||||||
));
|
|
||||||
AccordionTrigger.displayName = AccordionPrimitive.Trigger.displayName;
|
|
||||||
|
|
||||||
const AccordionContent = React.forwardRef<
|
|
||||||
React.ElementRef<typeof AccordionPrimitive.Content>,
|
|
||||||
React.ComponentPropsWithoutRef<typeof AccordionPrimitive.Content>
|
|
||||||
>(({ className, children, ...props }, ref) => (
|
|
||||||
<AccordionPrimitive.Content
|
|
||||||
ref={ref}
|
|
||||||
className="overflow-hidden text-sm transition-all data-[state=closed]:animate-accordion-up data-[state=open]:animate-accordion-down"
|
|
||||||
{...props}
|
|
||||||
>
|
|
||||||
<div className={cn("pb-4 pt-0", className)}>{children}</div>
|
|
||||||
</AccordionPrimitive.Content>
|
|
||||||
));
|
|
||||||
|
|
||||||
AccordionContent.displayName = AccordionPrimitive.Content.displayName;
|
|
||||||
|
|
||||||
export { Accordion, AccordionItem, AccordionTrigger, AccordionContent };
|
|
||||||
@@ -1,104 +0,0 @@
|
|||||||
import * as React from "react";
|
|
||||||
import * as AlertDialogPrimitive from "@radix-ui/react-alert-dialog";
|
|
||||||
|
|
||||||
import { cn } from "@/lib/utils";
|
|
||||||
import { buttonVariants } from "@/components/ui/button";
|
|
||||||
|
|
||||||
const AlertDialog = AlertDialogPrimitive.Root;
|
|
||||||
|
|
||||||
const AlertDialogTrigger = AlertDialogPrimitive.Trigger;
|
|
||||||
|
|
||||||
const AlertDialogPortal = AlertDialogPrimitive.Portal;
|
|
||||||
|
|
||||||
const AlertDialogOverlay = React.forwardRef<
|
|
||||||
React.ElementRef<typeof AlertDialogPrimitive.Overlay>,
|
|
||||||
React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Overlay>
|
|
||||||
>(({ className, ...props }, ref) => (
|
|
||||||
<AlertDialogPrimitive.Overlay
|
|
||||||
className={cn(
|
|
||||||
"fixed inset-0 z-50 bg-black/80 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0",
|
|
||||||
className,
|
|
||||||
)}
|
|
||||||
{...props}
|
|
||||||
ref={ref}
|
|
||||||
/>
|
|
||||||
));
|
|
||||||
AlertDialogOverlay.displayName = AlertDialogPrimitive.Overlay.displayName;
|
|
||||||
|
|
||||||
const AlertDialogContent = React.forwardRef<
|
|
||||||
React.ElementRef<typeof AlertDialogPrimitive.Content>,
|
|
||||||
React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Content>
|
|
||||||
>(({ className, ...props }, ref) => (
|
|
||||||
<AlertDialogPortal>
|
|
||||||
<AlertDialogOverlay />
|
|
||||||
<AlertDialogPrimitive.Content
|
|
||||||
ref={ref}
|
|
||||||
className={cn(
|
|
||||||
"fixed left-[50%] top-[50%] z-50 grid w-full max-w-lg translate-x-[-50%] translate-y-[-50%] gap-4 border bg-background p-6 shadow-lg duration-200 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-[48%] data-[state=open]:slide-in-from-left-1/2 data-[state=open]:slide-in-from-top-[48%] sm:rounded-lg",
|
|
||||||
className,
|
|
||||||
)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
</AlertDialogPortal>
|
|
||||||
));
|
|
||||||
AlertDialogContent.displayName = AlertDialogPrimitive.Content.displayName;
|
|
||||||
|
|
||||||
const AlertDialogHeader = ({ className, ...props }: React.HTMLAttributes<HTMLDivElement>) => (
|
|
||||||
<div className={cn("flex flex-col space-y-2 text-center sm:text-left", className)} {...props} />
|
|
||||||
);
|
|
||||||
AlertDialogHeader.displayName = "AlertDialogHeader";
|
|
||||||
|
|
||||||
const AlertDialogFooter = ({ className, ...props }: React.HTMLAttributes<HTMLDivElement>) => (
|
|
||||||
<div className={cn("flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2", className)} {...props} />
|
|
||||||
);
|
|
||||||
AlertDialogFooter.displayName = "AlertDialogFooter";
|
|
||||||
|
|
||||||
const AlertDialogTitle = React.forwardRef<
|
|
||||||
React.ElementRef<typeof AlertDialogPrimitive.Title>,
|
|
||||||
React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Title>
|
|
||||||
>(({ className, ...props }, ref) => (
|
|
||||||
<AlertDialogPrimitive.Title ref={ref} className={cn("text-lg font-semibold", className)} {...props} />
|
|
||||||
));
|
|
||||||
AlertDialogTitle.displayName = AlertDialogPrimitive.Title.displayName;
|
|
||||||
|
|
||||||
const AlertDialogDescription = React.forwardRef<
|
|
||||||
React.ElementRef<typeof AlertDialogPrimitive.Description>,
|
|
||||||
React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Description>
|
|
||||||
>(({ className, ...props }, ref) => (
|
|
||||||
<AlertDialogPrimitive.Description ref={ref} className={cn("text-sm text-muted-foreground", className)} {...props} />
|
|
||||||
));
|
|
||||||
AlertDialogDescription.displayName = AlertDialogPrimitive.Description.displayName;
|
|
||||||
|
|
||||||
const AlertDialogAction = React.forwardRef<
|
|
||||||
React.ElementRef<typeof AlertDialogPrimitive.Action>,
|
|
||||||
React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Action>
|
|
||||||
>(({ className, ...props }, ref) => (
|
|
||||||
<AlertDialogPrimitive.Action ref={ref} className={cn(buttonVariants(), className)} {...props} />
|
|
||||||
));
|
|
||||||
AlertDialogAction.displayName = AlertDialogPrimitive.Action.displayName;
|
|
||||||
|
|
||||||
const AlertDialogCancel = React.forwardRef<
|
|
||||||
React.ElementRef<typeof AlertDialogPrimitive.Cancel>,
|
|
||||||
React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Cancel>
|
|
||||||
>(({ className, ...props }, ref) => (
|
|
||||||
<AlertDialogPrimitive.Cancel
|
|
||||||
ref={ref}
|
|
||||||
className={cn(buttonVariants({ variant: "outline" }), "mt-2 sm:mt-0", className)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
));
|
|
||||||
AlertDialogCancel.displayName = AlertDialogPrimitive.Cancel.displayName;
|
|
||||||
|
|
||||||
export {
|
|
||||||
AlertDialog,
|
|
||||||
AlertDialogPortal,
|
|
||||||
AlertDialogOverlay,
|
|
||||||
AlertDialogTrigger,
|
|
||||||
AlertDialogContent,
|
|
||||||
AlertDialogHeader,
|
|
||||||
AlertDialogFooter,
|
|
||||||
AlertDialogTitle,
|
|
||||||
AlertDialogDescription,
|
|
||||||
AlertDialogAction,
|
|
||||||
AlertDialogCancel,
|
|
||||||
};
|
|
||||||
@@ -1,43 +0,0 @@
|
|||||||
import * as React from "react";
|
|
||||||
import { cva, type VariantProps } from "class-variance-authority";
|
|
||||||
|
|
||||||
import { cn } from "@/lib/utils";
|
|
||||||
|
|
||||||
const alertVariants = cva(
|
|
||||||
"relative w-full rounded-lg border p-4 [&>svg~*]:pl-7 [&>svg+div]:translate-y-[-3px] [&>svg]:absolute [&>svg]:left-4 [&>svg]:top-4 [&>svg]:text-foreground",
|
|
||||||
{
|
|
||||||
variants: {
|
|
||||||
variant: {
|
|
||||||
default: "bg-background text-foreground",
|
|
||||||
destructive: "border-destructive/50 text-destructive dark:border-destructive [&>svg]:text-destructive",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
defaultVariants: {
|
|
||||||
variant: "default",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
const Alert = React.forwardRef<
|
|
||||||
HTMLDivElement,
|
|
||||||
React.HTMLAttributes<HTMLDivElement> & VariantProps<typeof alertVariants>
|
|
||||||
>(({ className, variant, ...props }, ref) => (
|
|
||||||
<div ref={ref} role="alert" className={cn(alertVariants({ variant }), className)} {...props} />
|
|
||||||
));
|
|
||||||
Alert.displayName = "Alert";
|
|
||||||
|
|
||||||
const AlertTitle = React.forwardRef<HTMLParagraphElement, React.HTMLAttributes<HTMLHeadingElement>>(
|
|
||||||
({ className, ...props }, ref) => (
|
|
||||||
<h5 ref={ref} className={cn("mb-1 font-medium leading-none tracking-tight", className)} {...props} />
|
|
||||||
),
|
|
||||||
);
|
|
||||||
AlertTitle.displayName = "AlertTitle";
|
|
||||||
|
|
||||||
const AlertDescription = React.forwardRef<HTMLParagraphElement, React.HTMLAttributes<HTMLParagraphElement>>(
|
|
||||||
({ className, ...props }, ref) => (
|
|
||||||
<div ref={ref} className={cn("text-sm [&_p]:leading-relaxed", className)} {...props} />
|
|
||||||
),
|
|
||||||
);
|
|
||||||
AlertDescription.displayName = "AlertDescription";
|
|
||||||
|
|
||||||
export { Alert, AlertTitle, AlertDescription };
|
|
||||||
@@ -1,5 +0,0 @@
|
|||||||
import * as AspectRatioPrimitive from "@radix-ui/react-aspect-ratio";
|
|
||||||
|
|
||||||
const AspectRatio = AspectRatioPrimitive.Root;
|
|
||||||
|
|
||||||
export { AspectRatio };
|
|
||||||
@@ -1,38 +0,0 @@
|
|||||||
import * as React from "react";
|
|
||||||
import * as AvatarPrimitive from "@radix-ui/react-avatar";
|
|
||||||
|
|
||||||
import { cn } from "@/lib/utils";
|
|
||||||
|
|
||||||
const Avatar = React.forwardRef<
|
|
||||||
React.ElementRef<typeof AvatarPrimitive.Root>,
|
|
||||||
React.ComponentPropsWithoutRef<typeof AvatarPrimitive.Root>
|
|
||||||
>(({ className, ...props }, ref) => (
|
|
||||||
<AvatarPrimitive.Root
|
|
||||||
ref={ref}
|
|
||||||
className={cn("relative flex h-10 w-10 shrink-0 overflow-hidden rounded-full", className)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
));
|
|
||||||
Avatar.displayName = AvatarPrimitive.Root.displayName;
|
|
||||||
|
|
||||||
const AvatarImage = React.forwardRef<
|
|
||||||
React.ElementRef<typeof AvatarPrimitive.Image>,
|
|
||||||
React.ComponentPropsWithoutRef<typeof AvatarPrimitive.Image>
|
|
||||||
>(({ className, ...props }, ref) => (
|
|
||||||
<AvatarPrimitive.Image ref={ref} className={cn("aspect-square h-full w-full", className)} {...props} />
|
|
||||||
));
|
|
||||||
AvatarImage.displayName = AvatarPrimitive.Image.displayName;
|
|
||||||
|
|
||||||
const AvatarFallback = React.forwardRef<
|
|
||||||
React.ElementRef<typeof AvatarPrimitive.Fallback>,
|
|
||||||
React.ComponentPropsWithoutRef<typeof AvatarPrimitive.Fallback>
|
|
||||||
>(({ className, ...props }, ref) => (
|
|
||||||
<AvatarPrimitive.Fallback
|
|
||||||
ref={ref}
|
|
||||||
className={cn("flex h-full w-full items-center justify-center rounded-full bg-muted", className)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
));
|
|
||||||
AvatarFallback.displayName = AvatarPrimitive.Fallback.displayName;
|
|
||||||
|
|
||||||
export { Avatar, AvatarImage, AvatarFallback };
|
|
||||||
+27
-23
@@ -1,29 +1,33 @@
|
|||||||
import * as React from "react";
|
import * as React from "react";
|
||||||
import { cva, type VariantProps } from "class-variance-authority";
|
|
||||||
|
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
|
|
||||||
const badgeVariants = cva(
|
const badgeStyles: Record<string, string> = {
|
||||||
"inline-flex items-center rounded-full border px-2.5 py-0.5 text-xs font-semibold transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2",
|
default: "bg-indigo-600 text-white",
|
||||||
{
|
secondary: "bg-slate-100 text-slate-900",
|
||||||
variants: {
|
outline: "border border-slate-300 text-slate-900",
|
||||||
variant: {
|
destructive: "bg-red-600 text-white",
|
||||||
default: "border-transparent bg-primary text-primary-foreground hover:bg-primary/80",
|
success: "bg-emerald-100 text-emerald-700",
|
||||||
secondary: "border-transparent bg-secondary text-secondary-foreground hover:bg-secondary/80",
|
};
|
||||||
destructive: "border-transparent bg-destructive text-destructive-foreground hover:bg-destructive/80",
|
|
||||||
outline: "text-foreground",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
defaultVariants: {
|
|
||||||
variant: "default",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
export interface BadgeProps extends React.HTMLAttributes<HTMLDivElement>, VariantProps<typeof badgeVariants> {}
|
export interface BadgeProps extends React.HTMLAttributes<HTMLSpanElement> {
|
||||||
|
variant?: keyof typeof badgeStyles;
|
||||||
function Badge({ className, variant, ...props }: BadgeProps) {
|
|
||||||
return <div className={cn(badgeVariants({ variant }), className)} {...props} />;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export { Badge, badgeVariants };
|
export function Badge({ className, variant = "default", ...props }: BadgeProps) {
|
||||||
|
return (
|
||||||
|
<span
|
||||||
|
className={cn(
|
||||||
|
"inline-flex items-center rounded-full px-2.5 py-0.5 text-xs font-semibold",
|
||||||
|
badgeStyles[variant] || badgeStyles.default,
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function badgeVariants(opts?: { variant?: string; className?: string }) {
|
||||||
|
return cn("inline-flex items-center rounded-full px-2.5 py-0.5 text-xs font-semibold", badgeStyles[opts?.variant || "default"], opts?.className);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Badge;
|
||||||
|
|||||||
@@ -1,90 +0,0 @@
|
|||||||
import * as React from "react";
|
|
||||||
import { Slot } from "@radix-ui/react-slot";
|
|
||||||
import { ChevronRight, MoreHorizontal } from "lucide-react";
|
|
||||||
|
|
||||||
import { cn } from "@/lib/utils";
|
|
||||||
|
|
||||||
const Breadcrumb = React.forwardRef<
|
|
||||||
HTMLElement,
|
|
||||||
React.ComponentPropsWithoutRef<"nav"> & {
|
|
||||||
separator?: React.ReactNode;
|
|
||||||
}
|
|
||||||
>(({ ...props }, ref) => <nav ref={ref} aria-label="breadcrumb" {...props} />);
|
|
||||||
Breadcrumb.displayName = "Breadcrumb";
|
|
||||||
|
|
||||||
const BreadcrumbList = React.forwardRef<HTMLOListElement, React.ComponentPropsWithoutRef<"ol">>(
|
|
||||||
({ className, ...props }, ref) => (
|
|
||||||
<ol
|
|
||||||
ref={ref}
|
|
||||||
className={cn(
|
|
||||||
"flex flex-wrap items-center gap-1.5 break-words text-sm text-muted-foreground sm:gap-2.5",
|
|
||||||
className,
|
|
||||||
)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
),
|
|
||||||
);
|
|
||||||
BreadcrumbList.displayName = "BreadcrumbList";
|
|
||||||
|
|
||||||
const BreadcrumbItem = React.forwardRef<HTMLLIElement, React.ComponentPropsWithoutRef<"li">>(
|
|
||||||
({ className, ...props }, ref) => (
|
|
||||||
<li ref={ref} className={cn("inline-flex items-center gap-1.5", className)} {...props} />
|
|
||||||
),
|
|
||||||
);
|
|
||||||
BreadcrumbItem.displayName = "BreadcrumbItem";
|
|
||||||
|
|
||||||
const BreadcrumbLink = React.forwardRef<
|
|
||||||
HTMLAnchorElement,
|
|
||||||
React.ComponentPropsWithoutRef<"a"> & {
|
|
||||||
asChild?: boolean;
|
|
||||||
}
|
|
||||||
>(({ asChild, className, ...props }, ref) => {
|
|
||||||
const Comp = asChild ? Slot : "a";
|
|
||||||
|
|
||||||
return <Comp ref={ref} className={cn("transition-colors hover:text-foreground", className)} {...props} />;
|
|
||||||
});
|
|
||||||
BreadcrumbLink.displayName = "BreadcrumbLink";
|
|
||||||
|
|
||||||
const BreadcrumbPage = React.forwardRef<HTMLSpanElement, React.ComponentPropsWithoutRef<"span">>(
|
|
||||||
({ className, ...props }, ref) => (
|
|
||||||
<span
|
|
||||||
ref={ref}
|
|
||||||
role="link"
|
|
||||||
aria-disabled="true"
|
|
||||||
aria-current="page"
|
|
||||||
className={cn("font-normal text-foreground", className)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
),
|
|
||||||
);
|
|
||||||
BreadcrumbPage.displayName = "BreadcrumbPage";
|
|
||||||
|
|
||||||
const BreadcrumbSeparator = ({ children, className, ...props }: React.ComponentProps<"li">) => (
|
|
||||||
<li role="presentation" aria-hidden="true" className={cn("[&>svg]:size-3.5", className)} {...props}>
|
|
||||||
{children ?? <ChevronRight />}
|
|
||||||
</li>
|
|
||||||
);
|
|
||||||
BreadcrumbSeparator.displayName = "BreadcrumbSeparator";
|
|
||||||
|
|
||||||
const BreadcrumbEllipsis = ({ className, ...props }: React.ComponentProps<"span">) => (
|
|
||||||
<span
|
|
||||||
role="presentation"
|
|
||||||
aria-hidden="true"
|
|
||||||
className={cn("flex h-9 w-9 items-center justify-center", className)}
|
|
||||||
{...props}
|
|
||||||
>
|
|
||||||
<MoreHorizontal className="h-4 w-4" />
|
|
||||||
<span className="sr-only">More</span>
|
|
||||||
</span>
|
|
||||||
);
|
|
||||||
BreadcrumbEllipsis.displayName = "BreadcrumbElipssis";
|
|
||||||
|
|
||||||
export {
|
|
||||||
Breadcrumb,
|
|
||||||
BreadcrumbList,
|
|
||||||
BreadcrumbItem,
|
|
||||||
BreadcrumbLink,
|
|
||||||
BreadcrumbPage,
|
|
||||||
BreadcrumbSeparator,
|
|
||||||
BreadcrumbEllipsis,
|
|
||||||
};
|
|
||||||
@@ -1,51 +1,53 @@
|
|||||||
import * as React from "react";
|
import * as React from "react";
|
||||||
import { Slot } from "@radix-ui/react-slot";
|
|
||||||
import { cva, type VariantProps } from "class-variance-authority";
|
|
||||||
|
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
|
|
||||||
const buttonVariants = cva(
|
const variantClasses: Record<string, string> = {
|
||||||
"inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-lg text-sm font-medium ring-offset-background transition-all duration-300 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0",
|
default: "bg-indigo-600 text-white hover:bg-indigo-700",
|
||||||
{
|
secondary: "bg-slate-100 text-slate-900 hover:bg-slate-200",
|
||||||
variants: {
|
outline: "border border-slate-300 bg-transparent hover:bg-slate-50 text-slate-900",
|
||||||
variant: {
|
ghost: "bg-transparent hover:bg-slate-100 text-slate-900",
|
||||||
default: "bg-primary text-primary-foreground hover:bg-primary/90 hover:shadow-md",
|
destructive: "bg-red-600 text-white hover:bg-red-700",
|
||||||
destructive: "bg-destructive text-destructive-foreground hover:bg-destructive/90",
|
link: "bg-transparent underline-offset-4 hover:underline text-indigo-600",
|
||||||
outline: "border border-input bg-background hover:bg-accent hover:text-accent-foreground",
|
};
|
||||||
secondary: "bg-secondary text-secondary-foreground hover:bg-secondary/80",
|
|
||||||
ghost: "hover:bg-accent hover:text-accent-foreground",
|
|
||||||
link: "text-primary underline-offset-4 hover:underline",
|
|
||||||
hero: "bg-primary text-primary-foreground hover:bg-primary/90 hover:shadow-lg hover:-translate-y-0.5 font-medium tracking-wide",
|
|
||||||
heroOutline: "border-2 border-primary text-primary hover:bg-primary hover:text-primary-foreground font-medium tracking-wide",
|
|
||||||
terracotta: "bg-secondary text-secondary-foreground hover:bg-secondary/90 hover:shadow-md",
|
|
||||||
},
|
|
||||||
size: {
|
|
||||||
default: "h-10 px-4 py-2",
|
|
||||||
sm: "h-9 rounded-md px-3",
|
|
||||||
lg: "h-12 rounded-lg px-8 text-base",
|
|
||||||
xl: "h-14 rounded-lg px-10 text-lg",
|
|
||||||
icon: "h-10 w-10",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
defaultVariants: {
|
|
||||||
variant: "default",
|
|
||||||
size: "default",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
export interface ButtonProps
|
const sizeClasses: Record<string, string> = {
|
||||||
extends React.ButtonHTMLAttributes<HTMLButtonElement>,
|
default: "h-10 px-4 py-2 text-sm",
|
||||||
VariantProps<typeof buttonVariants> {
|
sm: "h-8 px-3 text-xs",
|
||||||
|
lg: "h-12 px-6 text-base",
|
||||||
|
icon: "h-10 w-10 p-0",
|
||||||
|
};
|
||||||
|
|
||||||
|
export interface ButtonProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {
|
||||||
|
variant?: keyof typeof variantClasses;
|
||||||
|
size?: keyof typeof sizeClasses;
|
||||||
asChild?: boolean;
|
asChild?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
|
export const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
|
||||||
({ className, variant, size, asChild = false, ...props }, ref) => {
|
({ className, variant = "default", size = "default", asChild, children, ...props }, ref) => {
|
||||||
const Comp = asChild ? Slot : "button";
|
const classes = cn(
|
||||||
return <Comp className={cn(buttonVariants({ variant, size, className }))} ref={ref} {...props} />;
|
"inline-flex items-center justify-center gap-2 rounded-md font-medium transition-colors disabled:opacity-50 disabled:pointer-events-none",
|
||||||
},
|
variantClasses[variant] || variantClasses.default,
|
||||||
|
sizeClasses[size] || sizeClasses.default,
|
||||||
|
className
|
||||||
|
);
|
||||||
|
if (asChild && React.isValidElement(children)) {
|
||||||
|
return React.cloneElement(children as React.ReactElement, {
|
||||||
|
className: cn(classes, (children as React.ReactElement).props?.className),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return <button ref={ref} className={classes} {...props}>{children}</button>;
|
||||||
|
}
|
||||||
);
|
);
|
||||||
Button.displayName = "Button";
|
Button.displayName = "Button";
|
||||||
|
|
||||||
export { Button, buttonVariants };
|
export function buttonVariants(opts?: { variant?: string; size?: string; className?: string }) {
|
||||||
|
return cn(
|
||||||
|
"inline-flex items-center justify-center gap-2 rounded-md font-medium transition-colors",
|
||||||
|
variantClasses[opts?.variant || "default"],
|
||||||
|
sizeClasses[opts?.size || "default"],
|
||||||
|
opts?.className
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Button;
|
||||||
|
|||||||
@@ -1,54 +0,0 @@
|
|||||||
import * as React from "react";
|
|
||||||
import { ChevronLeft, ChevronRight } from "lucide-react";
|
|
||||||
import { DayPicker } from "react-day-picker";
|
|
||||||
|
|
||||||
import { cn } from "@/lib/utils";
|
|
||||||
import { buttonVariants } from "@/components/ui/button";
|
|
||||||
|
|
||||||
export type CalendarProps = React.ComponentProps<typeof DayPicker>;
|
|
||||||
|
|
||||||
function Calendar({ className, classNames, showOutsideDays = true, ...props }: CalendarProps) {
|
|
||||||
return (
|
|
||||||
<DayPicker
|
|
||||||
showOutsideDays={showOutsideDays}
|
|
||||||
className={cn("p-3", className)}
|
|
||||||
classNames={{
|
|
||||||
months: "flex flex-col sm:flex-row space-y-4 sm:space-x-4 sm:space-y-0",
|
|
||||||
month: "space-y-4",
|
|
||||||
caption: "flex justify-center pt-1 relative items-center",
|
|
||||||
caption_label: "text-sm font-medium",
|
|
||||||
nav: "space-x-1 flex items-center",
|
|
||||||
nav_button: cn(
|
|
||||||
buttonVariants({ variant: "outline" }),
|
|
||||||
"h-7 w-7 bg-transparent p-0 opacity-50 hover:opacity-100",
|
|
||||||
),
|
|
||||||
nav_button_previous: "absolute left-1",
|
|
||||||
nav_button_next: "absolute right-1",
|
|
||||||
table: "w-full border-collapse space-y-1",
|
|
||||||
head_row: "flex",
|
|
||||||
head_cell: "text-muted-foreground rounded-md w-9 font-normal text-[0.8rem]",
|
|
||||||
row: "flex w-full mt-2",
|
|
||||||
cell: "h-9 w-9 text-center text-sm p-0 relative [&:has([aria-selected].day-range-end)]:rounded-r-md [&:has([aria-selected].day-outside)]:bg-accent/50 [&:has([aria-selected])]:bg-accent first:[&:has([aria-selected])]:rounded-l-md last:[&:has([aria-selected])]:rounded-r-md focus-within:relative focus-within:z-20",
|
|
||||||
day: cn(buttonVariants({ variant: "ghost" }), "h-9 w-9 p-0 font-normal aria-selected:opacity-100"),
|
|
||||||
day_range_end: "day-range-end",
|
|
||||||
day_selected:
|
|
||||||
"bg-primary text-primary-foreground hover:bg-primary hover:text-primary-foreground focus:bg-primary focus:text-primary-foreground",
|
|
||||||
day_today: "bg-accent text-accent-foreground",
|
|
||||||
day_outside:
|
|
||||||
"day-outside text-muted-foreground opacity-50 aria-selected:bg-accent/50 aria-selected:text-muted-foreground aria-selected:opacity-30",
|
|
||||||
day_disabled: "text-muted-foreground opacity-50",
|
|
||||||
day_range_middle: "aria-selected:bg-accent aria-selected:text-accent-foreground",
|
|
||||||
day_hidden: "invisible",
|
|
||||||
...classNames,
|
|
||||||
}}
|
|
||||||
components={{
|
|
||||||
IconLeft: ({ ..._props }) => <ChevronLeft className="h-4 w-4" />,
|
|
||||||
IconRight: ({ ..._props }) => <ChevronRight className="h-4 w-4" />,
|
|
||||||
}}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
Calendar.displayName = "Calendar";
|
|
||||||
|
|
||||||
export { Calendar };
|
|
||||||
+16
-28
@@ -1,43 +1,31 @@
|
|||||||
import * as React from "react";
|
import * as React from "react";
|
||||||
|
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
|
|
||||||
const Card = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(({ className, ...props }, ref) => (
|
export const Card = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(
|
||||||
<div ref={ref} className={cn("rounded-lg border bg-card text-card-foreground shadow-sm", className)} {...props} />
|
({ className, ...props }, ref) => (
|
||||||
));
|
<div ref={ref} className={cn("rounded-xl border border-slate-200 bg-white text-slate-950 shadow-sm", className)} {...props} />
|
||||||
|
)
|
||||||
|
);
|
||||||
Card.displayName = "Card";
|
Card.displayName = "Card";
|
||||||
|
|
||||||
const CardHeader = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(
|
export const CardHeader = ({ className, ...props }: React.HTMLAttributes<HTMLDivElement>) => (
|
||||||
({ className, ...props }, ref) => (
|
<div className={cn("flex flex-col space-y-1.5 p-6", className)} {...props} />
|
||||||
<div ref={ref} className={cn("flex flex-col space-y-1.5 p-6", className)} {...props} />
|
|
||||||
),
|
|
||||||
);
|
);
|
||||||
CardHeader.displayName = "CardHeader";
|
|
||||||
|
|
||||||
const CardTitle = React.forwardRef<HTMLParagraphElement, React.HTMLAttributes<HTMLHeadingElement>>(
|
export const CardTitle = ({ className, ...props }: React.HTMLAttributes<HTMLHeadingElement>) => (
|
||||||
({ className, ...props }, ref) => (
|
<h3 className={cn("text-lg font-semibold leading-none tracking-tight", className)} {...props} />
|
||||||
<h3 ref={ref} className={cn("text-2xl font-semibold leading-none tracking-tight", className)} {...props} />
|
|
||||||
),
|
|
||||||
);
|
);
|
||||||
CardTitle.displayName = "CardTitle";
|
|
||||||
|
|
||||||
const CardDescription = React.forwardRef<HTMLParagraphElement, React.HTMLAttributes<HTMLParagraphElement>>(
|
export const CardDescription = ({ className, ...props }: React.HTMLAttributes<HTMLParagraphElement>) => (
|
||||||
({ className, ...props }, ref) => (
|
<p className={cn("text-sm text-slate-500", className)} {...props} />
|
||||||
<p ref={ref} className={cn("text-sm text-muted-foreground", className)} {...props} />
|
|
||||||
),
|
|
||||||
);
|
);
|
||||||
CardDescription.displayName = "CardDescription";
|
|
||||||
|
|
||||||
const CardContent = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(
|
export const CardContent = ({ className, ...props }: React.HTMLAttributes<HTMLDivElement>) => (
|
||||||
({ className, ...props }, ref) => <div ref={ref} className={cn("p-6 pt-0", className)} {...props} />,
|
<div className={cn("p-6 pt-0", className)} {...props} />
|
||||||
);
|
);
|
||||||
CardContent.displayName = "CardContent";
|
|
||||||
|
|
||||||
const CardFooter = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(
|
export const CardFooter = ({ className, ...props }: React.HTMLAttributes<HTMLDivElement>) => (
|
||||||
({ className, ...props }, ref) => (
|
<div className={cn("flex items-center p-6 pt-0", className)} {...props} />
|
||||||
<div ref={ref} className={cn("flex items-center p-6 pt-0", className)} {...props} />
|
|
||||||
),
|
|
||||||
);
|
);
|
||||||
CardFooter.displayName = "CardFooter";
|
|
||||||
|
|
||||||
export { Card, CardHeader, CardFooter, CardTitle, CardDescription, CardContent };
|
export default Card;
|
||||||
|
|||||||
@@ -1,224 +0,0 @@
|
|||||||
import * as React from "react";
|
|
||||||
import useEmblaCarousel, { type UseEmblaCarouselType } from "embla-carousel-react";
|
|
||||||
import { ArrowLeft, ArrowRight } from "lucide-react";
|
|
||||||
|
|
||||||
import { cn } from "@/lib/utils";
|
|
||||||
import { Button } from "@/components/ui/button";
|
|
||||||
|
|
||||||
type CarouselApi = UseEmblaCarouselType[1];
|
|
||||||
type UseCarouselParameters = Parameters<typeof useEmblaCarousel>;
|
|
||||||
type CarouselOptions = UseCarouselParameters[0];
|
|
||||||
type CarouselPlugin = UseCarouselParameters[1];
|
|
||||||
|
|
||||||
type CarouselProps = {
|
|
||||||
opts?: CarouselOptions;
|
|
||||||
plugins?: CarouselPlugin;
|
|
||||||
orientation?: "horizontal" | "vertical";
|
|
||||||
setApi?: (api: CarouselApi) => void;
|
|
||||||
};
|
|
||||||
|
|
||||||
type CarouselContextProps = {
|
|
||||||
carouselRef: ReturnType<typeof useEmblaCarousel>[0];
|
|
||||||
api: ReturnType<typeof useEmblaCarousel>[1];
|
|
||||||
scrollPrev: () => void;
|
|
||||||
scrollNext: () => void;
|
|
||||||
canScrollPrev: boolean;
|
|
||||||
canScrollNext: boolean;
|
|
||||||
} & CarouselProps;
|
|
||||||
|
|
||||||
const CarouselContext = React.createContext<CarouselContextProps | null>(null);
|
|
||||||
|
|
||||||
function useCarousel() {
|
|
||||||
const context = React.useContext(CarouselContext);
|
|
||||||
|
|
||||||
if (!context) {
|
|
||||||
throw new Error("useCarousel must be used within a <Carousel />");
|
|
||||||
}
|
|
||||||
|
|
||||||
return context;
|
|
||||||
}
|
|
||||||
|
|
||||||
const Carousel = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement> & CarouselProps>(
|
|
||||||
({ orientation = "horizontal", opts, setApi, plugins, className, children, ...props }, ref) => {
|
|
||||||
const [carouselRef, api] = useEmblaCarousel(
|
|
||||||
{
|
|
||||||
...opts,
|
|
||||||
axis: orientation === "horizontal" ? "x" : "y",
|
|
||||||
},
|
|
||||||
plugins,
|
|
||||||
);
|
|
||||||
const [canScrollPrev, setCanScrollPrev] = React.useState(false);
|
|
||||||
const [canScrollNext, setCanScrollNext] = React.useState(false);
|
|
||||||
|
|
||||||
const onSelect = React.useCallback((api: CarouselApi) => {
|
|
||||||
if (!api) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
setCanScrollPrev(api.canScrollPrev());
|
|
||||||
setCanScrollNext(api.canScrollNext());
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const scrollPrev = React.useCallback(() => {
|
|
||||||
api?.scrollPrev();
|
|
||||||
}, [api]);
|
|
||||||
|
|
||||||
const scrollNext = React.useCallback(() => {
|
|
||||||
api?.scrollNext();
|
|
||||||
}, [api]);
|
|
||||||
|
|
||||||
const handleKeyDown = React.useCallback(
|
|
||||||
(event: React.KeyboardEvent<HTMLDivElement>) => {
|
|
||||||
if (event.key === "ArrowLeft") {
|
|
||||||
event.preventDefault();
|
|
||||||
scrollPrev();
|
|
||||||
} else if (event.key === "ArrowRight") {
|
|
||||||
event.preventDefault();
|
|
||||||
scrollNext();
|
|
||||||
}
|
|
||||||
},
|
|
||||||
[scrollPrev, scrollNext],
|
|
||||||
);
|
|
||||||
|
|
||||||
React.useEffect(() => {
|
|
||||||
if (!api || !setApi) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
setApi(api);
|
|
||||||
}, [api, setApi]);
|
|
||||||
|
|
||||||
React.useEffect(() => {
|
|
||||||
if (!api) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
onSelect(api);
|
|
||||||
api.on("reInit", onSelect);
|
|
||||||
api.on("select", onSelect);
|
|
||||||
|
|
||||||
return () => {
|
|
||||||
api?.off("select", onSelect);
|
|
||||||
};
|
|
||||||
}, [api, onSelect]);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<CarouselContext.Provider
|
|
||||||
value={{
|
|
||||||
carouselRef,
|
|
||||||
api: api,
|
|
||||||
opts,
|
|
||||||
orientation: orientation || (opts?.axis === "y" ? "vertical" : "horizontal"),
|
|
||||||
scrollPrev,
|
|
||||||
scrollNext,
|
|
||||||
canScrollPrev,
|
|
||||||
canScrollNext,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
ref={ref}
|
|
||||||
onKeyDownCapture={handleKeyDown}
|
|
||||||
className={cn("relative", className)}
|
|
||||||
role="region"
|
|
||||||
aria-roledescription="carousel"
|
|
||||||
{...props}
|
|
||||||
>
|
|
||||||
{children}
|
|
||||||
</div>
|
|
||||||
</CarouselContext.Provider>
|
|
||||||
);
|
|
||||||
},
|
|
||||||
);
|
|
||||||
Carousel.displayName = "Carousel";
|
|
||||||
|
|
||||||
const CarouselContent = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(
|
|
||||||
({ className, ...props }, ref) => {
|
|
||||||
const { carouselRef, orientation } = useCarousel();
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div ref={carouselRef} className="overflow-hidden">
|
|
||||||
<div
|
|
||||||
ref={ref}
|
|
||||||
className={cn("flex", orientation === "horizontal" ? "-ml-4" : "-mt-4 flex-col", className)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
},
|
|
||||||
);
|
|
||||||
CarouselContent.displayName = "CarouselContent";
|
|
||||||
|
|
||||||
const CarouselItem = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(
|
|
||||||
({ className, ...props }, ref) => {
|
|
||||||
const { orientation } = useCarousel();
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
ref={ref}
|
|
||||||
role="group"
|
|
||||||
aria-roledescription="slide"
|
|
||||||
className={cn("min-w-0 shrink-0 grow-0 basis-full", orientation === "horizontal" ? "pl-4" : "pt-4", className)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
},
|
|
||||||
);
|
|
||||||
CarouselItem.displayName = "CarouselItem";
|
|
||||||
|
|
||||||
const CarouselPrevious = React.forwardRef<HTMLButtonElement, React.ComponentProps<typeof Button>>(
|
|
||||||
({ className, variant = "outline", size = "icon", ...props }, ref) => {
|
|
||||||
const { orientation, scrollPrev, canScrollPrev } = useCarousel();
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Button
|
|
||||||
ref={ref}
|
|
||||||
variant={variant}
|
|
||||||
size={size}
|
|
||||||
className={cn(
|
|
||||||
"absolute h-8 w-8 rounded-full",
|
|
||||||
orientation === "horizontal"
|
|
||||||
? "-left-12 top-1/2 -translate-y-1/2"
|
|
||||||
: "-top-12 left-1/2 -translate-x-1/2 rotate-90",
|
|
||||||
className,
|
|
||||||
)}
|
|
||||||
disabled={!canScrollPrev}
|
|
||||||
onClick={scrollPrev}
|
|
||||||
{...props}
|
|
||||||
>
|
|
||||||
<ArrowLeft className="h-4 w-4" />
|
|
||||||
<span className="sr-only">Previous slide</span>
|
|
||||||
</Button>
|
|
||||||
);
|
|
||||||
},
|
|
||||||
);
|
|
||||||
CarouselPrevious.displayName = "CarouselPrevious";
|
|
||||||
|
|
||||||
const CarouselNext = React.forwardRef<HTMLButtonElement, React.ComponentProps<typeof Button>>(
|
|
||||||
({ className, variant = "outline", size = "icon", ...props }, ref) => {
|
|
||||||
const { orientation, scrollNext, canScrollNext } = useCarousel();
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Button
|
|
||||||
ref={ref}
|
|
||||||
variant={variant}
|
|
||||||
size={size}
|
|
||||||
className={cn(
|
|
||||||
"absolute h-8 w-8 rounded-full",
|
|
||||||
orientation === "horizontal"
|
|
||||||
? "-right-12 top-1/2 -translate-y-1/2"
|
|
||||||
: "-bottom-12 left-1/2 -translate-x-1/2 rotate-90",
|
|
||||||
className,
|
|
||||||
)}
|
|
||||||
disabled={!canScrollNext}
|
|
||||||
onClick={scrollNext}
|
|
||||||
{...props}
|
|
||||||
>
|
|
||||||
<ArrowRight className="h-4 w-4" />
|
|
||||||
<span className="sr-only">Next slide</span>
|
|
||||||
</Button>
|
|
||||||
);
|
|
||||||
},
|
|
||||||
);
|
|
||||||
CarouselNext.displayName = "CarouselNext";
|
|
||||||
|
|
||||||
export { type CarouselApi, Carousel, CarouselContent, CarouselItem, CarouselPrevious, CarouselNext };
|
|
||||||
@@ -1,303 +0,0 @@
|
|||||||
import * as React from "react";
|
|
||||||
import * as RechartsPrimitive from "recharts";
|
|
||||||
|
|
||||||
import { cn } from "@/lib/utils";
|
|
||||||
|
|
||||||
// Format: { THEME_NAME: CSS_SELECTOR }
|
|
||||||
const THEMES = { light: "", dark: ".dark" } as const;
|
|
||||||
|
|
||||||
export type ChartConfig = {
|
|
||||||
[k in string]: {
|
|
||||||
label?: React.ReactNode;
|
|
||||||
icon?: React.ComponentType;
|
|
||||||
} & ({ color?: string; theme?: never } | { color?: never; theme: Record<keyof typeof THEMES, string> });
|
|
||||||
};
|
|
||||||
|
|
||||||
type ChartContextProps = {
|
|
||||||
config: ChartConfig;
|
|
||||||
};
|
|
||||||
|
|
||||||
const ChartContext = React.createContext<ChartContextProps | null>(null);
|
|
||||||
|
|
||||||
function useChart() {
|
|
||||||
const context = React.useContext(ChartContext);
|
|
||||||
|
|
||||||
if (!context) {
|
|
||||||
throw new Error("useChart must be used within a <ChartContainer />");
|
|
||||||
}
|
|
||||||
|
|
||||||
return context;
|
|
||||||
}
|
|
||||||
|
|
||||||
const ChartContainer = React.forwardRef<
|
|
||||||
HTMLDivElement,
|
|
||||||
React.ComponentProps<"div"> & {
|
|
||||||
config: ChartConfig;
|
|
||||||
children: React.ComponentProps<typeof RechartsPrimitive.ResponsiveContainer>["children"];
|
|
||||||
}
|
|
||||||
>(({ id, className, children, config, ...props }, ref) => {
|
|
||||||
const uniqueId = React.useId();
|
|
||||||
const chartId = `chart-${id || uniqueId.replace(/:/g, "")}`;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<ChartContext.Provider value={{ config }}>
|
|
||||||
<div
|
|
||||||
data-chart={chartId}
|
|
||||||
ref={ref}
|
|
||||||
className={cn(
|
|
||||||
"flex aspect-video justify-center text-xs [&_.recharts-cartesian-axis-tick_text]:fill-muted-foreground [&_.recharts-cartesian-grid_line[stroke='#ccc']]:stroke-border/50 [&_.recharts-curve.recharts-tooltip-cursor]:stroke-border [&_.recharts-dot[stroke='#fff']]:stroke-transparent [&_.recharts-layer]:outline-none [&_.recharts-polar-grid_[stroke='#ccc']]:stroke-border [&_.recharts-radial-bar-background-sector]:fill-muted [&_.recharts-rectangle.recharts-tooltip-cursor]:fill-muted [&_.recharts-reference-line_[stroke='#ccc']]:stroke-border [&_.recharts-sector[stroke='#fff']]:stroke-transparent [&_.recharts-sector]:outline-none [&_.recharts-surface]:outline-none",
|
|
||||||
className,
|
|
||||||
)}
|
|
||||||
{...props}
|
|
||||||
>
|
|
||||||
<ChartStyle id={chartId} config={config} />
|
|
||||||
<RechartsPrimitive.ResponsiveContainer>{children}</RechartsPrimitive.ResponsiveContainer>
|
|
||||||
</div>
|
|
||||||
</ChartContext.Provider>
|
|
||||||
);
|
|
||||||
});
|
|
||||||
ChartContainer.displayName = "Chart";
|
|
||||||
|
|
||||||
const ChartStyle = ({ id, config }: { id: string; config: ChartConfig }) => {
|
|
||||||
const colorConfig = Object.entries(config).filter(([_, config]) => config.theme || config.color);
|
|
||||||
|
|
||||||
if (!colorConfig.length) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<style
|
|
||||||
dangerouslySetInnerHTML={{
|
|
||||||
__html: Object.entries(THEMES)
|
|
||||||
.map(
|
|
||||||
([theme, prefix]) => `
|
|
||||||
${prefix} [data-chart=${id}] {
|
|
||||||
${colorConfig
|
|
||||||
.map(([key, itemConfig]) => {
|
|
||||||
const color = itemConfig.theme?.[theme as keyof typeof itemConfig.theme] || itemConfig.color;
|
|
||||||
return color ? ` --color-${key}: ${color};` : null;
|
|
||||||
})
|
|
||||||
.join("\n")}
|
|
||||||
}
|
|
||||||
`,
|
|
||||||
)
|
|
||||||
.join("\n"),
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
const ChartTooltip = RechartsPrimitive.Tooltip;
|
|
||||||
|
|
||||||
const ChartTooltipContent = React.forwardRef<
|
|
||||||
HTMLDivElement,
|
|
||||||
React.ComponentProps<typeof RechartsPrimitive.Tooltip> &
|
|
||||||
React.ComponentProps<"div"> & {
|
|
||||||
hideLabel?: boolean;
|
|
||||||
hideIndicator?: boolean;
|
|
||||||
indicator?: "line" | "dot" | "dashed";
|
|
||||||
nameKey?: string;
|
|
||||||
labelKey?: string;
|
|
||||||
}
|
|
||||||
>(
|
|
||||||
(
|
|
||||||
{
|
|
||||||
active,
|
|
||||||
payload,
|
|
||||||
className,
|
|
||||||
indicator = "dot",
|
|
||||||
hideLabel = false,
|
|
||||||
hideIndicator = false,
|
|
||||||
label,
|
|
||||||
labelFormatter,
|
|
||||||
labelClassName,
|
|
||||||
formatter,
|
|
||||||
color,
|
|
||||||
nameKey,
|
|
||||||
labelKey,
|
|
||||||
},
|
|
||||||
ref,
|
|
||||||
) => {
|
|
||||||
const { config } = useChart();
|
|
||||||
|
|
||||||
const tooltipLabel = React.useMemo(() => {
|
|
||||||
if (hideLabel || !payload?.length) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
const [item] = payload;
|
|
||||||
const key = `${labelKey || item.dataKey || item.name || "value"}`;
|
|
||||||
const itemConfig = getPayloadConfigFromPayload(config, item, key);
|
|
||||||
const value =
|
|
||||||
!labelKey && typeof label === "string"
|
|
||||||
? config[label as keyof typeof config]?.label || label
|
|
||||||
: itemConfig?.label;
|
|
||||||
|
|
||||||
if (labelFormatter) {
|
|
||||||
return <div className={cn("font-medium", labelClassName)}>{labelFormatter(value, payload)}</div>;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!value) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return <div className={cn("font-medium", labelClassName)}>{value}</div>;
|
|
||||||
}, [label, labelFormatter, payload, hideLabel, labelClassName, config, labelKey]);
|
|
||||||
|
|
||||||
if (!active || !payload?.length) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
const nestLabel = payload.length === 1 && indicator !== "dot";
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
ref={ref}
|
|
||||||
className={cn(
|
|
||||||
"grid min-w-[8rem] items-start gap-1.5 rounded-lg border border-border/50 bg-background px-2.5 py-1.5 text-xs shadow-xl",
|
|
||||||
className,
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
{!nestLabel ? tooltipLabel : null}
|
|
||||||
<div className="grid gap-1.5">
|
|
||||||
{payload.map((item, index) => {
|
|
||||||
const key = `${nameKey || item.name || item.dataKey || "value"}`;
|
|
||||||
const itemConfig = getPayloadConfigFromPayload(config, item, key);
|
|
||||||
const indicatorColor = color || item.payload.fill || item.color;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
key={item.dataKey}
|
|
||||||
className={cn(
|
|
||||||
"flex w-full flex-wrap items-stretch gap-2 [&>svg]:h-2.5 [&>svg]:w-2.5 [&>svg]:text-muted-foreground",
|
|
||||||
indicator === "dot" && "items-center",
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
{formatter && item?.value !== undefined && item.name ? (
|
|
||||||
formatter(item.value, item.name, item, index, item.payload)
|
|
||||||
) : (
|
|
||||||
<>
|
|
||||||
{itemConfig?.icon ? (
|
|
||||||
<itemConfig.icon />
|
|
||||||
) : (
|
|
||||||
!hideIndicator && (
|
|
||||||
<div
|
|
||||||
className={cn("shrink-0 rounded-[2px] border-[--color-border] bg-[--color-bg]", {
|
|
||||||
"h-2.5 w-2.5": indicator === "dot",
|
|
||||||
"w-1": indicator === "line",
|
|
||||||
"w-0 border-[1.5px] border-dashed bg-transparent": indicator === "dashed",
|
|
||||||
"my-0.5": nestLabel && indicator === "dashed",
|
|
||||||
})}
|
|
||||||
style={
|
|
||||||
{
|
|
||||||
"--color-bg": indicatorColor,
|
|
||||||
"--color-border": indicatorColor,
|
|
||||||
} as React.CSSProperties
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
)}
|
|
||||||
<div
|
|
||||||
className={cn(
|
|
||||||
"flex flex-1 justify-between leading-none",
|
|
||||||
nestLabel ? "items-end" : "items-center",
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
<div className="grid gap-1.5">
|
|
||||||
{nestLabel ? tooltipLabel : null}
|
|
||||||
<span className="text-muted-foreground">{itemConfig?.label || item.name}</span>
|
|
||||||
</div>
|
|
||||||
{item.value && (
|
|
||||||
<span className="font-mono font-medium tabular-nums text-foreground">
|
|
||||||
{item.value.toLocaleString()}
|
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
},
|
|
||||||
);
|
|
||||||
ChartTooltipContent.displayName = "ChartTooltip";
|
|
||||||
|
|
||||||
const ChartLegend = RechartsPrimitive.Legend;
|
|
||||||
|
|
||||||
const ChartLegendContent = React.forwardRef<
|
|
||||||
HTMLDivElement,
|
|
||||||
React.ComponentProps<"div"> &
|
|
||||||
Pick<RechartsPrimitive.LegendProps, "payload" | "verticalAlign"> & {
|
|
||||||
hideIcon?: boolean;
|
|
||||||
nameKey?: string;
|
|
||||||
}
|
|
||||||
>(({ className, hideIcon = false, payload, verticalAlign = "bottom", nameKey }, ref) => {
|
|
||||||
const { config } = useChart();
|
|
||||||
|
|
||||||
if (!payload?.length) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
ref={ref}
|
|
||||||
className={cn("flex items-center justify-center gap-4", verticalAlign === "top" ? "pb-3" : "pt-3", className)}
|
|
||||||
>
|
|
||||||
{payload.map((item) => {
|
|
||||||
const key = `${nameKey || item.dataKey || "value"}`;
|
|
||||||
const itemConfig = getPayloadConfigFromPayload(config, item, key);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
key={item.value}
|
|
||||||
className={cn("flex items-center gap-1.5 [&>svg]:h-3 [&>svg]:w-3 [&>svg]:text-muted-foreground")}
|
|
||||||
>
|
|
||||||
{itemConfig?.icon && !hideIcon ? (
|
|
||||||
<itemConfig.icon />
|
|
||||||
) : (
|
|
||||||
<div
|
|
||||||
className="h-2 w-2 shrink-0 rounded-[2px]"
|
|
||||||
style={{
|
|
||||||
backgroundColor: item.color,
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
{itemConfig?.label}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
});
|
|
||||||
ChartLegendContent.displayName = "ChartLegend";
|
|
||||||
|
|
||||||
// Helper to extract item config from a payload.
|
|
||||||
function getPayloadConfigFromPayload(config: ChartConfig, payload: unknown, key: string) {
|
|
||||||
if (typeof payload !== "object" || payload === null) {
|
|
||||||
return undefined;
|
|
||||||
}
|
|
||||||
|
|
||||||
const payloadPayload =
|
|
||||||
"payload" in payload && typeof payload.payload === "object" && payload.payload !== null
|
|
||||||
? payload.payload
|
|
||||||
: undefined;
|
|
||||||
|
|
||||||
let configLabelKey: string = key;
|
|
||||||
|
|
||||||
if (key in payload && typeof payload[key as keyof typeof payload] === "string") {
|
|
||||||
configLabelKey = payload[key as keyof typeof payload] as string;
|
|
||||||
} else if (
|
|
||||||
payloadPayload &&
|
|
||||||
key in payloadPayload &&
|
|
||||||
typeof payloadPayload[key as keyof typeof payloadPayload] === "string"
|
|
||||||
) {
|
|
||||||
configLabelKey = payloadPayload[key as keyof typeof payloadPayload] as string;
|
|
||||||
}
|
|
||||||
|
|
||||||
return configLabelKey in config ? config[configLabelKey] : config[key as keyof typeof config];
|
|
||||||
}
|
|
||||||
|
|
||||||
export { ChartContainer, ChartTooltip, ChartTooltipContent, ChartLegend, ChartLegendContent, ChartStyle };
|
|
||||||
@@ -1,26 +0,0 @@
|
|||||||
import * as React from "react";
|
|
||||||
import * as CheckboxPrimitive from "@radix-ui/react-checkbox";
|
|
||||||
import { Check } from "lucide-react";
|
|
||||||
|
|
||||||
import { cn } from "@/lib/utils";
|
|
||||||
|
|
||||||
const Checkbox = React.forwardRef<
|
|
||||||
React.ElementRef<typeof CheckboxPrimitive.Root>,
|
|
||||||
React.ComponentPropsWithoutRef<typeof CheckboxPrimitive.Root>
|
|
||||||
>(({ className, ...props }, ref) => (
|
|
||||||
<CheckboxPrimitive.Root
|
|
||||||
ref={ref}
|
|
||||||
className={cn(
|
|
||||||
"peer h-4 w-4 shrink-0 rounded-sm border border-primary ring-offset-background data-[state=checked]:bg-primary data-[state=checked]:text-primary-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50",
|
|
||||||
className,
|
|
||||||
)}
|
|
||||||
{...props}
|
|
||||||
>
|
|
||||||
<CheckboxPrimitive.Indicator className={cn("flex items-center justify-center text-current")}>
|
|
||||||
<Check className="h-4 w-4" />
|
|
||||||
</CheckboxPrimitive.Indicator>
|
|
||||||
</CheckboxPrimitive.Root>
|
|
||||||
));
|
|
||||||
Checkbox.displayName = CheckboxPrimitive.Root.displayName;
|
|
||||||
|
|
||||||
export { Checkbox };
|
|
||||||
@@ -1,9 +0,0 @@
|
|||||||
import * as CollapsiblePrimitive from "@radix-ui/react-collapsible";
|
|
||||||
|
|
||||||
const Collapsible = CollapsiblePrimitive.Root;
|
|
||||||
|
|
||||||
const CollapsibleTrigger = CollapsiblePrimitive.CollapsibleTrigger;
|
|
||||||
|
|
||||||
const CollapsibleContent = CollapsiblePrimitive.CollapsibleContent;
|
|
||||||
|
|
||||||
export { Collapsible, CollapsibleTrigger, CollapsibleContent };
|
|
||||||
@@ -1,132 +0,0 @@
|
|||||||
import * as React from "react";
|
|
||||||
import { type DialogProps } from "@radix-ui/react-dialog";
|
|
||||||
import { Command as CommandPrimitive } from "cmdk";
|
|
||||||
import { Search } from "lucide-react";
|
|
||||||
|
|
||||||
import { cn } from "@/lib/utils";
|
|
||||||
import { Dialog, DialogContent } from "@/components/ui/dialog";
|
|
||||||
|
|
||||||
const Command = React.forwardRef<
|
|
||||||
React.ElementRef<typeof CommandPrimitive>,
|
|
||||||
React.ComponentPropsWithoutRef<typeof CommandPrimitive>
|
|
||||||
>(({ className, ...props }, ref) => (
|
|
||||||
<CommandPrimitive
|
|
||||||
ref={ref}
|
|
||||||
className={cn(
|
|
||||||
"flex h-full w-full flex-col overflow-hidden rounded-md bg-popover text-popover-foreground",
|
|
||||||
className,
|
|
||||||
)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
));
|
|
||||||
Command.displayName = CommandPrimitive.displayName;
|
|
||||||
|
|
||||||
interface CommandDialogProps extends DialogProps {}
|
|
||||||
|
|
||||||
const CommandDialog = ({ children, ...props }: CommandDialogProps) => {
|
|
||||||
return (
|
|
||||||
<Dialog {...props}>
|
|
||||||
<DialogContent className="overflow-hidden p-0 shadow-lg">
|
|
||||||
<Command className="[&_[cmdk-group-heading]]:px-2 [&_[cmdk-group-heading]]:font-medium [&_[cmdk-group-heading]]:text-muted-foreground [&_[cmdk-group]:not([hidden])_~[cmdk-group]]:pt-0 [&_[cmdk-group]]:px-2 [&_[cmdk-input-wrapper]_svg]:h-5 [&_[cmdk-input-wrapper]_svg]:w-5 [&_[cmdk-input]]:h-12 [&_[cmdk-item]]:px-2 [&_[cmdk-item]]:py-3 [&_[cmdk-item]_svg]:h-5 [&_[cmdk-item]_svg]:w-5">
|
|
||||||
{children}
|
|
||||||
</Command>
|
|
||||||
</DialogContent>
|
|
||||||
</Dialog>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
const CommandInput = React.forwardRef<
|
|
||||||
React.ElementRef<typeof CommandPrimitive.Input>,
|
|
||||||
React.ComponentPropsWithoutRef<typeof CommandPrimitive.Input>
|
|
||||||
>(({ className, ...props }, ref) => (
|
|
||||||
<div className="flex items-center border-b px-3" cmdk-input-wrapper="">
|
|
||||||
<Search className="mr-2 h-4 w-4 shrink-0 opacity-50" />
|
|
||||||
<CommandPrimitive.Input
|
|
||||||
ref={ref}
|
|
||||||
className={cn(
|
|
||||||
"flex h-11 w-full rounded-md bg-transparent py-3 text-sm outline-none placeholder:text-muted-foreground disabled:cursor-not-allowed disabled:opacity-50",
|
|
||||||
className,
|
|
||||||
)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
));
|
|
||||||
|
|
||||||
CommandInput.displayName = CommandPrimitive.Input.displayName;
|
|
||||||
|
|
||||||
const CommandList = React.forwardRef<
|
|
||||||
React.ElementRef<typeof CommandPrimitive.List>,
|
|
||||||
React.ComponentPropsWithoutRef<typeof CommandPrimitive.List>
|
|
||||||
>(({ className, ...props }, ref) => (
|
|
||||||
<CommandPrimitive.List
|
|
||||||
ref={ref}
|
|
||||||
className={cn("max-h-[300px] overflow-y-auto overflow-x-hidden", className)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
));
|
|
||||||
|
|
||||||
CommandList.displayName = CommandPrimitive.List.displayName;
|
|
||||||
|
|
||||||
const CommandEmpty = React.forwardRef<
|
|
||||||
React.ElementRef<typeof CommandPrimitive.Empty>,
|
|
||||||
React.ComponentPropsWithoutRef<typeof CommandPrimitive.Empty>
|
|
||||||
>((props, ref) => <CommandPrimitive.Empty ref={ref} className="py-6 text-center text-sm" {...props} />);
|
|
||||||
|
|
||||||
CommandEmpty.displayName = CommandPrimitive.Empty.displayName;
|
|
||||||
|
|
||||||
const CommandGroup = React.forwardRef<
|
|
||||||
React.ElementRef<typeof CommandPrimitive.Group>,
|
|
||||||
React.ComponentPropsWithoutRef<typeof CommandPrimitive.Group>
|
|
||||||
>(({ className, ...props }, ref) => (
|
|
||||||
<CommandPrimitive.Group
|
|
||||||
ref={ref}
|
|
||||||
className={cn(
|
|
||||||
"overflow-hidden p-1 text-foreground [&_[cmdk-group-heading]]:px-2 [&_[cmdk-group-heading]]:py-1.5 [&_[cmdk-group-heading]]:text-xs [&_[cmdk-group-heading]]:font-medium [&_[cmdk-group-heading]]:text-muted-foreground",
|
|
||||||
className,
|
|
||||||
)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
));
|
|
||||||
|
|
||||||
CommandGroup.displayName = CommandPrimitive.Group.displayName;
|
|
||||||
|
|
||||||
const CommandSeparator = React.forwardRef<
|
|
||||||
React.ElementRef<typeof CommandPrimitive.Separator>,
|
|
||||||
React.ComponentPropsWithoutRef<typeof CommandPrimitive.Separator>
|
|
||||||
>(({ className, ...props }, ref) => (
|
|
||||||
<CommandPrimitive.Separator ref={ref} className={cn("-mx-1 h-px bg-border", className)} {...props} />
|
|
||||||
));
|
|
||||||
CommandSeparator.displayName = CommandPrimitive.Separator.displayName;
|
|
||||||
|
|
||||||
const CommandItem = React.forwardRef<
|
|
||||||
React.ElementRef<typeof CommandPrimitive.Item>,
|
|
||||||
React.ComponentPropsWithoutRef<typeof CommandPrimitive.Item>
|
|
||||||
>(({ className, ...props }, ref) => (
|
|
||||||
<CommandPrimitive.Item
|
|
||||||
ref={ref}
|
|
||||||
className={cn(
|
|
||||||
"relative flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none data-[disabled=true]:pointer-events-none data-[selected='true']:bg-accent data-[selected=true]:text-accent-foreground data-[disabled=true]:opacity-50",
|
|
||||||
className,
|
|
||||||
)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
));
|
|
||||||
|
|
||||||
CommandItem.displayName = CommandPrimitive.Item.displayName;
|
|
||||||
|
|
||||||
const CommandShortcut = ({ className, ...props }: React.HTMLAttributes<HTMLSpanElement>) => {
|
|
||||||
return <span className={cn("ml-auto text-xs tracking-widest text-muted-foreground", className)} {...props} />;
|
|
||||||
};
|
|
||||||
CommandShortcut.displayName = "CommandShortcut";
|
|
||||||
|
|
||||||
export {
|
|
||||||
Command,
|
|
||||||
CommandDialog,
|
|
||||||
CommandInput,
|
|
||||||
CommandList,
|
|
||||||
CommandEmpty,
|
|
||||||
CommandGroup,
|
|
||||||
CommandItem,
|
|
||||||
CommandShortcut,
|
|
||||||
CommandSeparator,
|
|
||||||
};
|
|
||||||
@@ -1,178 +0,0 @@
|
|||||||
import * as React from "react";
|
|
||||||
import * as ContextMenuPrimitive from "@radix-ui/react-context-menu";
|
|
||||||
import { Check, ChevronRight, Circle } from "lucide-react";
|
|
||||||
|
|
||||||
import { cn } from "@/lib/utils";
|
|
||||||
|
|
||||||
const ContextMenu = ContextMenuPrimitive.Root;
|
|
||||||
|
|
||||||
const ContextMenuTrigger = ContextMenuPrimitive.Trigger;
|
|
||||||
|
|
||||||
const ContextMenuGroup = ContextMenuPrimitive.Group;
|
|
||||||
|
|
||||||
const ContextMenuPortal = ContextMenuPrimitive.Portal;
|
|
||||||
|
|
||||||
const ContextMenuSub = ContextMenuPrimitive.Sub;
|
|
||||||
|
|
||||||
const ContextMenuRadioGroup = ContextMenuPrimitive.RadioGroup;
|
|
||||||
|
|
||||||
const ContextMenuSubTrigger = React.forwardRef<
|
|
||||||
React.ElementRef<typeof ContextMenuPrimitive.SubTrigger>,
|
|
||||||
React.ComponentPropsWithoutRef<typeof ContextMenuPrimitive.SubTrigger> & {
|
|
||||||
inset?: boolean;
|
|
||||||
}
|
|
||||||
>(({ className, inset, children, ...props }, ref) => (
|
|
||||||
<ContextMenuPrimitive.SubTrigger
|
|
||||||
ref={ref}
|
|
||||||
className={cn(
|
|
||||||
"flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none data-[state=open]:bg-accent data-[state=open]:text-accent-foreground focus:bg-accent focus:text-accent-foreground",
|
|
||||||
inset && "pl-8",
|
|
||||||
className,
|
|
||||||
)}
|
|
||||||
{...props}
|
|
||||||
>
|
|
||||||
{children}
|
|
||||||
<ChevronRight className="ml-auto h-4 w-4" />
|
|
||||||
</ContextMenuPrimitive.SubTrigger>
|
|
||||||
));
|
|
||||||
ContextMenuSubTrigger.displayName = ContextMenuPrimitive.SubTrigger.displayName;
|
|
||||||
|
|
||||||
const ContextMenuSubContent = React.forwardRef<
|
|
||||||
React.ElementRef<typeof ContextMenuPrimitive.SubContent>,
|
|
||||||
React.ComponentPropsWithoutRef<typeof ContextMenuPrimitive.SubContent>
|
|
||||||
>(({ className, ...props }, ref) => (
|
|
||||||
<ContextMenuPrimitive.SubContent
|
|
||||||
ref={ref}
|
|
||||||
className={cn(
|
|
||||||
"z-50 min-w-[8rem] overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-md data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
|
|
||||||
className,
|
|
||||||
)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
));
|
|
||||||
ContextMenuSubContent.displayName = ContextMenuPrimitive.SubContent.displayName;
|
|
||||||
|
|
||||||
const ContextMenuContent = React.forwardRef<
|
|
||||||
React.ElementRef<typeof ContextMenuPrimitive.Content>,
|
|
||||||
React.ComponentPropsWithoutRef<typeof ContextMenuPrimitive.Content>
|
|
||||||
>(({ className, ...props }, ref) => (
|
|
||||||
<ContextMenuPrimitive.Portal>
|
|
||||||
<ContextMenuPrimitive.Content
|
|
||||||
ref={ref}
|
|
||||||
className={cn(
|
|
||||||
"z-50 min-w-[8rem] overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-md animate-in fade-in-80 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
|
|
||||||
className,
|
|
||||||
)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
</ContextMenuPrimitive.Portal>
|
|
||||||
));
|
|
||||||
ContextMenuContent.displayName = ContextMenuPrimitive.Content.displayName;
|
|
||||||
|
|
||||||
const ContextMenuItem = React.forwardRef<
|
|
||||||
React.ElementRef<typeof ContextMenuPrimitive.Item>,
|
|
||||||
React.ComponentPropsWithoutRef<typeof ContextMenuPrimitive.Item> & {
|
|
||||||
inset?: boolean;
|
|
||||||
}
|
|
||||||
>(({ className, inset, ...props }, ref) => (
|
|
||||||
<ContextMenuPrimitive.Item
|
|
||||||
ref={ref}
|
|
||||||
className={cn(
|
|
||||||
"relative flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 focus:bg-accent focus:text-accent-foreground",
|
|
||||||
inset && "pl-8",
|
|
||||||
className,
|
|
||||||
)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
));
|
|
||||||
ContextMenuItem.displayName = ContextMenuPrimitive.Item.displayName;
|
|
||||||
|
|
||||||
const ContextMenuCheckboxItem = React.forwardRef<
|
|
||||||
React.ElementRef<typeof ContextMenuPrimitive.CheckboxItem>,
|
|
||||||
React.ComponentPropsWithoutRef<typeof ContextMenuPrimitive.CheckboxItem>
|
|
||||||
>(({ className, children, checked, ...props }, ref) => (
|
|
||||||
<ContextMenuPrimitive.CheckboxItem
|
|
||||||
ref={ref}
|
|
||||||
className={cn(
|
|
||||||
"relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 focus:bg-accent focus:text-accent-foreground",
|
|
||||||
className,
|
|
||||||
)}
|
|
||||||
checked={checked}
|
|
||||||
{...props}
|
|
||||||
>
|
|
||||||
<span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
|
|
||||||
<ContextMenuPrimitive.ItemIndicator>
|
|
||||||
<Check className="h-4 w-4" />
|
|
||||||
</ContextMenuPrimitive.ItemIndicator>
|
|
||||||
</span>
|
|
||||||
{children}
|
|
||||||
</ContextMenuPrimitive.CheckboxItem>
|
|
||||||
));
|
|
||||||
ContextMenuCheckboxItem.displayName = ContextMenuPrimitive.CheckboxItem.displayName;
|
|
||||||
|
|
||||||
const ContextMenuRadioItem = React.forwardRef<
|
|
||||||
React.ElementRef<typeof ContextMenuPrimitive.RadioItem>,
|
|
||||||
React.ComponentPropsWithoutRef<typeof ContextMenuPrimitive.RadioItem>
|
|
||||||
>(({ className, children, ...props }, ref) => (
|
|
||||||
<ContextMenuPrimitive.RadioItem
|
|
||||||
ref={ref}
|
|
||||||
className={cn(
|
|
||||||
"relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 focus:bg-accent focus:text-accent-foreground",
|
|
||||||
className,
|
|
||||||
)}
|
|
||||||
{...props}
|
|
||||||
>
|
|
||||||
<span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
|
|
||||||
<ContextMenuPrimitive.ItemIndicator>
|
|
||||||
<Circle className="h-2 w-2 fill-current" />
|
|
||||||
</ContextMenuPrimitive.ItemIndicator>
|
|
||||||
</span>
|
|
||||||
{children}
|
|
||||||
</ContextMenuPrimitive.RadioItem>
|
|
||||||
));
|
|
||||||
ContextMenuRadioItem.displayName = ContextMenuPrimitive.RadioItem.displayName;
|
|
||||||
|
|
||||||
const ContextMenuLabel = React.forwardRef<
|
|
||||||
React.ElementRef<typeof ContextMenuPrimitive.Label>,
|
|
||||||
React.ComponentPropsWithoutRef<typeof ContextMenuPrimitive.Label> & {
|
|
||||||
inset?: boolean;
|
|
||||||
}
|
|
||||||
>(({ className, inset, ...props }, ref) => (
|
|
||||||
<ContextMenuPrimitive.Label
|
|
||||||
ref={ref}
|
|
||||||
className={cn("px-2 py-1.5 text-sm font-semibold text-foreground", inset && "pl-8", className)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
));
|
|
||||||
ContextMenuLabel.displayName = ContextMenuPrimitive.Label.displayName;
|
|
||||||
|
|
||||||
const ContextMenuSeparator = React.forwardRef<
|
|
||||||
React.ElementRef<typeof ContextMenuPrimitive.Separator>,
|
|
||||||
React.ComponentPropsWithoutRef<typeof ContextMenuPrimitive.Separator>
|
|
||||||
>(({ className, ...props }, ref) => (
|
|
||||||
<ContextMenuPrimitive.Separator ref={ref} className={cn("-mx-1 my-1 h-px bg-border", className)} {...props} />
|
|
||||||
));
|
|
||||||
ContextMenuSeparator.displayName = ContextMenuPrimitive.Separator.displayName;
|
|
||||||
|
|
||||||
const ContextMenuShortcut = ({ className, ...props }: React.HTMLAttributes<HTMLSpanElement>) => {
|
|
||||||
return <span className={cn("ml-auto text-xs tracking-widest text-muted-foreground", className)} {...props} />;
|
|
||||||
};
|
|
||||||
ContextMenuShortcut.displayName = "ContextMenuShortcut";
|
|
||||||
|
|
||||||
export {
|
|
||||||
ContextMenu,
|
|
||||||
ContextMenuTrigger,
|
|
||||||
ContextMenuContent,
|
|
||||||
ContextMenuItem,
|
|
||||||
ContextMenuCheckboxItem,
|
|
||||||
ContextMenuRadioItem,
|
|
||||||
ContextMenuLabel,
|
|
||||||
ContextMenuSeparator,
|
|
||||||
ContextMenuShortcut,
|
|
||||||
ContextMenuGroup,
|
|
||||||
ContextMenuPortal,
|
|
||||||
ContextMenuSub,
|
|
||||||
ContextMenuSubContent,
|
|
||||||
ContextMenuSubTrigger,
|
|
||||||
ContextMenuRadioGroup,
|
|
||||||
};
|
|
||||||
@@ -1,95 +0,0 @@
|
|||||||
import * as React from "react";
|
|
||||||
import * as DialogPrimitive from "@radix-ui/react-dialog";
|
|
||||||
import { X } from "lucide-react";
|
|
||||||
|
|
||||||
import { cn } from "@/lib/utils";
|
|
||||||
|
|
||||||
const Dialog = DialogPrimitive.Root;
|
|
||||||
|
|
||||||
const DialogTrigger = DialogPrimitive.Trigger;
|
|
||||||
|
|
||||||
const DialogPortal = DialogPrimitive.Portal;
|
|
||||||
|
|
||||||
const DialogClose = DialogPrimitive.Close;
|
|
||||||
|
|
||||||
const DialogOverlay = React.forwardRef<
|
|
||||||
React.ElementRef<typeof DialogPrimitive.Overlay>,
|
|
||||||
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Overlay>
|
|
||||||
>(({ className, ...props }, ref) => (
|
|
||||||
<DialogPrimitive.Overlay
|
|
||||||
ref={ref}
|
|
||||||
className={cn(
|
|
||||||
"fixed inset-0 z-50 bg-black/80 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0",
|
|
||||||
className,
|
|
||||||
)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
));
|
|
||||||
DialogOverlay.displayName = DialogPrimitive.Overlay.displayName;
|
|
||||||
|
|
||||||
const DialogContent = React.forwardRef<
|
|
||||||
React.ElementRef<typeof DialogPrimitive.Content>,
|
|
||||||
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Content>
|
|
||||||
>(({ className, children, ...props }, ref) => (
|
|
||||||
<DialogPortal>
|
|
||||||
<DialogOverlay />
|
|
||||||
<DialogPrimitive.Content
|
|
||||||
ref={ref}
|
|
||||||
className={cn(
|
|
||||||
"fixed left-[50%] top-[50%] z-50 grid w-full max-w-lg translate-x-[-50%] translate-y-[-50%] gap-4 border bg-background p-6 shadow-lg duration-200 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-[48%] data-[state=open]:slide-in-from-left-1/2 data-[state=open]:slide-in-from-top-[48%] sm:rounded-lg",
|
|
||||||
className,
|
|
||||||
)}
|
|
||||||
{...props}
|
|
||||||
>
|
|
||||||
{children}
|
|
||||||
<DialogPrimitive.Close className="absolute right-4 top-4 rounded-sm opacity-70 ring-offset-background transition-opacity data-[state=open]:bg-accent data-[state=open]:text-muted-foreground hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:pointer-events-none">
|
|
||||||
<X className="h-4 w-4" />
|
|
||||||
<span className="sr-only">Close</span>
|
|
||||||
</DialogPrimitive.Close>
|
|
||||||
</DialogPrimitive.Content>
|
|
||||||
</DialogPortal>
|
|
||||||
));
|
|
||||||
DialogContent.displayName = DialogPrimitive.Content.displayName;
|
|
||||||
|
|
||||||
const DialogHeader = ({ className, ...props }: React.HTMLAttributes<HTMLDivElement>) => (
|
|
||||||
<div className={cn("flex flex-col space-y-1.5 text-center sm:text-left", className)} {...props} />
|
|
||||||
);
|
|
||||||
DialogHeader.displayName = "DialogHeader";
|
|
||||||
|
|
||||||
const DialogFooter = ({ className, ...props }: React.HTMLAttributes<HTMLDivElement>) => (
|
|
||||||
<div className={cn("flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2", className)} {...props} />
|
|
||||||
);
|
|
||||||
DialogFooter.displayName = "DialogFooter";
|
|
||||||
|
|
||||||
const DialogTitle = React.forwardRef<
|
|
||||||
React.ElementRef<typeof DialogPrimitive.Title>,
|
|
||||||
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Title>
|
|
||||||
>(({ className, ...props }, ref) => (
|
|
||||||
<DialogPrimitive.Title
|
|
||||||
ref={ref}
|
|
||||||
className={cn("text-lg font-semibold leading-none tracking-tight", className)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
));
|
|
||||||
DialogTitle.displayName = DialogPrimitive.Title.displayName;
|
|
||||||
|
|
||||||
const DialogDescription = React.forwardRef<
|
|
||||||
React.ElementRef<typeof DialogPrimitive.Description>,
|
|
||||||
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Description>
|
|
||||||
>(({ className, ...props }, ref) => (
|
|
||||||
<DialogPrimitive.Description ref={ref} className={cn("text-sm text-muted-foreground", className)} {...props} />
|
|
||||||
));
|
|
||||||
DialogDescription.displayName = DialogPrimitive.Description.displayName;
|
|
||||||
|
|
||||||
export {
|
|
||||||
Dialog,
|
|
||||||
DialogPortal,
|
|
||||||
DialogOverlay,
|
|
||||||
DialogClose,
|
|
||||||
DialogTrigger,
|
|
||||||
DialogContent,
|
|
||||||
DialogHeader,
|
|
||||||
DialogFooter,
|
|
||||||
DialogTitle,
|
|
||||||
DialogDescription,
|
|
||||||
};
|
|
||||||
@@ -1,87 +0,0 @@
|
|||||||
import * as React from "react";
|
|
||||||
import { Drawer as DrawerPrimitive } from "vaul";
|
|
||||||
|
|
||||||
import { cn } from "@/lib/utils";
|
|
||||||
|
|
||||||
const Drawer = ({ shouldScaleBackground = true, ...props }: React.ComponentProps<typeof DrawerPrimitive.Root>) => (
|
|
||||||
<DrawerPrimitive.Root shouldScaleBackground={shouldScaleBackground} {...props} />
|
|
||||||
);
|
|
||||||
Drawer.displayName = "Drawer";
|
|
||||||
|
|
||||||
const DrawerTrigger = DrawerPrimitive.Trigger;
|
|
||||||
|
|
||||||
const DrawerPortal = DrawerPrimitive.Portal;
|
|
||||||
|
|
||||||
const DrawerClose = DrawerPrimitive.Close;
|
|
||||||
|
|
||||||
const DrawerOverlay = React.forwardRef<
|
|
||||||
React.ElementRef<typeof DrawerPrimitive.Overlay>,
|
|
||||||
React.ComponentPropsWithoutRef<typeof DrawerPrimitive.Overlay>
|
|
||||||
>(({ className, ...props }, ref) => (
|
|
||||||
<DrawerPrimitive.Overlay ref={ref} className={cn("fixed inset-0 z-50 bg-black/80", className)} {...props} />
|
|
||||||
));
|
|
||||||
DrawerOverlay.displayName = DrawerPrimitive.Overlay.displayName;
|
|
||||||
|
|
||||||
const DrawerContent = React.forwardRef<
|
|
||||||
React.ElementRef<typeof DrawerPrimitive.Content>,
|
|
||||||
React.ComponentPropsWithoutRef<typeof DrawerPrimitive.Content>
|
|
||||||
>(({ className, children, ...props }, ref) => (
|
|
||||||
<DrawerPortal>
|
|
||||||
<DrawerOverlay />
|
|
||||||
<DrawerPrimitive.Content
|
|
||||||
ref={ref}
|
|
||||||
className={cn(
|
|
||||||
"fixed inset-x-0 bottom-0 z-50 mt-24 flex h-auto flex-col rounded-t-[10px] border bg-background",
|
|
||||||
className,
|
|
||||||
)}
|
|
||||||
{...props}
|
|
||||||
>
|
|
||||||
<div className="mx-auto mt-4 h-2 w-[100px] rounded-full bg-muted" />
|
|
||||||
{children}
|
|
||||||
</DrawerPrimitive.Content>
|
|
||||||
</DrawerPortal>
|
|
||||||
));
|
|
||||||
DrawerContent.displayName = "DrawerContent";
|
|
||||||
|
|
||||||
const DrawerHeader = ({ className, ...props }: React.HTMLAttributes<HTMLDivElement>) => (
|
|
||||||
<div className={cn("grid gap-1.5 p-4 text-center sm:text-left", className)} {...props} />
|
|
||||||
);
|
|
||||||
DrawerHeader.displayName = "DrawerHeader";
|
|
||||||
|
|
||||||
const DrawerFooter = ({ className, ...props }: React.HTMLAttributes<HTMLDivElement>) => (
|
|
||||||
<div className={cn("mt-auto flex flex-col gap-2 p-4", className)} {...props} />
|
|
||||||
);
|
|
||||||
DrawerFooter.displayName = "DrawerFooter";
|
|
||||||
|
|
||||||
const DrawerTitle = React.forwardRef<
|
|
||||||
React.ElementRef<typeof DrawerPrimitive.Title>,
|
|
||||||
React.ComponentPropsWithoutRef<typeof DrawerPrimitive.Title>
|
|
||||||
>(({ className, ...props }, ref) => (
|
|
||||||
<DrawerPrimitive.Title
|
|
||||||
ref={ref}
|
|
||||||
className={cn("text-lg font-semibold leading-none tracking-tight", className)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
));
|
|
||||||
DrawerTitle.displayName = DrawerPrimitive.Title.displayName;
|
|
||||||
|
|
||||||
const DrawerDescription = React.forwardRef<
|
|
||||||
React.ElementRef<typeof DrawerPrimitive.Description>,
|
|
||||||
React.ComponentPropsWithoutRef<typeof DrawerPrimitive.Description>
|
|
||||||
>(({ className, ...props }, ref) => (
|
|
||||||
<DrawerPrimitive.Description ref={ref} className={cn("text-sm text-muted-foreground", className)} {...props} />
|
|
||||||
));
|
|
||||||
DrawerDescription.displayName = DrawerPrimitive.Description.displayName;
|
|
||||||
|
|
||||||
export {
|
|
||||||
Drawer,
|
|
||||||
DrawerPortal,
|
|
||||||
DrawerOverlay,
|
|
||||||
DrawerTrigger,
|
|
||||||
DrawerClose,
|
|
||||||
DrawerContent,
|
|
||||||
DrawerHeader,
|
|
||||||
DrawerFooter,
|
|
||||||
DrawerTitle,
|
|
||||||
DrawerDescription,
|
|
||||||
};
|
|
||||||
@@ -1,179 +0,0 @@
|
|||||||
import * as React from "react";
|
|
||||||
import * as DropdownMenuPrimitive from "@radix-ui/react-dropdown-menu";
|
|
||||||
import { Check, ChevronRight, Circle } from "lucide-react";
|
|
||||||
|
|
||||||
import { cn } from "@/lib/utils";
|
|
||||||
|
|
||||||
const DropdownMenu = DropdownMenuPrimitive.Root;
|
|
||||||
|
|
||||||
const DropdownMenuTrigger = DropdownMenuPrimitive.Trigger;
|
|
||||||
|
|
||||||
const DropdownMenuGroup = DropdownMenuPrimitive.Group;
|
|
||||||
|
|
||||||
const DropdownMenuPortal = DropdownMenuPrimitive.Portal;
|
|
||||||
|
|
||||||
const DropdownMenuSub = DropdownMenuPrimitive.Sub;
|
|
||||||
|
|
||||||
const DropdownMenuRadioGroup = DropdownMenuPrimitive.RadioGroup;
|
|
||||||
|
|
||||||
const DropdownMenuSubTrigger = React.forwardRef<
|
|
||||||
React.ElementRef<typeof DropdownMenuPrimitive.SubTrigger>,
|
|
||||||
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.SubTrigger> & {
|
|
||||||
inset?: boolean;
|
|
||||||
}
|
|
||||||
>(({ className, inset, children, ...props }, ref) => (
|
|
||||||
<DropdownMenuPrimitive.SubTrigger
|
|
||||||
ref={ref}
|
|
||||||
className={cn(
|
|
||||||
"flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none data-[state=open]:bg-accent focus:bg-accent",
|
|
||||||
inset && "pl-8",
|
|
||||||
className,
|
|
||||||
)}
|
|
||||||
{...props}
|
|
||||||
>
|
|
||||||
{children}
|
|
||||||
<ChevronRight className="ml-auto h-4 w-4" />
|
|
||||||
</DropdownMenuPrimitive.SubTrigger>
|
|
||||||
));
|
|
||||||
DropdownMenuSubTrigger.displayName = DropdownMenuPrimitive.SubTrigger.displayName;
|
|
||||||
|
|
||||||
const DropdownMenuSubContent = React.forwardRef<
|
|
||||||
React.ElementRef<typeof DropdownMenuPrimitive.SubContent>,
|
|
||||||
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.SubContent>
|
|
||||||
>(({ className, ...props }, ref) => (
|
|
||||||
<DropdownMenuPrimitive.SubContent
|
|
||||||
ref={ref}
|
|
||||||
className={cn(
|
|
||||||
"z-50 min-w-[8rem] overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-lg data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
|
|
||||||
className,
|
|
||||||
)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
));
|
|
||||||
DropdownMenuSubContent.displayName = DropdownMenuPrimitive.SubContent.displayName;
|
|
||||||
|
|
||||||
const DropdownMenuContent = React.forwardRef<
|
|
||||||
React.ElementRef<typeof DropdownMenuPrimitive.Content>,
|
|
||||||
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Content>
|
|
||||||
>(({ className, sideOffset = 4, ...props }, ref) => (
|
|
||||||
<DropdownMenuPrimitive.Portal>
|
|
||||||
<DropdownMenuPrimitive.Content
|
|
||||||
ref={ref}
|
|
||||||
sideOffset={sideOffset}
|
|
||||||
className={cn(
|
|
||||||
"z-50 min-w-[8rem] overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-md data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
|
|
||||||
className,
|
|
||||||
)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
</DropdownMenuPrimitive.Portal>
|
|
||||||
));
|
|
||||||
DropdownMenuContent.displayName = DropdownMenuPrimitive.Content.displayName;
|
|
||||||
|
|
||||||
const DropdownMenuItem = React.forwardRef<
|
|
||||||
React.ElementRef<typeof DropdownMenuPrimitive.Item>,
|
|
||||||
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Item> & {
|
|
||||||
inset?: boolean;
|
|
||||||
}
|
|
||||||
>(({ className, inset, ...props }, ref) => (
|
|
||||||
<DropdownMenuPrimitive.Item
|
|
||||||
ref={ref}
|
|
||||||
className={cn(
|
|
||||||
"relative flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none transition-colors data-[disabled]:pointer-events-none data-[disabled]:opacity-50 focus:bg-accent focus:text-accent-foreground",
|
|
||||||
inset && "pl-8",
|
|
||||||
className,
|
|
||||||
)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
));
|
|
||||||
DropdownMenuItem.displayName = DropdownMenuPrimitive.Item.displayName;
|
|
||||||
|
|
||||||
const DropdownMenuCheckboxItem = React.forwardRef<
|
|
||||||
React.ElementRef<typeof DropdownMenuPrimitive.CheckboxItem>,
|
|
||||||
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.CheckboxItem>
|
|
||||||
>(({ className, children, checked, ...props }, ref) => (
|
|
||||||
<DropdownMenuPrimitive.CheckboxItem
|
|
||||||
ref={ref}
|
|
||||||
className={cn(
|
|
||||||
"relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none transition-colors data-[disabled]:pointer-events-none data-[disabled]:opacity-50 focus:bg-accent focus:text-accent-foreground",
|
|
||||||
className,
|
|
||||||
)}
|
|
||||||
checked={checked}
|
|
||||||
{...props}
|
|
||||||
>
|
|
||||||
<span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
|
|
||||||
<DropdownMenuPrimitive.ItemIndicator>
|
|
||||||
<Check className="h-4 w-4" />
|
|
||||||
</DropdownMenuPrimitive.ItemIndicator>
|
|
||||||
</span>
|
|
||||||
{children}
|
|
||||||
</DropdownMenuPrimitive.CheckboxItem>
|
|
||||||
));
|
|
||||||
DropdownMenuCheckboxItem.displayName = DropdownMenuPrimitive.CheckboxItem.displayName;
|
|
||||||
|
|
||||||
const DropdownMenuRadioItem = React.forwardRef<
|
|
||||||
React.ElementRef<typeof DropdownMenuPrimitive.RadioItem>,
|
|
||||||
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.RadioItem>
|
|
||||||
>(({ className, children, ...props }, ref) => (
|
|
||||||
<DropdownMenuPrimitive.RadioItem
|
|
||||||
ref={ref}
|
|
||||||
className={cn(
|
|
||||||
"relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none transition-colors data-[disabled]:pointer-events-none data-[disabled]:opacity-50 focus:bg-accent focus:text-accent-foreground",
|
|
||||||
className,
|
|
||||||
)}
|
|
||||||
{...props}
|
|
||||||
>
|
|
||||||
<span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
|
|
||||||
<DropdownMenuPrimitive.ItemIndicator>
|
|
||||||
<Circle className="h-2 w-2 fill-current" />
|
|
||||||
</DropdownMenuPrimitive.ItemIndicator>
|
|
||||||
</span>
|
|
||||||
{children}
|
|
||||||
</DropdownMenuPrimitive.RadioItem>
|
|
||||||
));
|
|
||||||
DropdownMenuRadioItem.displayName = DropdownMenuPrimitive.RadioItem.displayName;
|
|
||||||
|
|
||||||
const DropdownMenuLabel = React.forwardRef<
|
|
||||||
React.ElementRef<typeof DropdownMenuPrimitive.Label>,
|
|
||||||
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Label> & {
|
|
||||||
inset?: boolean;
|
|
||||||
}
|
|
||||||
>(({ className, inset, ...props }, ref) => (
|
|
||||||
<DropdownMenuPrimitive.Label
|
|
||||||
ref={ref}
|
|
||||||
className={cn("px-2 py-1.5 text-sm font-semibold", inset && "pl-8", className)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
));
|
|
||||||
DropdownMenuLabel.displayName = DropdownMenuPrimitive.Label.displayName;
|
|
||||||
|
|
||||||
const DropdownMenuSeparator = React.forwardRef<
|
|
||||||
React.ElementRef<typeof DropdownMenuPrimitive.Separator>,
|
|
||||||
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Separator>
|
|
||||||
>(({ className, ...props }, ref) => (
|
|
||||||
<DropdownMenuPrimitive.Separator ref={ref} className={cn("-mx-1 my-1 h-px bg-muted", className)} {...props} />
|
|
||||||
));
|
|
||||||
DropdownMenuSeparator.displayName = DropdownMenuPrimitive.Separator.displayName;
|
|
||||||
|
|
||||||
const DropdownMenuShortcut = ({ className, ...props }: React.HTMLAttributes<HTMLSpanElement>) => {
|
|
||||||
return <span className={cn("ml-auto text-xs tracking-widest opacity-60", className)} {...props} />;
|
|
||||||
};
|
|
||||||
DropdownMenuShortcut.displayName = "DropdownMenuShortcut";
|
|
||||||
|
|
||||||
export {
|
|
||||||
DropdownMenu,
|
|
||||||
DropdownMenuTrigger,
|
|
||||||
DropdownMenuContent,
|
|
||||||
DropdownMenuItem,
|
|
||||||
DropdownMenuCheckboxItem,
|
|
||||||
DropdownMenuRadioItem,
|
|
||||||
DropdownMenuLabel,
|
|
||||||
DropdownMenuSeparator,
|
|
||||||
DropdownMenuShortcut,
|
|
||||||
DropdownMenuGroup,
|
|
||||||
DropdownMenuPortal,
|
|
||||||
DropdownMenuSub,
|
|
||||||
DropdownMenuSubContent,
|
|
||||||
DropdownMenuSubTrigger,
|
|
||||||
DropdownMenuRadioGroup,
|
|
||||||
};
|
|
||||||
@@ -1,129 +0,0 @@
|
|||||||
import * as React from "react";
|
|
||||||
import * as LabelPrimitive from "@radix-ui/react-label";
|
|
||||||
import { Slot } from "@radix-ui/react-slot";
|
|
||||||
import { Controller, ControllerProps, FieldPath, FieldValues, FormProvider, useFormContext } from "react-hook-form";
|
|
||||||
|
|
||||||
import { cn } from "@/lib/utils";
|
|
||||||
import { Label } from "@/components/ui/label";
|
|
||||||
|
|
||||||
const Form = FormProvider;
|
|
||||||
|
|
||||||
type FormFieldContextValue<
|
|
||||||
TFieldValues extends FieldValues = FieldValues,
|
|
||||||
TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>,
|
|
||||||
> = {
|
|
||||||
name: TName;
|
|
||||||
};
|
|
||||||
|
|
||||||
const FormFieldContext = React.createContext<FormFieldContextValue>({} as FormFieldContextValue);
|
|
||||||
|
|
||||||
const FormField = <
|
|
||||||
TFieldValues extends FieldValues = FieldValues,
|
|
||||||
TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>,
|
|
||||||
>({
|
|
||||||
...props
|
|
||||||
}: ControllerProps<TFieldValues, TName>) => {
|
|
||||||
return (
|
|
||||||
<FormFieldContext.Provider value={{ name: props.name }}>
|
|
||||||
<Controller {...props} />
|
|
||||||
</FormFieldContext.Provider>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
const useFormField = () => {
|
|
||||||
const fieldContext = React.useContext(FormFieldContext);
|
|
||||||
const itemContext = React.useContext(FormItemContext);
|
|
||||||
const { getFieldState, formState } = useFormContext();
|
|
||||||
|
|
||||||
const fieldState = getFieldState(fieldContext.name, formState);
|
|
||||||
|
|
||||||
if (!fieldContext) {
|
|
||||||
throw new Error("useFormField should be used within <FormField>");
|
|
||||||
}
|
|
||||||
|
|
||||||
const { id } = itemContext;
|
|
||||||
|
|
||||||
return {
|
|
||||||
id,
|
|
||||||
name: fieldContext.name,
|
|
||||||
formItemId: `${id}-form-item`,
|
|
||||||
formDescriptionId: `${id}-form-item-description`,
|
|
||||||
formMessageId: `${id}-form-item-message`,
|
|
||||||
...fieldState,
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
type FormItemContextValue = {
|
|
||||||
id: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
const FormItemContext = React.createContext<FormItemContextValue>({} as FormItemContextValue);
|
|
||||||
|
|
||||||
const FormItem = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(
|
|
||||||
({ className, ...props }, ref) => {
|
|
||||||
const id = React.useId();
|
|
||||||
|
|
||||||
return (
|
|
||||||
<FormItemContext.Provider value={{ id }}>
|
|
||||||
<div ref={ref} className={cn("space-y-2", className)} {...props} />
|
|
||||||
</FormItemContext.Provider>
|
|
||||||
);
|
|
||||||
},
|
|
||||||
);
|
|
||||||
FormItem.displayName = "FormItem";
|
|
||||||
|
|
||||||
const FormLabel = React.forwardRef<
|
|
||||||
React.ElementRef<typeof LabelPrimitive.Root>,
|
|
||||||
React.ComponentPropsWithoutRef<typeof LabelPrimitive.Root>
|
|
||||||
>(({ className, ...props }, ref) => {
|
|
||||||
const { error, formItemId } = useFormField();
|
|
||||||
|
|
||||||
return <Label ref={ref} className={cn(error && "text-destructive", className)} htmlFor={formItemId} {...props} />;
|
|
||||||
});
|
|
||||||
FormLabel.displayName = "FormLabel";
|
|
||||||
|
|
||||||
const FormControl = React.forwardRef<React.ElementRef<typeof Slot>, React.ComponentPropsWithoutRef<typeof Slot>>(
|
|
||||||
({ ...props }, ref) => {
|
|
||||||
const { error, formItemId, formDescriptionId, formMessageId } = useFormField();
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Slot
|
|
||||||
ref={ref}
|
|
||||||
id={formItemId}
|
|
||||||
aria-describedby={!error ? `${formDescriptionId}` : `${formDescriptionId} ${formMessageId}`}
|
|
||||||
aria-invalid={!!error}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
},
|
|
||||||
);
|
|
||||||
FormControl.displayName = "FormControl";
|
|
||||||
|
|
||||||
const FormDescription = React.forwardRef<HTMLParagraphElement, React.HTMLAttributes<HTMLParagraphElement>>(
|
|
||||||
({ className, ...props }, ref) => {
|
|
||||||
const { formDescriptionId } = useFormField();
|
|
||||||
|
|
||||||
return <p ref={ref} id={formDescriptionId} className={cn("text-sm text-muted-foreground", className)} {...props} />;
|
|
||||||
},
|
|
||||||
);
|
|
||||||
FormDescription.displayName = "FormDescription";
|
|
||||||
|
|
||||||
const FormMessage = React.forwardRef<HTMLParagraphElement, React.HTMLAttributes<HTMLParagraphElement>>(
|
|
||||||
({ className, children, ...props }, ref) => {
|
|
||||||
const { error, formMessageId } = useFormField();
|
|
||||||
const body = error ? String(error?.message) : children;
|
|
||||||
|
|
||||||
if (!body) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<p ref={ref} id={formMessageId} className={cn("text-sm font-medium text-destructive", className)} {...props}>
|
|
||||||
{body}
|
|
||||||
</p>
|
|
||||||
);
|
|
||||||
},
|
|
||||||
);
|
|
||||||
FormMessage.displayName = "FormMessage";
|
|
||||||
|
|
||||||
export { useFormField, Form, FormItem, FormLabel, FormControl, FormDescription, FormMessage, FormField };
|
|
||||||
@@ -1,27 +0,0 @@
|
|||||||
import * as React from "react";
|
|
||||||
import * as HoverCardPrimitive from "@radix-ui/react-hover-card";
|
|
||||||
|
|
||||||
import { cn } from "@/lib/utils";
|
|
||||||
|
|
||||||
const HoverCard = HoverCardPrimitive.Root;
|
|
||||||
|
|
||||||
const HoverCardTrigger = HoverCardPrimitive.Trigger;
|
|
||||||
|
|
||||||
const HoverCardContent = React.forwardRef<
|
|
||||||
React.ElementRef<typeof HoverCardPrimitive.Content>,
|
|
||||||
React.ComponentPropsWithoutRef<typeof HoverCardPrimitive.Content>
|
|
||||||
>(({ className, align = "center", sideOffset = 4, ...props }, ref) => (
|
|
||||||
<HoverCardPrimitive.Content
|
|
||||||
ref={ref}
|
|
||||||
align={align}
|
|
||||||
sideOffset={sideOffset}
|
|
||||||
className={cn(
|
|
||||||
"z-50 w-64 rounded-md border bg-popover p-4 text-popover-foreground shadow-md outline-none data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
|
|
||||||
className,
|
|
||||||
)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
));
|
|
||||||
HoverCardContent.displayName = HoverCardPrimitive.Content.displayName;
|
|
||||||
|
|
||||||
export { HoverCard, HoverCardTrigger, HoverCardContent };
|
|
||||||
@@ -1,61 +0,0 @@
|
|||||||
import * as React from "react";
|
|
||||||
import { OTPInput, OTPInputContext } from "input-otp";
|
|
||||||
import { Dot } from "lucide-react";
|
|
||||||
|
|
||||||
import { cn } from "@/lib/utils";
|
|
||||||
|
|
||||||
const InputOTP = React.forwardRef<React.ElementRef<typeof OTPInput>, React.ComponentPropsWithoutRef<typeof OTPInput>>(
|
|
||||||
({ className, containerClassName, ...props }, ref) => (
|
|
||||||
<OTPInput
|
|
||||||
ref={ref}
|
|
||||||
containerClassName={cn("flex items-center gap-2 has-[:disabled]:opacity-50", containerClassName)}
|
|
||||||
className={cn("disabled:cursor-not-allowed", className)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
),
|
|
||||||
);
|
|
||||||
InputOTP.displayName = "InputOTP";
|
|
||||||
|
|
||||||
const InputOTPGroup = React.forwardRef<React.ElementRef<"div">, React.ComponentPropsWithoutRef<"div">>(
|
|
||||||
({ className, ...props }, ref) => <div ref={ref} className={cn("flex items-center", className)} {...props} />,
|
|
||||||
);
|
|
||||||
InputOTPGroup.displayName = "InputOTPGroup";
|
|
||||||
|
|
||||||
const InputOTPSlot = React.forwardRef<
|
|
||||||
React.ElementRef<"div">,
|
|
||||||
React.ComponentPropsWithoutRef<"div"> & { index: number }
|
|
||||||
>(({ index, className, ...props }, ref) => {
|
|
||||||
const inputOTPContext = React.useContext(OTPInputContext);
|
|
||||||
const { char, hasFakeCaret, isActive } = inputOTPContext.slots[index];
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
ref={ref}
|
|
||||||
className={cn(
|
|
||||||
"relative flex h-10 w-10 items-center justify-center border-y border-r border-input text-sm transition-all first:rounded-l-md first:border-l last:rounded-r-md",
|
|
||||||
isActive && "z-10 ring-2 ring-ring ring-offset-background",
|
|
||||||
className,
|
|
||||||
)}
|
|
||||||
{...props}
|
|
||||||
>
|
|
||||||
{char}
|
|
||||||
{hasFakeCaret && (
|
|
||||||
<div className="pointer-events-none absolute inset-0 flex items-center justify-center">
|
|
||||||
<div className="animate-caret-blink h-4 w-px bg-foreground duration-1000" />
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
});
|
|
||||||
InputOTPSlot.displayName = "InputOTPSlot";
|
|
||||||
|
|
||||||
const InputOTPSeparator = React.forwardRef<React.ElementRef<"div">, React.ComponentPropsWithoutRef<"div">>(
|
|
||||||
({ ...props }, ref) => (
|
|
||||||
<div ref={ref} role="separator" {...props}>
|
|
||||||
<Dot />
|
|
||||||
</div>
|
|
||||||
),
|
|
||||||
);
|
|
||||||
InputOTPSeparator.displayName = "InputOTPSeparator";
|
|
||||||
|
|
||||||
export { InputOTP, InputOTPGroup, InputOTPSlot, InputOTPSeparator };
|
|
||||||
+13
-16
@@ -1,22 +1,19 @@
|
|||||||
import * as React from "react";
|
import * as React from "react";
|
||||||
|
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
|
|
||||||
const Input = React.forwardRef<HTMLInputElement, React.ComponentProps<"input">>(
|
export const Input = React.forwardRef<HTMLInputElement, React.InputHTMLAttributes<HTMLInputElement>>(
|
||||||
({ className, type, ...props }, ref) => {
|
({ className, type = "text", ...props }, ref) => (
|
||||||
return (
|
<input
|
||||||
<input
|
ref={ref}
|
||||||
type={type}
|
type={type}
|
||||||
className={cn(
|
className={cn(
|
||||||
"flex h-10 w-full rounded-md border border-input bg-background px-3 py-2 text-base ring-offset-background file:border-0 file:bg-transparent file:text-sm file:font-medium file:text-foreground placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 md:text-sm",
|
"flex h-10 w-full rounded-md border border-slate-300 bg-white px-3 py-2 text-sm placeholder:text-slate-400 focus:outline-none focus:ring-2 focus:ring-indigo-500 disabled:opacity-50",
|
||||||
className,
|
className
|
||||||
)}
|
)}
|
||||||
ref={ref}
|
{...props}
|
||||||
{...props}
|
/>
|
||||||
/>
|
)
|
||||||
);
|
|
||||||
},
|
|
||||||
);
|
);
|
||||||
Input.displayName = "Input";
|
Input.displayName = "Input";
|
||||||
|
|
||||||
export { Input };
|
export default Input;
|
||||||
|
|||||||
@@ -1,17 +0,0 @@
|
|||||||
import * as React from "react";
|
|
||||||
import * as LabelPrimitive from "@radix-ui/react-label";
|
|
||||||
import { cva, type VariantProps } from "class-variance-authority";
|
|
||||||
|
|
||||||
import { cn } from "@/lib/utils";
|
|
||||||
|
|
||||||
const labelVariants = cva("text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70");
|
|
||||||
|
|
||||||
const Label = React.forwardRef<
|
|
||||||
React.ElementRef<typeof LabelPrimitive.Root>,
|
|
||||||
React.ComponentPropsWithoutRef<typeof LabelPrimitive.Root> & VariantProps<typeof labelVariants>
|
|
||||||
>(({ className, ...props }, ref) => (
|
|
||||||
<LabelPrimitive.Root ref={ref} className={cn(labelVariants(), className)} {...props} />
|
|
||||||
));
|
|
||||||
Label.displayName = LabelPrimitive.Root.displayName;
|
|
||||||
|
|
||||||
export { Label };
|
|
||||||
@@ -1,207 +0,0 @@
|
|||||||
import * as React from "react";
|
|
||||||
import * as MenubarPrimitive from "@radix-ui/react-menubar";
|
|
||||||
import { Check, ChevronRight, Circle } from "lucide-react";
|
|
||||||
|
|
||||||
import { cn } from "@/lib/utils";
|
|
||||||
|
|
||||||
const MenubarMenu = MenubarPrimitive.Menu;
|
|
||||||
|
|
||||||
const MenubarGroup = MenubarPrimitive.Group;
|
|
||||||
|
|
||||||
const MenubarPortal = MenubarPrimitive.Portal;
|
|
||||||
|
|
||||||
const MenubarSub = MenubarPrimitive.Sub;
|
|
||||||
|
|
||||||
const MenubarRadioGroup = MenubarPrimitive.RadioGroup;
|
|
||||||
|
|
||||||
const Menubar = React.forwardRef<
|
|
||||||
React.ElementRef<typeof MenubarPrimitive.Root>,
|
|
||||||
React.ComponentPropsWithoutRef<typeof MenubarPrimitive.Root>
|
|
||||||
>(({ className, ...props }, ref) => (
|
|
||||||
<MenubarPrimitive.Root
|
|
||||||
ref={ref}
|
|
||||||
className={cn("flex h-10 items-center space-x-1 rounded-md border bg-background p-1", className)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
));
|
|
||||||
Menubar.displayName = MenubarPrimitive.Root.displayName;
|
|
||||||
|
|
||||||
const MenubarTrigger = React.forwardRef<
|
|
||||||
React.ElementRef<typeof MenubarPrimitive.Trigger>,
|
|
||||||
React.ComponentPropsWithoutRef<typeof MenubarPrimitive.Trigger>
|
|
||||||
>(({ className, ...props }, ref) => (
|
|
||||||
<MenubarPrimitive.Trigger
|
|
||||||
ref={ref}
|
|
||||||
className={cn(
|
|
||||||
"flex cursor-default select-none items-center rounded-sm px-3 py-1.5 text-sm font-medium outline-none data-[state=open]:bg-accent data-[state=open]:text-accent-foreground focus:bg-accent focus:text-accent-foreground",
|
|
||||||
className,
|
|
||||||
)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
));
|
|
||||||
MenubarTrigger.displayName = MenubarPrimitive.Trigger.displayName;
|
|
||||||
|
|
||||||
const MenubarSubTrigger = React.forwardRef<
|
|
||||||
React.ElementRef<typeof MenubarPrimitive.SubTrigger>,
|
|
||||||
React.ComponentPropsWithoutRef<typeof MenubarPrimitive.SubTrigger> & {
|
|
||||||
inset?: boolean;
|
|
||||||
}
|
|
||||||
>(({ className, inset, children, ...props }, ref) => (
|
|
||||||
<MenubarPrimitive.SubTrigger
|
|
||||||
ref={ref}
|
|
||||||
className={cn(
|
|
||||||
"flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none data-[state=open]:bg-accent data-[state=open]:text-accent-foreground focus:bg-accent focus:text-accent-foreground",
|
|
||||||
inset && "pl-8",
|
|
||||||
className,
|
|
||||||
)}
|
|
||||||
{...props}
|
|
||||||
>
|
|
||||||
{children}
|
|
||||||
<ChevronRight className="ml-auto h-4 w-4" />
|
|
||||||
</MenubarPrimitive.SubTrigger>
|
|
||||||
));
|
|
||||||
MenubarSubTrigger.displayName = MenubarPrimitive.SubTrigger.displayName;
|
|
||||||
|
|
||||||
const MenubarSubContent = React.forwardRef<
|
|
||||||
React.ElementRef<typeof MenubarPrimitive.SubContent>,
|
|
||||||
React.ComponentPropsWithoutRef<typeof MenubarPrimitive.SubContent>
|
|
||||||
>(({ className, ...props }, ref) => (
|
|
||||||
<MenubarPrimitive.SubContent
|
|
||||||
ref={ref}
|
|
||||||
className={cn(
|
|
||||||
"z-50 min-w-[8rem] overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
|
|
||||||
className,
|
|
||||||
)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
));
|
|
||||||
MenubarSubContent.displayName = MenubarPrimitive.SubContent.displayName;
|
|
||||||
|
|
||||||
const MenubarContent = React.forwardRef<
|
|
||||||
React.ElementRef<typeof MenubarPrimitive.Content>,
|
|
||||||
React.ComponentPropsWithoutRef<typeof MenubarPrimitive.Content>
|
|
||||||
>(({ className, align = "start", alignOffset = -4, sideOffset = 8, ...props }, ref) => (
|
|
||||||
<MenubarPrimitive.Portal>
|
|
||||||
<MenubarPrimitive.Content
|
|
||||||
ref={ref}
|
|
||||||
align={align}
|
|
||||||
alignOffset={alignOffset}
|
|
||||||
sideOffset={sideOffset}
|
|
||||||
className={cn(
|
|
||||||
"z-50 min-w-[12rem] overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-md data-[state=open]:animate-in data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
|
|
||||||
className,
|
|
||||||
)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
</MenubarPrimitive.Portal>
|
|
||||||
));
|
|
||||||
MenubarContent.displayName = MenubarPrimitive.Content.displayName;
|
|
||||||
|
|
||||||
const MenubarItem = React.forwardRef<
|
|
||||||
React.ElementRef<typeof MenubarPrimitive.Item>,
|
|
||||||
React.ComponentPropsWithoutRef<typeof MenubarPrimitive.Item> & {
|
|
||||||
inset?: boolean;
|
|
||||||
}
|
|
||||||
>(({ className, inset, ...props }, ref) => (
|
|
||||||
<MenubarPrimitive.Item
|
|
||||||
ref={ref}
|
|
||||||
className={cn(
|
|
||||||
"relative flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 focus:bg-accent focus:text-accent-foreground",
|
|
||||||
inset && "pl-8",
|
|
||||||
className,
|
|
||||||
)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
));
|
|
||||||
MenubarItem.displayName = MenubarPrimitive.Item.displayName;
|
|
||||||
|
|
||||||
const MenubarCheckboxItem = React.forwardRef<
|
|
||||||
React.ElementRef<typeof MenubarPrimitive.CheckboxItem>,
|
|
||||||
React.ComponentPropsWithoutRef<typeof MenubarPrimitive.CheckboxItem>
|
|
||||||
>(({ className, children, checked, ...props }, ref) => (
|
|
||||||
<MenubarPrimitive.CheckboxItem
|
|
||||||
ref={ref}
|
|
||||||
className={cn(
|
|
||||||
"relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 focus:bg-accent focus:text-accent-foreground",
|
|
||||||
className,
|
|
||||||
)}
|
|
||||||
checked={checked}
|
|
||||||
{...props}
|
|
||||||
>
|
|
||||||
<span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
|
|
||||||
<MenubarPrimitive.ItemIndicator>
|
|
||||||
<Check className="h-4 w-4" />
|
|
||||||
</MenubarPrimitive.ItemIndicator>
|
|
||||||
</span>
|
|
||||||
{children}
|
|
||||||
</MenubarPrimitive.CheckboxItem>
|
|
||||||
));
|
|
||||||
MenubarCheckboxItem.displayName = MenubarPrimitive.CheckboxItem.displayName;
|
|
||||||
|
|
||||||
const MenubarRadioItem = React.forwardRef<
|
|
||||||
React.ElementRef<typeof MenubarPrimitive.RadioItem>,
|
|
||||||
React.ComponentPropsWithoutRef<typeof MenubarPrimitive.RadioItem>
|
|
||||||
>(({ className, children, ...props }, ref) => (
|
|
||||||
<MenubarPrimitive.RadioItem
|
|
||||||
ref={ref}
|
|
||||||
className={cn(
|
|
||||||
"relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 focus:bg-accent focus:text-accent-foreground",
|
|
||||||
className,
|
|
||||||
)}
|
|
||||||
{...props}
|
|
||||||
>
|
|
||||||
<span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
|
|
||||||
<MenubarPrimitive.ItemIndicator>
|
|
||||||
<Circle className="h-2 w-2 fill-current" />
|
|
||||||
</MenubarPrimitive.ItemIndicator>
|
|
||||||
</span>
|
|
||||||
{children}
|
|
||||||
</MenubarPrimitive.RadioItem>
|
|
||||||
));
|
|
||||||
MenubarRadioItem.displayName = MenubarPrimitive.RadioItem.displayName;
|
|
||||||
|
|
||||||
const MenubarLabel = React.forwardRef<
|
|
||||||
React.ElementRef<typeof MenubarPrimitive.Label>,
|
|
||||||
React.ComponentPropsWithoutRef<typeof MenubarPrimitive.Label> & {
|
|
||||||
inset?: boolean;
|
|
||||||
}
|
|
||||||
>(({ className, inset, ...props }, ref) => (
|
|
||||||
<MenubarPrimitive.Label
|
|
||||||
ref={ref}
|
|
||||||
className={cn("px-2 py-1.5 text-sm font-semibold", inset && "pl-8", className)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
));
|
|
||||||
MenubarLabel.displayName = MenubarPrimitive.Label.displayName;
|
|
||||||
|
|
||||||
const MenubarSeparator = React.forwardRef<
|
|
||||||
React.ElementRef<typeof MenubarPrimitive.Separator>,
|
|
||||||
React.ComponentPropsWithoutRef<typeof MenubarPrimitive.Separator>
|
|
||||||
>(({ className, ...props }, ref) => (
|
|
||||||
<MenubarPrimitive.Separator ref={ref} className={cn("-mx-1 my-1 h-px bg-muted", className)} {...props} />
|
|
||||||
));
|
|
||||||
MenubarSeparator.displayName = MenubarPrimitive.Separator.displayName;
|
|
||||||
|
|
||||||
const MenubarShortcut = ({ className, ...props }: React.HTMLAttributes<HTMLSpanElement>) => {
|
|
||||||
return <span className={cn("ml-auto text-xs tracking-widest text-muted-foreground", className)} {...props} />;
|
|
||||||
};
|
|
||||||
MenubarShortcut.displayname = "MenubarShortcut";
|
|
||||||
|
|
||||||
export {
|
|
||||||
Menubar,
|
|
||||||
MenubarMenu,
|
|
||||||
MenubarTrigger,
|
|
||||||
MenubarContent,
|
|
||||||
MenubarItem,
|
|
||||||
MenubarSeparator,
|
|
||||||
MenubarLabel,
|
|
||||||
MenubarCheckboxItem,
|
|
||||||
MenubarRadioGroup,
|
|
||||||
MenubarRadioItem,
|
|
||||||
MenubarPortal,
|
|
||||||
MenubarSubContent,
|
|
||||||
MenubarSubTrigger,
|
|
||||||
MenubarGroup,
|
|
||||||
MenubarSub,
|
|
||||||
MenubarShortcut,
|
|
||||||
};
|
|
||||||
@@ -1,120 +0,0 @@
|
|||||||
import * as React from "react";
|
|
||||||
import * as NavigationMenuPrimitive from "@radix-ui/react-navigation-menu";
|
|
||||||
import { cva } from "class-variance-authority";
|
|
||||||
import { ChevronDown } from "lucide-react";
|
|
||||||
|
|
||||||
import { cn } from "@/lib/utils";
|
|
||||||
|
|
||||||
const NavigationMenu = React.forwardRef<
|
|
||||||
React.ElementRef<typeof NavigationMenuPrimitive.Root>,
|
|
||||||
React.ComponentPropsWithoutRef<typeof NavigationMenuPrimitive.Root>
|
|
||||||
>(({ className, children, ...props }, ref) => (
|
|
||||||
<NavigationMenuPrimitive.Root
|
|
||||||
ref={ref}
|
|
||||||
className={cn("relative z-10 flex max-w-max flex-1 items-center justify-center", className)}
|
|
||||||
{...props}
|
|
||||||
>
|
|
||||||
{children}
|
|
||||||
<NavigationMenuViewport />
|
|
||||||
</NavigationMenuPrimitive.Root>
|
|
||||||
));
|
|
||||||
NavigationMenu.displayName = NavigationMenuPrimitive.Root.displayName;
|
|
||||||
|
|
||||||
const NavigationMenuList = React.forwardRef<
|
|
||||||
React.ElementRef<typeof NavigationMenuPrimitive.List>,
|
|
||||||
React.ComponentPropsWithoutRef<typeof NavigationMenuPrimitive.List>
|
|
||||||
>(({ className, ...props }, ref) => (
|
|
||||||
<NavigationMenuPrimitive.List
|
|
||||||
ref={ref}
|
|
||||||
className={cn("group flex flex-1 list-none items-center justify-center space-x-1", className)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
));
|
|
||||||
NavigationMenuList.displayName = NavigationMenuPrimitive.List.displayName;
|
|
||||||
|
|
||||||
const NavigationMenuItem = NavigationMenuPrimitive.Item;
|
|
||||||
|
|
||||||
const navigationMenuTriggerStyle = cva(
|
|
||||||
"group inline-flex h-10 w-max items-center justify-center rounded-md bg-background px-4 py-2 text-sm font-medium transition-colors hover:bg-accent hover:text-accent-foreground focus:bg-accent focus:text-accent-foreground focus:outline-none disabled:pointer-events-none disabled:opacity-50 data-[active]:bg-accent/50 data-[state=open]:bg-accent/50",
|
|
||||||
);
|
|
||||||
|
|
||||||
const NavigationMenuTrigger = React.forwardRef<
|
|
||||||
React.ElementRef<typeof NavigationMenuPrimitive.Trigger>,
|
|
||||||
React.ComponentPropsWithoutRef<typeof NavigationMenuPrimitive.Trigger>
|
|
||||||
>(({ className, children, ...props }, ref) => (
|
|
||||||
<NavigationMenuPrimitive.Trigger
|
|
||||||
ref={ref}
|
|
||||||
className={cn(navigationMenuTriggerStyle(), "group", className)}
|
|
||||||
{...props}
|
|
||||||
>
|
|
||||||
{children}{" "}
|
|
||||||
<ChevronDown
|
|
||||||
className="relative top-[1px] ml-1 h-3 w-3 transition duration-200 group-data-[state=open]:rotate-180"
|
|
||||||
aria-hidden="true"
|
|
||||||
/>
|
|
||||||
</NavigationMenuPrimitive.Trigger>
|
|
||||||
));
|
|
||||||
NavigationMenuTrigger.displayName = NavigationMenuPrimitive.Trigger.displayName;
|
|
||||||
|
|
||||||
const NavigationMenuContent = React.forwardRef<
|
|
||||||
React.ElementRef<typeof NavigationMenuPrimitive.Content>,
|
|
||||||
React.ComponentPropsWithoutRef<typeof NavigationMenuPrimitive.Content>
|
|
||||||
>(({ className, ...props }, ref) => (
|
|
||||||
<NavigationMenuPrimitive.Content
|
|
||||||
ref={ref}
|
|
||||||
className={cn(
|
|
||||||
"left-0 top-0 w-full data-[motion^=from-]:animate-in data-[motion^=to-]:animate-out data-[motion^=from-]:fade-in data-[motion^=to-]:fade-out data-[motion=from-end]:slide-in-from-right-52 data-[motion=from-start]:slide-in-from-left-52 data-[motion=to-end]:slide-out-to-right-52 data-[motion=to-start]:slide-out-to-left-52 md:absolute md:w-auto",
|
|
||||||
className,
|
|
||||||
)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
));
|
|
||||||
NavigationMenuContent.displayName = NavigationMenuPrimitive.Content.displayName;
|
|
||||||
|
|
||||||
const NavigationMenuLink = NavigationMenuPrimitive.Link;
|
|
||||||
|
|
||||||
const NavigationMenuViewport = React.forwardRef<
|
|
||||||
React.ElementRef<typeof NavigationMenuPrimitive.Viewport>,
|
|
||||||
React.ComponentPropsWithoutRef<typeof NavigationMenuPrimitive.Viewport>
|
|
||||||
>(({ className, ...props }, ref) => (
|
|
||||||
<div className={cn("absolute left-0 top-full flex justify-center")}>
|
|
||||||
<NavigationMenuPrimitive.Viewport
|
|
||||||
className={cn(
|
|
||||||
"origin-top-center relative mt-1.5 h-[var(--radix-navigation-menu-viewport-height)] w-full overflow-hidden rounded-md border bg-popover text-popover-foreground shadow-lg data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-90 md:w-[var(--radix-navigation-menu-viewport-width)]",
|
|
||||||
className,
|
|
||||||
)}
|
|
||||||
ref={ref}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
));
|
|
||||||
NavigationMenuViewport.displayName = NavigationMenuPrimitive.Viewport.displayName;
|
|
||||||
|
|
||||||
const NavigationMenuIndicator = React.forwardRef<
|
|
||||||
React.ElementRef<typeof NavigationMenuPrimitive.Indicator>,
|
|
||||||
React.ComponentPropsWithoutRef<typeof NavigationMenuPrimitive.Indicator>
|
|
||||||
>(({ className, ...props }, ref) => (
|
|
||||||
<NavigationMenuPrimitive.Indicator
|
|
||||||
ref={ref}
|
|
||||||
className={cn(
|
|
||||||
"top-full z-[1] flex h-1.5 items-end justify-center overflow-hidden data-[state=visible]:animate-in data-[state=hidden]:animate-out data-[state=hidden]:fade-out data-[state=visible]:fade-in",
|
|
||||||
className,
|
|
||||||
)}
|
|
||||||
{...props}
|
|
||||||
>
|
|
||||||
<div className="relative top-[60%] h-2 w-2 rotate-45 rounded-tl-sm bg-border shadow-md" />
|
|
||||||
</NavigationMenuPrimitive.Indicator>
|
|
||||||
));
|
|
||||||
NavigationMenuIndicator.displayName = NavigationMenuPrimitive.Indicator.displayName;
|
|
||||||
|
|
||||||
export {
|
|
||||||
navigationMenuTriggerStyle,
|
|
||||||
NavigationMenu,
|
|
||||||
NavigationMenuList,
|
|
||||||
NavigationMenuItem,
|
|
||||||
NavigationMenuContent,
|
|
||||||
NavigationMenuTrigger,
|
|
||||||
NavigationMenuLink,
|
|
||||||
NavigationMenuIndicator,
|
|
||||||
NavigationMenuViewport,
|
|
||||||
};
|
|
||||||
@@ -1,81 +0,0 @@
|
|||||||
import * as React from "react";
|
|
||||||
import { ChevronLeft, ChevronRight, MoreHorizontal } from "lucide-react";
|
|
||||||
|
|
||||||
import { cn } from "@/lib/utils";
|
|
||||||
import { ButtonProps, buttonVariants } from "@/components/ui/button";
|
|
||||||
|
|
||||||
const Pagination = ({ className, ...props }: React.ComponentProps<"nav">) => (
|
|
||||||
<nav
|
|
||||||
role="navigation"
|
|
||||||
aria-label="pagination"
|
|
||||||
className={cn("mx-auto flex w-full justify-center", className)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
Pagination.displayName = "Pagination";
|
|
||||||
|
|
||||||
const PaginationContent = React.forwardRef<HTMLUListElement, React.ComponentProps<"ul">>(
|
|
||||||
({ className, ...props }, ref) => (
|
|
||||||
<ul ref={ref} className={cn("flex flex-row items-center gap-1", className)} {...props} />
|
|
||||||
),
|
|
||||||
);
|
|
||||||
PaginationContent.displayName = "PaginationContent";
|
|
||||||
|
|
||||||
const PaginationItem = React.forwardRef<HTMLLIElement, React.ComponentProps<"li">>(({ className, ...props }, ref) => (
|
|
||||||
<li ref={ref} className={cn("", className)} {...props} />
|
|
||||||
));
|
|
||||||
PaginationItem.displayName = "PaginationItem";
|
|
||||||
|
|
||||||
type PaginationLinkProps = {
|
|
||||||
isActive?: boolean;
|
|
||||||
} & Pick<ButtonProps, "size"> &
|
|
||||||
React.ComponentProps<"a">;
|
|
||||||
|
|
||||||
const PaginationLink = ({ className, isActive, size = "icon", ...props }: PaginationLinkProps) => (
|
|
||||||
<a
|
|
||||||
aria-current={isActive ? "page" : undefined}
|
|
||||||
className={cn(
|
|
||||||
buttonVariants({
|
|
||||||
variant: isActive ? "outline" : "ghost",
|
|
||||||
size,
|
|
||||||
}),
|
|
||||||
className,
|
|
||||||
)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
PaginationLink.displayName = "PaginationLink";
|
|
||||||
|
|
||||||
const PaginationPrevious = ({ className, ...props }: React.ComponentProps<typeof PaginationLink>) => (
|
|
||||||
<PaginationLink aria-label="Go to previous page" size="default" className={cn("gap-1 pl-2.5", className)} {...props}>
|
|
||||||
<ChevronLeft className="h-4 w-4" />
|
|
||||||
<span>Previous</span>
|
|
||||||
</PaginationLink>
|
|
||||||
);
|
|
||||||
PaginationPrevious.displayName = "PaginationPrevious";
|
|
||||||
|
|
||||||
const PaginationNext = ({ className, ...props }: React.ComponentProps<typeof PaginationLink>) => (
|
|
||||||
<PaginationLink aria-label="Go to next page" size="default" className={cn("gap-1 pr-2.5", className)} {...props}>
|
|
||||||
<span>Next</span>
|
|
||||||
<ChevronRight className="h-4 w-4" />
|
|
||||||
</PaginationLink>
|
|
||||||
);
|
|
||||||
PaginationNext.displayName = "PaginationNext";
|
|
||||||
|
|
||||||
const PaginationEllipsis = ({ className, ...props }: React.ComponentProps<"span">) => (
|
|
||||||
<span aria-hidden className={cn("flex h-9 w-9 items-center justify-center", className)} {...props}>
|
|
||||||
<MoreHorizontal className="h-4 w-4" />
|
|
||||||
<span className="sr-only">More pages</span>
|
|
||||||
</span>
|
|
||||||
);
|
|
||||||
PaginationEllipsis.displayName = "PaginationEllipsis";
|
|
||||||
|
|
||||||
export {
|
|
||||||
Pagination,
|
|
||||||
PaginationContent,
|
|
||||||
PaginationEllipsis,
|
|
||||||
PaginationItem,
|
|
||||||
PaginationLink,
|
|
||||||
PaginationNext,
|
|
||||||
PaginationPrevious,
|
|
||||||
};
|
|
||||||
@@ -1,29 +0,0 @@
|
|||||||
import * as React from "react";
|
|
||||||
import * as PopoverPrimitive from "@radix-ui/react-popover";
|
|
||||||
|
|
||||||
import { cn } from "@/lib/utils";
|
|
||||||
|
|
||||||
const Popover = PopoverPrimitive.Root;
|
|
||||||
|
|
||||||
const PopoverTrigger = PopoverPrimitive.Trigger;
|
|
||||||
|
|
||||||
const PopoverContent = React.forwardRef<
|
|
||||||
React.ElementRef<typeof PopoverPrimitive.Content>,
|
|
||||||
React.ComponentPropsWithoutRef<typeof PopoverPrimitive.Content>
|
|
||||||
>(({ className, align = "center", sideOffset = 4, ...props }, ref) => (
|
|
||||||
<PopoverPrimitive.Portal>
|
|
||||||
<PopoverPrimitive.Content
|
|
||||||
ref={ref}
|
|
||||||
align={align}
|
|
||||||
sideOffset={sideOffset}
|
|
||||||
className={cn(
|
|
||||||
"z-50 w-72 rounded-md border bg-popover p-4 text-popover-foreground shadow-md outline-none data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
|
|
||||||
className,
|
|
||||||
)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
</PopoverPrimitive.Portal>
|
|
||||||
));
|
|
||||||
PopoverContent.displayName = PopoverPrimitive.Content.displayName;
|
|
||||||
|
|
||||||
export { Popover, PopoverTrigger, PopoverContent };
|
|
||||||
@@ -1,23 +0,0 @@
|
|||||||
import * as React from "react";
|
|
||||||
import * as ProgressPrimitive from "@radix-ui/react-progress";
|
|
||||||
|
|
||||||
import { cn } from "@/lib/utils";
|
|
||||||
|
|
||||||
const Progress = React.forwardRef<
|
|
||||||
React.ElementRef<typeof ProgressPrimitive.Root>,
|
|
||||||
React.ComponentPropsWithoutRef<typeof ProgressPrimitive.Root>
|
|
||||||
>(({ className, value, ...props }, ref) => (
|
|
||||||
<ProgressPrimitive.Root
|
|
||||||
ref={ref}
|
|
||||||
className={cn("relative h-4 w-full overflow-hidden rounded-full bg-secondary", className)}
|
|
||||||
{...props}
|
|
||||||
>
|
|
||||||
<ProgressPrimitive.Indicator
|
|
||||||
className="h-full w-full flex-1 bg-primary transition-all"
|
|
||||||
style={{ transform: `translateX(-${100 - (value || 0)}%)` }}
|
|
||||||
/>
|
|
||||||
</ProgressPrimitive.Root>
|
|
||||||
));
|
|
||||||
Progress.displayName = ProgressPrimitive.Root.displayName;
|
|
||||||
|
|
||||||
export { Progress };
|
|
||||||
@@ -1,36 +0,0 @@
|
|||||||
import * as React from "react";
|
|
||||||
import * as RadioGroupPrimitive from "@radix-ui/react-radio-group";
|
|
||||||
import { Circle } from "lucide-react";
|
|
||||||
|
|
||||||
import { cn } from "@/lib/utils";
|
|
||||||
|
|
||||||
const RadioGroup = React.forwardRef<
|
|
||||||
React.ElementRef<typeof RadioGroupPrimitive.Root>,
|
|
||||||
React.ComponentPropsWithoutRef<typeof RadioGroupPrimitive.Root>
|
|
||||||
>(({ className, ...props }, ref) => {
|
|
||||||
return <RadioGroupPrimitive.Root className={cn("grid gap-2", className)} {...props} ref={ref} />;
|
|
||||||
});
|
|
||||||
RadioGroup.displayName = RadioGroupPrimitive.Root.displayName;
|
|
||||||
|
|
||||||
const RadioGroupItem = React.forwardRef<
|
|
||||||
React.ElementRef<typeof RadioGroupPrimitive.Item>,
|
|
||||||
React.ComponentPropsWithoutRef<typeof RadioGroupPrimitive.Item>
|
|
||||||
>(({ className, ...props }, ref) => {
|
|
||||||
return (
|
|
||||||
<RadioGroupPrimitive.Item
|
|
||||||
ref={ref}
|
|
||||||
className={cn(
|
|
||||||
"aspect-square h-4 w-4 rounded-full border border-primary text-primary ring-offset-background focus:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50",
|
|
||||||
className,
|
|
||||||
)}
|
|
||||||
{...props}
|
|
||||||
>
|
|
||||||
<RadioGroupPrimitive.Indicator className="flex items-center justify-center">
|
|
||||||
<Circle className="h-2.5 w-2.5 fill-current text-current" />
|
|
||||||
</RadioGroupPrimitive.Indicator>
|
|
||||||
</RadioGroupPrimitive.Item>
|
|
||||||
);
|
|
||||||
});
|
|
||||||
RadioGroupItem.displayName = RadioGroupPrimitive.Item.displayName;
|
|
||||||
|
|
||||||
export { RadioGroup, RadioGroupItem };
|
|
||||||
@@ -1,37 +0,0 @@
|
|||||||
import { GripVertical } from "lucide-react";
|
|
||||||
import * as ResizablePrimitive from "react-resizable-panels";
|
|
||||||
|
|
||||||
import { cn } from "@/lib/utils";
|
|
||||||
|
|
||||||
const ResizablePanelGroup = ({ className, ...props }: React.ComponentProps<typeof ResizablePrimitive.PanelGroup>) => (
|
|
||||||
<ResizablePrimitive.PanelGroup
|
|
||||||
className={cn("flex h-full w-full data-[panel-group-direction=vertical]:flex-col", className)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
|
|
||||||
const ResizablePanel = ResizablePrimitive.Panel;
|
|
||||||
|
|
||||||
const ResizableHandle = ({
|
|
||||||
withHandle,
|
|
||||||
className,
|
|
||||||
...props
|
|
||||||
}: React.ComponentProps<typeof ResizablePrimitive.PanelResizeHandle> & {
|
|
||||||
withHandle?: boolean;
|
|
||||||
}) => (
|
|
||||||
<ResizablePrimitive.PanelResizeHandle
|
|
||||||
className={cn(
|
|
||||||
"relative flex w-px items-center justify-center bg-border after:absolute after:inset-y-0 after:left-1/2 after:w-1 after:-translate-x-1/2 data-[panel-group-direction=vertical]:h-px data-[panel-group-direction=vertical]:w-full data-[panel-group-direction=vertical]:after:left-0 data-[panel-group-direction=vertical]:after:h-1 data-[panel-group-direction=vertical]:after:w-full data-[panel-group-direction=vertical]:after:-translate-y-1/2 data-[panel-group-direction=vertical]:after:translate-x-0 focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring focus-visible:ring-offset-1 [&[data-panel-group-direction=vertical]>div]:rotate-90",
|
|
||||||
className,
|
|
||||||
)}
|
|
||||||
{...props}
|
|
||||||
>
|
|
||||||
{withHandle && (
|
|
||||||
<div className="z-10 flex h-4 w-3 items-center justify-center rounded-sm border bg-border">
|
|
||||||
<GripVertical className="h-2.5 w-2.5" />
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</ResizablePrimitive.PanelResizeHandle>
|
|
||||||
);
|
|
||||||
|
|
||||||
export { ResizablePanelGroup, ResizablePanel, ResizableHandle };
|
|
||||||
@@ -1,38 +0,0 @@
|
|||||||
import * as React from "react";
|
|
||||||
import * as ScrollAreaPrimitive from "@radix-ui/react-scroll-area";
|
|
||||||
|
|
||||||
import { cn } from "@/lib/utils";
|
|
||||||
|
|
||||||
const ScrollArea = React.forwardRef<
|
|
||||||
React.ElementRef<typeof ScrollAreaPrimitive.Root>,
|
|
||||||
React.ComponentPropsWithoutRef<typeof ScrollAreaPrimitive.Root>
|
|
||||||
>(({ className, children, ...props }, ref) => (
|
|
||||||
<ScrollAreaPrimitive.Root ref={ref} className={cn("relative overflow-hidden", className)} {...props}>
|
|
||||||
<ScrollAreaPrimitive.Viewport className="h-full w-full rounded-[inherit]">{children}</ScrollAreaPrimitive.Viewport>
|
|
||||||
<ScrollBar />
|
|
||||||
<ScrollAreaPrimitive.Corner />
|
|
||||||
</ScrollAreaPrimitive.Root>
|
|
||||||
));
|
|
||||||
ScrollArea.displayName = ScrollAreaPrimitive.Root.displayName;
|
|
||||||
|
|
||||||
const ScrollBar = React.forwardRef<
|
|
||||||
React.ElementRef<typeof ScrollAreaPrimitive.ScrollAreaScrollbar>,
|
|
||||||
React.ComponentPropsWithoutRef<typeof ScrollAreaPrimitive.ScrollAreaScrollbar>
|
|
||||||
>(({ className, orientation = "vertical", ...props }, ref) => (
|
|
||||||
<ScrollAreaPrimitive.ScrollAreaScrollbar
|
|
||||||
ref={ref}
|
|
||||||
orientation={orientation}
|
|
||||||
className={cn(
|
|
||||||
"flex touch-none select-none transition-colors",
|
|
||||||
orientation === "vertical" && "h-full w-2.5 border-l border-l-transparent p-[1px]",
|
|
||||||
orientation === "horizontal" && "h-2.5 flex-col border-t border-t-transparent p-[1px]",
|
|
||||||
className,
|
|
||||||
)}
|
|
||||||
{...props}
|
|
||||||
>
|
|
||||||
<ScrollAreaPrimitive.ScrollAreaThumb className="relative flex-1 rounded-full bg-border" />
|
|
||||||
</ScrollAreaPrimitive.ScrollAreaScrollbar>
|
|
||||||
));
|
|
||||||
ScrollBar.displayName = ScrollAreaPrimitive.ScrollAreaScrollbar.displayName;
|
|
||||||
|
|
||||||
export { ScrollArea, ScrollBar };
|
|
||||||
@@ -1,143 +0,0 @@
|
|||||||
import * as React from "react";
|
|
||||||
import * as SelectPrimitive from "@radix-ui/react-select";
|
|
||||||
import { Check, ChevronDown, ChevronUp } from "lucide-react";
|
|
||||||
|
|
||||||
import { cn } from "@/lib/utils";
|
|
||||||
|
|
||||||
const Select = SelectPrimitive.Root;
|
|
||||||
|
|
||||||
const SelectGroup = SelectPrimitive.Group;
|
|
||||||
|
|
||||||
const SelectValue = SelectPrimitive.Value;
|
|
||||||
|
|
||||||
const SelectTrigger = React.forwardRef<
|
|
||||||
React.ElementRef<typeof SelectPrimitive.Trigger>,
|
|
||||||
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Trigger>
|
|
||||||
>(({ className, children, ...props }, ref) => (
|
|
||||||
<SelectPrimitive.Trigger
|
|
||||||
ref={ref}
|
|
||||||
className={cn(
|
|
||||||
"flex h-10 w-full items-center justify-between rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background placeholder:text-muted-foreground focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 [&>span]:line-clamp-1",
|
|
||||||
className,
|
|
||||||
)}
|
|
||||||
{...props}
|
|
||||||
>
|
|
||||||
{children}
|
|
||||||
<SelectPrimitive.Icon asChild>
|
|
||||||
<ChevronDown className="h-4 w-4 opacity-50" />
|
|
||||||
</SelectPrimitive.Icon>
|
|
||||||
</SelectPrimitive.Trigger>
|
|
||||||
));
|
|
||||||
SelectTrigger.displayName = SelectPrimitive.Trigger.displayName;
|
|
||||||
|
|
||||||
const SelectScrollUpButton = React.forwardRef<
|
|
||||||
React.ElementRef<typeof SelectPrimitive.ScrollUpButton>,
|
|
||||||
React.ComponentPropsWithoutRef<typeof SelectPrimitive.ScrollUpButton>
|
|
||||||
>(({ className, ...props }, ref) => (
|
|
||||||
<SelectPrimitive.ScrollUpButton
|
|
||||||
ref={ref}
|
|
||||||
className={cn("flex cursor-default items-center justify-center py-1", className)}
|
|
||||||
{...props}
|
|
||||||
>
|
|
||||||
<ChevronUp className="h-4 w-4" />
|
|
||||||
</SelectPrimitive.ScrollUpButton>
|
|
||||||
));
|
|
||||||
SelectScrollUpButton.displayName = SelectPrimitive.ScrollUpButton.displayName;
|
|
||||||
|
|
||||||
const SelectScrollDownButton = React.forwardRef<
|
|
||||||
React.ElementRef<typeof SelectPrimitive.ScrollDownButton>,
|
|
||||||
React.ComponentPropsWithoutRef<typeof SelectPrimitive.ScrollDownButton>
|
|
||||||
>(({ className, ...props }, ref) => (
|
|
||||||
<SelectPrimitive.ScrollDownButton
|
|
||||||
ref={ref}
|
|
||||||
className={cn("flex cursor-default items-center justify-center py-1", className)}
|
|
||||||
{...props}
|
|
||||||
>
|
|
||||||
<ChevronDown className="h-4 w-4" />
|
|
||||||
</SelectPrimitive.ScrollDownButton>
|
|
||||||
));
|
|
||||||
SelectScrollDownButton.displayName = SelectPrimitive.ScrollDownButton.displayName;
|
|
||||||
|
|
||||||
const SelectContent = React.forwardRef<
|
|
||||||
React.ElementRef<typeof SelectPrimitive.Content>,
|
|
||||||
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Content>
|
|
||||||
>(({ className, children, position = "popper", ...props }, ref) => (
|
|
||||||
<SelectPrimitive.Portal>
|
|
||||||
<SelectPrimitive.Content
|
|
||||||
ref={ref}
|
|
||||||
className={cn(
|
|
||||||
"relative z-50 max-h-96 min-w-[8rem] overflow-hidden rounded-md border bg-popover text-popover-foreground shadow-md data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
|
|
||||||
position === "popper" &&
|
|
||||||
"data-[side=bottom]:translate-y-1 data-[side=left]:-translate-x-1 data-[side=right]:translate-x-1 data-[side=top]:-translate-y-1",
|
|
||||||
className,
|
|
||||||
)}
|
|
||||||
position={position}
|
|
||||||
{...props}
|
|
||||||
>
|
|
||||||
<SelectScrollUpButton />
|
|
||||||
<SelectPrimitive.Viewport
|
|
||||||
className={cn(
|
|
||||||
"p-1",
|
|
||||||
position === "popper" &&
|
|
||||||
"h-[var(--radix-select-trigger-height)] w-full min-w-[var(--radix-select-trigger-width)]",
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
{children}
|
|
||||||
</SelectPrimitive.Viewport>
|
|
||||||
<SelectScrollDownButton />
|
|
||||||
</SelectPrimitive.Content>
|
|
||||||
</SelectPrimitive.Portal>
|
|
||||||
));
|
|
||||||
SelectContent.displayName = SelectPrimitive.Content.displayName;
|
|
||||||
|
|
||||||
const SelectLabel = React.forwardRef<
|
|
||||||
React.ElementRef<typeof SelectPrimitive.Label>,
|
|
||||||
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Label>
|
|
||||||
>(({ className, ...props }, ref) => (
|
|
||||||
<SelectPrimitive.Label ref={ref} className={cn("py-1.5 pl-8 pr-2 text-sm font-semibold", className)} {...props} />
|
|
||||||
));
|
|
||||||
SelectLabel.displayName = SelectPrimitive.Label.displayName;
|
|
||||||
|
|
||||||
const SelectItem = React.forwardRef<
|
|
||||||
React.ElementRef<typeof SelectPrimitive.Item>,
|
|
||||||
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Item>
|
|
||||||
>(({ className, children, ...props }, ref) => (
|
|
||||||
<SelectPrimitive.Item
|
|
||||||
ref={ref}
|
|
||||||
className={cn(
|
|
||||||
"relative flex w-full cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 focus:bg-accent focus:text-accent-foreground",
|
|
||||||
className,
|
|
||||||
)}
|
|
||||||
{...props}
|
|
||||||
>
|
|
||||||
<span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
|
|
||||||
<SelectPrimitive.ItemIndicator>
|
|
||||||
<Check className="h-4 w-4" />
|
|
||||||
</SelectPrimitive.ItemIndicator>
|
|
||||||
</span>
|
|
||||||
|
|
||||||
<SelectPrimitive.ItemText>{children}</SelectPrimitive.ItemText>
|
|
||||||
</SelectPrimitive.Item>
|
|
||||||
));
|
|
||||||
SelectItem.displayName = SelectPrimitive.Item.displayName;
|
|
||||||
|
|
||||||
const SelectSeparator = React.forwardRef<
|
|
||||||
React.ElementRef<typeof SelectPrimitive.Separator>,
|
|
||||||
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Separator>
|
|
||||||
>(({ className, ...props }, ref) => (
|
|
||||||
<SelectPrimitive.Separator ref={ref} className={cn("-mx-1 my-1 h-px bg-muted", className)} {...props} />
|
|
||||||
));
|
|
||||||
SelectSeparator.displayName = SelectPrimitive.Separator.displayName;
|
|
||||||
|
|
||||||
export {
|
|
||||||
Select,
|
|
||||||
SelectGroup,
|
|
||||||
SelectValue,
|
|
||||||
SelectTrigger,
|
|
||||||
SelectContent,
|
|
||||||
SelectLabel,
|
|
||||||
SelectItem,
|
|
||||||
SelectSeparator,
|
|
||||||
SelectScrollUpButton,
|
|
||||||
SelectScrollDownButton,
|
|
||||||
};
|
|
||||||
@@ -1,20 +1,15 @@
|
|||||||
import * as React from "react";
|
import * as React from "react";
|
||||||
import * as SeparatorPrimitive from "@radix-ui/react-separator";
|
|
||||||
|
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
|
|
||||||
const Separator = React.forwardRef<
|
export const Separator = ({ className, orientation = "horizontal", ...props }: React.HTMLAttributes<HTMLDivElement> & { orientation?: "horizontal" | "vertical" }) => (
|
||||||
React.ElementRef<typeof SeparatorPrimitive.Root>,
|
<div
|
||||||
React.ComponentPropsWithoutRef<typeof SeparatorPrimitive.Root>
|
className={cn(
|
||||||
>(({ className, orientation = "horizontal", decorative = true, ...props }, ref) => (
|
"shrink-0 bg-slate-200",
|
||||||
<SeparatorPrimitive.Root
|
orientation === "horizontal" ? "h-px w-full" : "h-full w-px",
|
||||||
ref={ref}
|
className
|
||||||
decorative={decorative}
|
)}
|
||||||
orientation={orientation}
|
|
||||||
className={cn("shrink-0 bg-border", orientation === "horizontal" ? "h-[1px] w-full" : "h-full w-[1px]", className)}
|
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
));
|
);
|
||||||
Separator.displayName = SeparatorPrimitive.Root.displayName;
|
|
||||||
|
|
||||||
export { Separator };
|
export default Separator;
|
||||||
|
|||||||
+41
-98
@@ -1,107 +1,50 @@
|
|||||||
import * as SheetPrimitive from "@radix-ui/react-dialog";
|
|
||||||
import { cva, type VariantProps } from "class-variance-authority";
|
|
||||||
import { X } from "lucide-react";
|
|
||||||
import * as React from "react";
|
import * as React from "react";
|
||||||
|
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
|
|
||||||
const Sheet = SheetPrimitive.Root;
|
const SheetContext = React.createContext<{ open: boolean; setOpen: (o: boolean) => void }>({ open: false, setOpen: () => {} });
|
||||||
|
|
||||||
const SheetTrigger = SheetPrimitive.Trigger;
|
export function Sheet({ open: controlled, onOpenChange, children }: { open?: boolean; onOpenChange?: (o: boolean) => void; children?: React.ReactNode }) {
|
||||||
|
const [internal, setInternal] = React.useState(false);
|
||||||
|
const open = controlled ?? internal;
|
||||||
|
const setOpen = (o: boolean) => { setInternal(o); onOpenChange?.(o); };
|
||||||
|
return <SheetContext.Provider value={{ open, setOpen }}>{children}</SheetContext.Provider>;
|
||||||
|
}
|
||||||
|
|
||||||
const SheetClose = SheetPrimitive.Close;
|
export function SheetTrigger({ asChild, children, ...props }: React.ButtonHTMLAttributes<HTMLButtonElement> & { asChild?: boolean }) {
|
||||||
|
const ctx = React.useContext(SheetContext);
|
||||||
|
if (asChild && React.isValidElement(children)) {
|
||||||
|
return React.cloneElement(children as React.ReactElement, { onClick: () => ctx.setOpen(true) });
|
||||||
|
}
|
||||||
|
return <button type="button" onClick={() => ctx.setOpen(true)} {...props}>{children}</button>;
|
||||||
|
}
|
||||||
|
|
||||||
const SheetPortal = SheetPrimitive.Portal;
|
export function SheetContent({ side = "right", className, children, ...props }: React.HTMLAttributes<HTMLDivElement> & { side?: "left" | "right" | "top" | "bottom" }) {
|
||||||
|
const ctx = React.useContext(SheetContext);
|
||||||
const SheetOverlay = React.forwardRef<
|
if (!ctx.open) return null;
|
||||||
React.ElementRef<typeof SheetPrimitive.Overlay>,
|
const pos = side === "left" ? "left-0 top-0 h-full w-72" : side === "right" ? "right-0 top-0 h-full w-72" : side === "top" ? "top-0 left-0 w-full" : "bottom-0 left-0 w-full";
|
||||||
React.ComponentPropsWithoutRef<typeof SheetPrimitive.Overlay>
|
return (
|
||||||
>(({ className, ...props }, ref) => (
|
<div className="fixed inset-0 z-50">
|
||||||
<SheetPrimitive.Overlay
|
<div className="absolute inset-0 bg-black/40" onClick={() => ctx.setOpen(false)} />
|
||||||
className={cn(
|
<div className={cn("absolute bg-white p-6 shadow-xl", pos, className)} {...props}>
|
||||||
"fixed inset-0 z-50 bg-black/80 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0",
|
<button type="button" className="absolute right-4 top-4 text-slate-500" onClick={() => ctx.setOpen(false)}>✕</button>
|
||||||
className,
|
|
||||||
)}
|
|
||||||
{...props}
|
|
||||||
ref={ref}
|
|
||||||
/>
|
|
||||||
));
|
|
||||||
SheetOverlay.displayName = SheetPrimitive.Overlay.displayName;
|
|
||||||
|
|
||||||
const sheetVariants = cva(
|
|
||||||
"fixed z-50 gap-4 bg-background p-6 shadow-lg transition ease-in-out data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:duration-300 data-[state=open]:duration-500",
|
|
||||||
{
|
|
||||||
variants: {
|
|
||||||
side: {
|
|
||||||
top: "inset-x-0 top-0 border-b data-[state=closed]:slide-out-to-top data-[state=open]:slide-in-from-top",
|
|
||||||
bottom:
|
|
||||||
"inset-x-0 bottom-0 border-t data-[state=closed]:slide-out-to-bottom data-[state=open]:slide-in-from-bottom",
|
|
||||||
left: "inset-y-0 left-0 h-full w-3/4 border-r data-[state=closed]:slide-out-to-left data-[state=open]:slide-in-from-left sm:max-w-sm",
|
|
||||||
right:
|
|
||||||
"inset-y-0 right-0 h-full w-3/4 border-l data-[state=closed]:slide-out-to-right data-[state=open]:slide-in-from-right sm:max-w-sm",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
defaultVariants: {
|
|
||||||
side: "right",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
interface SheetContentProps
|
|
||||||
extends React.ComponentPropsWithoutRef<typeof SheetPrimitive.Content>,
|
|
||||||
VariantProps<typeof sheetVariants> {}
|
|
||||||
|
|
||||||
const SheetContent = React.forwardRef<React.ElementRef<typeof SheetPrimitive.Content>, SheetContentProps>(
|
|
||||||
({ side = "right", className, children, ...props }, ref) => (
|
|
||||||
<SheetPortal>
|
|
||||||
<SheetOverlay />
|
|
||||||
<SheetPrimitive.Content ref={ref} className={cn(sheetVariants({ side }), className)} {...props}>
|
|
||||||
{children}
|
{children}
|
||||||
<SheetPrimitive.Close className="absolute right-4 top-4 rounded-sm opacity-70 ring-offset-background transition-opacity data-[state=open]:bg-secondary hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:pointer-events-none">
|
</div>
|
||||||
<X className="h-4 w-4" />
|
</div>
|
||||||
<span className="sr-only">Close</span>
|
);
|
||||||
</SheetPrimitive.Close>
|
}
|
||||||
</SheetPrimitive.Content>
|
|
||||||
</SheetPortal>
|
|
||||||
),
|
|
||||||
);
|
|
||||||
SheetContent.displayName = SheetPrimitive.Content.displayName;
|
|
||||||
|
|
||||||
const SheetHeader = ({ className, ...props }: React.HTMLAttributes<HTMLDivElement>) => (
|
export function SheetHeader({ className, ...props }: React.HTMLAttributes<HTMLDivElement>) {
|
||||||
<div className={cn("flex flex-col space-y-2 text-center sm:text-left", className)} {...props} />
|
return <div className={cn("flex flex-col space-y-2", className)} {...props} />;
|
||||||
);
|
}
|
||||||
SheetHeader.displayName = "SheetHeader";
|
export function SheetTitle({ className, ...props }: React.HTMLAttributes<HTMLHeadingElement>) {
|
||||||
|
return <h2 className={cn("text-lg font-semibold", className)} {...props} />;
|
||||||
|
}
|
||||||
|
export function SheetDescription({ className, ...props }: React.HTMLAttributes<HTMLParagraphElement>) {
|
||||||
|
return <p className={cn("text-sm text-slate-500", className)} {...props} />;
|
||||||
|
}
|
||||||
|
export function SheetClose({ children, ...props }: React.ButtonHTMLAttributes<HTMLButtonElement>) {
|
||||||
|
const ctx = React.useContext(SheetContext);
|
||||||
|
return <button type="button" onClick={() => ctx.setOpen(false)} {...props}>{children}</button>;
|
||||||
|
}
|
||||||
|
|
||||||
const SheetFooter = ({ className, ...props }: React.HTMLAttributes<HTMLDivElement>) => (
|
export default Sheet;
|
||||||
<div className={cn("flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2", className)} {...props} />
|
|
||||||
);
|
|
||||||
SheetFooter.displayName = "SheetFooter";
|
|
||||||
|
|
||||||
const SheetTitle = React.forwardRef<
|
|
||||||
React.ElementRef<typeof SheetPrimitive.Title>,
|
|
||||||
React.ComponentPropsWithoutRef<typeof SheetPrimitive.Title>
|
|
||||||
>(({ className, ...props }, ref) => (
|
|
||||||
<SheetPrimitive.Title ref={ref} className={cn("text-lg font-semibold text-foreground", className)} {...props} />
|
|
||||||
));
|
|
||||||
SheetTitle.displayName = SheetPrimitive.Title.displayName;
|
|
||||||
|
|
||||||
const SheetDescription = React.forwardRef<
|
|
||||||
React.ElementRef<typeof SheetPrimitive.Description>,
|
|
||||||
React.ComponentPropsWithoutRef<typeof SheetPrimitive.Description>
|
|
||||||
>(({ className, ...props }, ref) => (
|
|
||||||
<SheetPrimitive.Description ref={ref} className={cn("text-sm text-muted-foreground", className)} {...props} />
|
|
||||||
));
|
|
||||||
SheetDescription.displayName = SheetPrimitive.Description.displayName;
|
|
||||||
|
|
||||||
export {
|
|
||||||
Sheet,
|
|
||||||
SheetClose,
|
|
||||||
SheetContent,
|
|
||||||
SheetDescription,
|
|
||||||
SheetFooter,
|
|
||||||
SheetHeader,
|
|
||||||
SheetOverlay,
|
|
||||||
SheetPortal,
|
|
||||||
SheetTitle,
|
|
||||||
SheetTrigger,
|
|
||||||
};
|
|
||||||
|
|||||||
@@ -1,637 +0,0 @@
|
|||||||
import * as React from "react";
|
|
||||||
import { Slot } from "@radix-ui/react-slot";
|
|
||||||
import { VariantProps, cva } from "class-variance-authority";
|
|
||||||
import { PanelLeft } from "lucide-react";
|
|
||||||
|
|
||||||
import { useIsMobile } from "@/hooks/use-mobile";
|
|
||||||
import { cn } from "@/lib/utils";
|
|
||||||
import { Button } from "@/components/ui/button";
|
|
||||||
import { Input } from "@/components/ui/input";
|
|
||||||
import { Separator } from "@/components/ui/separator";
|
|
||||||
import { Sheet, SheetContent } from "@/components/ui/sheet";
|
|
||||||
import { Skeleton } from "@/components/ui/skeleton";
|
|
||||||
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/components/ui/tooltip";
|
|
||||||
|
|
||||||
const SIDEBAR_COOKIE_NAME = "sidebar:state";
|
|
||||||
const SIDEBAR_COOKIE_MAX_AGE = 60 * 60 * 24 * 7;
|
|
||||||
const SIDEBAR_WIDTH = "16rem";
|
|
||||||
const SIDEBAR_WIDTH_MOBILE = "18rem";
|
|
||||||
const SIDEBAR_WIDTH_ICON = "3rem";
|
|
||||||
const SIDEBAR_KEYBOARD_SHORTCUT = "b";
|
|
||||||
|
|
||||||
type SidebarContext = {
|
|
||||||
state: "expanded" | "collapsed";
|
|
||||||
open: boolean;
|
|
||||||
setOpen: (open: boolean) => void;
|
|
||||||
openMobile: boolean;
|
|
||||||
setOpenMobile: (open: boolean) => void;
|
|
||||||
isMobile: boolean;
|
|
||||||
toggleSidebar: () => void;
|
|
||||||
};
|
|
||||||
|
|
||||||
const SidebarContext = React.createContext<SidebarContext | null>(null);
|
|
||||||
|
|
||||||
function useSidebar() {
|
|
||||||
const context = React.useContext(SidebarContext);
|
|
||||||
if (!context) {
|
|
||||||
throw new Error("useSidebar must be used within a SidebarProvider.");
|
|
||||||
}
|
|
||||||
|
|
||||||
return context;
|
|
||||||
}
|
|
||||||
|
|
||||||
const SidebarProvider = React.forwardRef<
|
|
||||||
HTMLDivElement,
|
|
||||||
React.ComponentProps<"div"> & {
|
|
||||||
defaultOpen?: boolean;
|
|
||||||
open?: boolean;
|
|
||||||
onOpenChange?: (open: boolean) => void;
|
|
||||||
}
|
|
||||||
>(({ defaultOpen = true, open: openProp, onOpenChange: setOpenProp, className, style, children, ...props }, ref) => {
|
|
||||||
const isMobile = useIsMobile();
|
|
||||||
const [openMobile, setOpenMobile] = React.useState(false);
|
|
||||||
|
|
||||||
// This is the internal state of the sidebar.
|
|
||||||
// We use openProp and setOpenProp for control from outside the component.
|
|
||||||
const [_open, _setOpen] = React.useState(defaultOpen);
|
|
||||||
const open = openProp ?? _open;
|
|
||||||
const setOpen = React.useCallback(
|
|
||||||
(value: boolean | ((value: boolean) => boolean)) => {
|
|
||||||
const openState = typeof value === "function" ? value(open) : value;
|
|
||||||
if (setOpenProp) {
|
|
||||||
setOpenProp(openState);
|
|
||||||
} else {
|
|
||||||
_setOpen(openState);
|
|
||||||
}
|
|
||||||
|
|
||||||
// This sets the cookie to keep the sidebar state.
|
|
||||||
document.cookie = `${SIDEBAR_COOKIE_NAME}=${openState}; path=/; max-age=${SIDEBAR_COOKIE_MAX_AGE}`;
|
|
||||||
},
|
|
||||||
[setOpenProp, open],
|
|
||||||
);
|
|
||||||
|
|
||||||
// Helper to toggle the sidebar.
|
|
||||||
const toggleSidebar = React.useCallback(() => {
|
|
||||||
return isMobile ? setOpenMobile((open) => !open) : setOpen((open) => !open);
|
|
||||||
}, [isMobile, setOpen, setOpenMobile]);
|
|
||||||
|
|
||||||
// Adds a keyboard shortcut to toggle the sidebar.
|
|
||||||
React.useEffect(() => {
|
|
||||||
const handleKeyDown = (event: KeyboardEvent) => {
|
|
||||||
if (event.key === SIDEBAR_KEYBOARD_SHORTCUT && (event.metaKey || event.ctrlKey)) {
|
|
||||||
event.preventDefault();
|
|
||||||
toggleSidebar();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
window.addEventListener("keydown", handleKeyDown);
|
|
||||||
return () => window.removeEventListener("keydown", handleKeyDown);
|
|
||||||
}, [toggleSidebar]);
|
|
||||||
|
|
||||||
// We add a state so that we can do data-state="expanded" or "collapsed".
|
|
||||||
// This makes it easier to style the sidebar with Tailwind classes.
|
|
||||||
const state = open ? "expanded" : "collapsed";
|
|
||||||
|
|
||||||
const contextValue = React.useMemo<SidebarContext>(
|
|
||||||
() => ({
|
|
||||||
state,
|
|
||||||
open,
|
|
||||||
setOpen,
|
|
||||||
isMobile,
|
|
||||||
openMobile,
|
|
||||||
setOpenMobile,
|
|
||||||
toggleSidebar,
|
|
||||||
}),
|
|
||||||
[state, open, setOpen, isMobile, openMobile, setOpenMobile, toggleSidebar],
|
|
||||||
);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<SidebarContext.Provider value={contextValue}>
|
|
||||||
<TooltipProvider delayDuration={0}>
|
|
||||||
<div
|
|
||||||
style={
|
|
||||||
{
|
|
||||||
"--sidebar-width": SIDEBAR_WIDTH,
|
|
||||||
"--sidebar-width-icon": SIDEBAR_WIDTH_ICON,
|
|
||||||
...style,
|
|
||||||
} as React.CSSProperties
|
|
||||||
}
|
|
||||||
className={cn("group/sidebar-wrapper flex min-h-svh w-full has-[[data-variant=inset]]:bg-sidebar", className)}
|
|
||||||
ref={ref}
|
|
||||||
{...props}
|
|
||||||
>
|
|
||||||
{children}
|
|
||||||
</div>
|
|
||||||
</TooltipProvider>
|
|
||||||
</SidebarContext.Provider>
|
|
||||||
);
|
|
||||||
});
|
|
||||||
SidebarProvider.displayName = "SidebarProvider";
|
|
||||||
|
|
||||||
const Sidebar = React.forwardRef<
|
|
||||||
HTMLDivElement,
|
|
||||||
React.ComponentProps<"div"> & {
|
|
||||||
side?: "left" | "right";
|
|
||||||
variant?: "sidebar" | "floating" | "inset";
|
|
||||||
collapsible?: "offcanvas" | "icon" | "none";
|
|
||||||
}
|
|
||||||
>(({ side = "left", variant = "sidebar", collapsible = "offcanvas", className, children, ...props }, ref) => {
|
|
||||||
const { isMobile, state, openMobile, setOpenMobile } = useSidebar();
|
|
||||||
|
|
||||||
if (collapsible === "none") {
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
className={cn("flex h-full w-[--sidebar-width] flex-col bg-sidebar text-sidebar-foreground", className)}
|
|
||||||
ref={ref}
|
|
||||||
{...props}
|
|
||||||
>
|
|
||||||
{children}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (isMobile) {
|
|
||||||
return (
|
|
||||||
<Sheet open={openMobile} onOpenChange={setOpenMobile} {...props}>
|
|
||||||
<SheetContent
|
|
||||||
data-sidebar="sidebar"
|
|
||||||
data-mobile="true"
|
|
||||||
className="w-[--sidebar-width] bg-sidebar p-0 text-sidebar-foreground [&>button]:hidden"
|
|
||||||
style={
|
|
||||||
{
|
|
||||||
"--sidebar-width": SIDEBAR_WIDTH_MOBILE,
|
|
||||||
} as React.CSSProperties
|
|
||||||
}
|
|
||||||
side={side}
|
|
||||||
>
|
|
||||||
<div className="flex h-full w-full flex-col">{children}</div>
|
|
||||||
</SheetContent>
|
|
||||||
</Sheet>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
ref={ref}
|
|
||||||
className="group peer hidden text-sidebar-foreground md:block"
|
|
||||||
data-state={state}
|
|
||||||
data-collapsible={state === "collapsed" ? collapsible : ""}
|
|
||||||
data-variant={variant}
|
|
||||||
data-side={side}
|
|
||||||
>
|
|
||||||
{/* This is what handles the sidebar gap on desktop */}
|
|
||||||
<div
|
|
||||||
className={cn(
|
|
||||||
"relative h-svh w-[--sidebar-width] bg-transparent transition-[width] duration-200 ease-linear",
|
|
||||||
"group-data-[collapsible=offcanvas]:w-0",
|
|
||||||
"group-data-[side=right]:rotate-180",
|
|
||||||
variant === "floating" || variant === "inset"
|
|
||||||
? "group-data-[collapsible=icon]:w-[calc(var(--sidebar-width-icon)_+_theme(spacing.4))]"
|
|
||||||
: "group-data-[collapsible=icon]:w-[--sidebar-width-icon]",
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
<div
|
|
||||||
className={cn(
|
|
||||||
"fixed inset-y-0 z-10 hidden h-svh w-[--sidebar-width] transition-[left,right,width] duration-200 ease-linear md:flex",
|
|
||||||
side === "left"
|
|
||||||
? "left-0 group-data-[collapsible=offcanvas]:left-[calc(var(--sidebar-width)*-1)]"
|
|
||||||
: "right-0 group-data-[collapsible=offcanvas]:right-[calc(var(--sidebar-width)*-1)]",
|
|
||||||
// Adjust the padding for floating and inset variants.
|
|
||||||
variant === "floating" || variant === "inset"
|
|
||||||
? "p-2 group-data-[collapsible=icon]:w-[calc(var(--sidebar-width-icon)_+_theme(spacing.4)_+2px)]"
|
|
||||||
: "group-data-[collapsible=icon]:w-[--sidebar-width-icon] group-data-[side=left]:border-r group-data-[side=right]:border-l",
|
|
||||||
className,
|
|
||||||
)}
|
|
||||||
{...props}
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
data-sidebar="sidebar"
|
|
||||||
className="flex h-full w-full flex-col bg-sidebar group-data-[variant=floating]:rounded-lg group-data-[variant=floating]:border group-data-[variant=floating]:border-sidebar-border group-data-[variant=floating]:shadow"
|
|
||||||
>
|
|
||||||
{children}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
});
|
|
||||||
Sidebar.displayName = "Sidebar";
|
|
||||||
|
|
||||||
const SidebarTrigger = React.forwardRef<React.ElementRef<typeof Button>, React.ComponentProps<typeof Button>>(
|
|
||||||
({ className, onClick, ...props }, ref) => {
|
|
||||||
const { toggleSidebar } = useSidebar();
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Button
|
|
||||||
ref={ref}
|
|
||||||
data-sidebar="trigger"
|
|
||||||
variant="ghost"
|
|
||||||
size="icon"
|
|
||||||
className={cn("h-7 w-7", className)}
|
|
||||||
onClick={(event) => {
|
|
||||||
onClick?.(event);
|
|
||||||
toggleSidebar();
|
|
||||||
}}
|
|
||||||
{...props}
|
|
||||||
>
|
|
||||||
<PanelLeft />
|
|
||||||
<span className="sr-only">Toggle Sidebar</span>
|
|
||||||
</Button>
|
|
||||||
);
|
|
||||||
},
|
|
||||||
);
|
|
||||||
SidebarTrigger.displayName = "SidebarTrigger";
|
|
||||||
|
|
||||||
const SidebarRail = React.forwardRef<HTMLButtonElement, React.ComponentProps<"button">>(
|
|
||||||
({ className, ...props }, ref) => {
|
|
||||||
const { toggleSidebar } = useSidebar();
|
|
||||||
|
|
||||||
return (
|
|
||||||
<button
|
|
||||||
ref={ref}
|
|
||||||
data-sidebar="rail"
|
|
||||||
aria-label="Toggle Sidebar"
|
|
||||||
tabIndex={-1}
|
|
||||||
onClick={toggleSidebar}
|
|
||||||
title="Toggle Sidebar"
|
|
||||||
className={cn(
|
|
||||||
"absolute inset-y-0 z-20 hidden w-4 -translate-x-1/2 transition-all ease-linear after:absolute after:inset-y-0 after:left-1/2 after:w-[2px] group-data-[side=left]:-right-4 group-data-[side=right]:left-0 hover:after:bg-sidebar-border sm:flex",
|
|
||||||
"[[data-side=left]_&]:cursor-w-resize [[data-side=right]_&]:cursor-e-resize",
|
|
||||||
"[[data-side=left][data-state=collapsed]_&]:cursor-e-resize [[data-side=right][data-state=collapsed]_&]:cursor-w-resize",
|
|
||||||
"group-data-[collapsible=offcanvas]:translate-x-0 group-data-[collapsible=offcanvas]:after:left-full group-data-[collapsible=offcanvas]:hover:bg-sidebar",
|
|
||||||
"[[data-side=left][data-collapsible=offcanvas]_&]:-right-2",
|
|
||||||
"[[data-side=right][data-collapsible=offcanvas]_&]:-left-2",
|
|
||||||
className,
|
|
||||||
)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
},
|
|
||||||
);
|
|
||||||
SidebarRail.displayName = "SidebarRail";
|
|
||||||
|
|
||||||
const SidebarInset = React.forwardRef<HTMLDivElement, React.ComponentProps<"main">>(({ className, ...props }, ref) => {
|
|
||||||
return (
|
|
||||||
<main
|
|
||||||
ref={ref}
|
|
||||||
className={cn(
|
|
||||||
"relative flex min-h-svh flex-1 flex-col bg-background",
|
|
||||||
"peer-data-[variant=inset]:min-h-[calc(100svh-theme(spacing.4))] md:peer-data-[variant=inset]:m-2 md:peer-data-[state=collapsed]:peer-data-[variant=inset]:ml-2 md:peer-data-[variant=inset]:ml-0 md:peer-data-[variant=inset]:rounded-xl md:peer-data-[variant=inset]:shadow",
|
|
||||||
className,
|
|
||||||
)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
});
|
|
||||||
SidebarInset.displayName = "SidebarInset";
|
|
||||||
|
|
||||||
const SidebarInput = React.forwardRef<React.ElementRef<typeof Input>, React.ComponentProps<typeof Input>>(
|
|
||||||
({ className, ...props }, ref) => {
|
|
||||||
return (
|
|
||||||
<Input
|
|
||||||
ref={ref}
|
|
||||||
data-sidebar="input"
|
|
||||||
className={cn(
|
|
||||||
"h-8 w-full bg-background shadow-none focus-visible:ring-2 focus-visible:ring-sidebar-ring",
|
|
||||||
className,
|
|
||||||
)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
},
|
|
||||||
);
|
|
||||||
SidebarInput.displayName = "SidebarInput";
|
|
||||||
|
|
||||||
const SidebarHeader = React.forwardRef<HTMLDivElement, React.ComponentProps<"div">>(({ className, ...props }, ref) => {
|
|
||||||
return <div ref={ref} data-sidebar="header" className={cn("flex flex-col gap-2 p-2", className)} {...props} />;
|
|
||||||
});
|
|
||||||
SidebarHeader.displayName = "SidebarHeader";
|
|
||||||
|
|
||||||
const SidebarFooter = React.forwardRef<HTMLDivElement, React.ComponentProps<"div">>(({ className, ...props }, ref) => {
|
|
||||||
return <div ref={ref} data-sidebar="footer" className={cn("flex flex-col gap-2 p-2", className)} {...props} />;
|
|
||||||
});
|
|
||||||
SidebarFooter.displayName = "SidebarFooter";
|
|
||||||
|
|
||||||
const SidebarSeparator = React.forwardRef<React.ElementRef<typeof Separator>, React.ComponentProps<typeof Separator>>(
|
|
||||||
({ className, ...props }, ref) => {
|
|
||||||
return (
|
|
||||||
<Separator
|
|
||||||
ref={ref}
|
|
||||||
data-sidebar="separator"
|
|
||||||
className={cn("mx-2 w-auto bg-sidebar-border", className)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
},
|
|
||||||
);
|
|
||||||
SidebarSeparator.displayName = "SidebarSeparator";
|
|
||||||
|
|
||||||
const SidebarContent = React.forwardRef<HTMLDivElement, React.ComponentProps<"div">>(({ className, ...props }, ref) => {
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
ref={ref}
|
|
||||||
data-sidebar="content"
|
|
||||||
className={cn(
|
|
||||||
"flex min-h-0 flex-1 flex-col gap-2 overflow-auto group-data-[collapsible=icon]:overflow-hidden",
|
|
||||||
className,
|
|
||||||
)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
});
|
|
||||||
SidebarContent.displayName = "SidebarContent";
|
|
||||||
|
|
||||||
const SidebarGroup = React.forwardRef<HTMLDivElement, React.ComponentProps<"div">>(({ className, ...props }, ref) => {
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
ref={ref}
|
|
||||||
data-sidebar="group"
|
|
||||||
className={cn("relative flex w-full min-w-0 flex-col p-2", className)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
});
|
|
||||||
SidebarGroup.displayName = "SidebarGroup";
|
|
||||||
|
|
||||||
const SidebarGroupLabel = React.forwardRef<HTMLDivElement, React.ComponentProps<"div"> & { asChild?: boolean }>(
|
|
||||||
({ className, asChild = false, ...props }, ref) => {
|
|
||||||
const Comp = asChild ? Slot : "div";
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Comp
|
|
||||||
ref={ref}
|
|
||||||
data-sidebar="group-label"
|
|
||||||
className={cn(
|
|
||||||
"flex h-8 shrink-0 items-center rounded-md px-2 text-xs font-medium text-sidebar-foreground/70 outline-none ring-sidebar-ring transition-[margin,opa] duration-200 ease-linear focus-visible:ring-2 [&>svg]:size-4 [&>svg]:shrink-0",
|
|
||||||
"group-data-[collapsible=icon]:-mt-8 group-data-[collapsible=icon]:opacity-0",
|
|
||||||
className,
|
|
||||||
)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
},
|
|
||||||
);
|
|
||||||
SidebarGroupLabel.displayName = "SidebarGroupLabel";
|
|
||||||
|
|
||||||
const SidebarGroupAction = React.forwardRef<HTMLButtonElement, React.ComponentProps<"button"> & { asChild?: boolean }>(
|
|
||||||
({ className, asChild = false, ...props }, ref) => {
|
|
||||||
const Comp = asChild ? Slot : "button";
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Comp
|
|
||||||
ref={ref}
|
|
||||||
data-sidebar="group-action"
|
|
||||||
className={cn(
|
|
||||||
"absolute right-3 top-3.5 flex aspect-square w-5 items-center justify-center rounded-md p-0 text-sidebar-foreground outline-none ring-sidebar-ring transition-transform hover:bg-sidebar-accent hover:text-sidebar-accent-foreground focus-visible:ring-2 [&>svg]:size-4 [&>svg]:shrink-0",
|
|
||||||
// Increases the hit area of the button on mobile.
|
|
||||||
"after:absolute after:-inset-2 after:md:hidden",
|
|
||||||
"group-data-[collapsible=icon]:hidden",
|
|
||||||
className,
|
|
||||||
)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
},
|
|
||||||
);
|
|
||||||
SidebarGroupAction.displayName = "SidebarGroupAction";
|
|
||||||
|
|
||||||
const SidebarGroupContent = React.forwardRef<HTMLDivElement, React.ComponentProps<"div">>(
|
|
||||||
({ className, ...props }, ref) => (
|
|
||||||
<div ref={ref} data-sidebar="group-content" className={cn("w-full text-sm", className)} {...props} />
|
|
||||||
),
|
|
||||||
);
|
|
||||||
SidebarGroupContent.displayName = "SidebarGroupContent";
|
|
||||||
|
|
||||||
const SidebarMenu = React.forwardRef<HTMLUListElement, React.ComponentProps<"ul">>(({ className, ...props }, ref) => (
|
|
||||||
<ul ref={ref} data-sidebar="menu" className={cn("flex w-full min-w-0 flex-col gap-1", className)} {...props} />
|
|
||||||
));
|
|
||||||
SidebarMenu.displayName = "SidebarMenu";
|
|
||||||
|
|
||||||
const SidebarMenuItem = React.forwardRef<HTMLLIElement, React.ComponentProps<"li">>(({ className, ...props }, ref) => (
|
|
||||||
<li ref={ref} data-sidebar="menu-item" className={cn("group/menu-item relative", className)} {...props} />
|
|
||||||
));
|
|
||||||
SidebarMenuItem.displayName = "SidebarMenuItem";
|
|
||||||
|
|
||||||
const sidebarMenuButtonVariants = cva(
|
|
||||||
"peer/menu-button flex w-full items-center gap-2 overflow-hidden rounded-md p-2 text-left text-sm outline-none ring-sidebar-ring transition-[width,height,padding] hover:bg-sidebar-accent hover:text-sidebar-accent-foreground focus-visible:ring-2 active:bg-sidebar-accent active:text-sidebar-accent-foreground disabled:pointer-events-none disabled:opacity-50 group-has-[[data-sidebar=menu-action]]/menu-item:pr-8 aria-disabled:pointer-events-none aria-disabled:opacity-50 data-[active=true]:bg-sidebar-accent data-[active=true]:font-medium data-[active=true]:text-sidebar-accent-foreground data-[state=open]:hover:bg-sidebar-accent data-[state=open]:hover:text-sidebar-accent-foreground group-data-[collapsible=icon]:!size-8 group-data-[collapsible=icon]:!p-2 [&>span:last-child]:truncate [&>svg]:size-4 [&>svg]:shrink-0",
|
|
||||||
{
|
|
||||||
variants: {
|
|
||||||
variant: {
|
|
||||||
default: "hover:bg-sidebar-accent hover:text-sidebar-accent-foreground",
|
|
||||||
outline:
|
|
||||||
"bg-background shadow-[0_0_0_1px_hsl(var(--sidebar-border))] hover:bg-sidebar-accent hover:text-sidebar-accent-foreground hover:shadow-[0_0_0_1px_hsl(var(--sidebar-accent))]",
|
|
||||||
},
|
|
||||||
size: {
|
|
||||||
default: "h-8 text-sm",
|
|
||||||
sm: "h-7 text-xs",
|
|
||||||
lg: "h-12 text-sm group-data-[collapsible=icon]:!p-0",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
defaultVariants: {
|
|
||||||
variant: "default",
|
|
||||||
size: "default",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
const SidebarMenuButton = React.forwardRef<
|
|
||||||
HTMLButtonElement,
|
|
||||||
React.ComponentProps<"button"> & {
|
|
||||||
asChild?: boolean;
|
|
||||||
isActive?: boolean;
|
|
||||||
tooltip?: string | React.ComponentProps<typeof TooltipContent>;
|
|
||||||
} & VariantProps<typeof sidebarMenuButtonVariants>
|
|
||||||
>(({ asChild = false, isActive = false, variant = "default", size = "default", tooltip, className, ...props }, ref) => {
|
|
||||||
const Comp = asChild ? Slot : "button";
|
|
||||||
const { isMobile, state } = useSidebar();
|
|
||||||
|
|
||||||
const button = (
|
|
||||||
<Comp
|
|
||||||
ref={ref}
|
|
||||||
data-sidebar="menu-button"
|
|
||||||
data-size={size}
|
|
||||||
data-active={isActive}
|
|
||||||
className={cn(sidebarMenuButtonVariants({ variant, size }), className)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
|
|
||||||
if (!tooltip) {
|
|
||||||
return button;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (typeof tooltip === "string") {
|
|
||||||
tooltip = {
|
|
||||||
children: tooltip,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Tooltip>
|
|
||||||
<TooltipTrigger asChild>{button}</TooltipTrigger>
|
|
||||||
<TooltipContent side="right" align="center" hidden={state !== "collapsed" || isMobile} {...tooltip} />
|
|
||||||
</Tooltip>
|
|
||||||
);
|
|
||||||
});
|
|
||||||
SidebarMenuButton.displayName = "SidebarMenuButton";
|
|
||||||
|
|
||||||
const SidebarMenuAction = React.forwardRef<
|
|
||||||
HTMLButtonElement,
|
|
||||||
React.ComponentProps<"button"> & {
|
|
||||||
asChild?: boolean;
|
|
||||||
showOnHover?: boolean;
|
|
||||||
}
|
|
||||||
>(({ className, asChild = false, showOnHover = false, ...props }, ref) => {
|
|
||||||
const Comp = asChild ? Slot : "button";
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Comp
|
|
||||||
ref={ref}
|
|
||||||
data-sidebar="menu-action"
|
|
||||||
className={cn(
|
|
||||||
"absolute right-1 top-1.5 flex aspect-square w-5 items-center justify-center rounded-md p-0 text-sidebar-foreground outline-none ring-sidebar-ring transition-transform peer-hover/menu-button:text-sidebar-accent-foreground hover:bg-sidebar-accent hover:text-sidebar-accent-foreground focus-visible:ring-2 [&>svg]:size-4 [&>svg]:shrink-0",
|
|
||||||
// Increases the hit area of the button on mobile.
|
|
||||||
"after:absolute after:-inset-2 after:md:hidden",
|
|
||||||
"peer-data-[size=sm]/menu-button:top-1",
|
|
||||||
"peer-data-[size=default]/menu-button:top-1.5",
|
|
||||||
"peer-data-[size=lg]/menu-button:top-2.5",
|
|
||||||
"group-data-[collapsible=icon]:hidden",
|
|
||||||
showOnHover &&
|
|
||||||
"group-focus-within/menu-item:opacity-100 group-hover/menu-item:opacity-100 data-[state=open]:opacity-100 peer-data-[active=true]/menu-button:text-sidebar-accent-foreground md:opacity-0",
|
|
||||||
className,
|
|
||||||
)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
});
|
|
||||||
SidebarMenuAction.displayName = "SidebarMenuAction";
|
|
||||||
|
|
||||||
const SidebarMenuBadge = React.forwardRef<HTMLDivElement, React.ComponentProps<"div">>(
|
|
||||||
({ className, ...props }, ref) => (
|
|
||||||
<div
|
|
||||||
ref={ref}
|
|
||||||
data-sidebar="menu-badge"
|
|
||||||
className={cn(
|
|
||||||
"pointer-events-none absolute right-1 flex h-5 min-w-5 select-none items-center justify-center rounded-md px-1 text-xs font-medium tabular-nums text-sidebar-foreground",
|
|
||||||
"peer-hover/menu-button:text-sidebar-accent-foreground peer-data-[active=true]/menu-button:text-sidebar-accent-foreground",
|
|
||||||
"peer-data-[size=sm]/menu-button:top-1",
|
|
||||||
"peer-data-[size=default]/menu-button:top-1.5",
|
|
||||||
"peer-data-[size=lg]/menu-button:top-2.5",
|
|
||||||
"group-data-[collapsible=icon]:hidden",
|
|
||||||
className,
|
|
||||||
)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
),
|
|
||||||
);
|
|
||||||
SidebarMenuBadge.displayName = "SidebarMenuBadge";
|
|
||||||
|
|
||||||
const SidebarMenuSkeleton = React.forwardRef<
|
|
||||||
HTMLDivElement,
|
|
||||||
React.ComponentProps<"div"> & {
|
|
||||||
showIcon?: boolean;
|
|
||||||
}
|
|
||||||
>(({ className, showIcon = false, ...props }, ref) => {
|
|
||||||
// Random width between 50 to 90%.
|
|
||||||
const width = React.useMemo(() => {
|
|
||||||
return `${Math.floor(Math.random() * 40) + 50}%`;
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
ref={ref}
|
|
||||||
data-sidebar="menu-skeleton"
|
|
||||||
className={cn("flex h-8 items-center gap-2 rounded-md px-2", className)}
|
|
||||||
{...props}
|
|
||||||
>
|
|
||||||
{showIcon && <Skeleton className="size-4 rounded-md" data-sidebar="menu-skeleton-icon" />}
|
|
||||||
<Skeleton
|
|
||||||
className="h-4 max-w-[--skeleton-width] flex-1"
|
|
||||||
data-sidebar="menu-skeleton-text"
|
|
||||||
style={
|
|
||||||
{
|
|
||||||
"--skeleton-width": width,
|
|
||||||
} as React.CSSProperties
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
});
|
|
||||||
SidebarMenuSkeleton.displayName = "SidebarMenuSkeleton";
|
|
||||||
|
|
||||||
const SidebarMenuSub = React.forwardRef<HTMLUListElement, React.ComponentProps<"ul">>(
|
|
||||||
({ className, ...props }, ref) => (
|
|
||||||
<ul
|
|
||||||
ref={ref}
|
|
||||||
data-sidebar="menu-sub"
|
|
||||||
className={cn(
|
|
||||||
"mx-3.5 flex min-w-0 translate-x-px flex-col gap-1 border-l border-sidebar-border px-2.5 py-0.5",
|
|
||||||
"group-data-[collapsible=icon]:hidden",
|
|
||||||
className,
|
|
||||||
)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
),
|
|
||||||
);
|
|
||||||
SidebarMenuSub.displayName = "SidebarMenuSub";
|
|
||||||
|
|
||||||
const SidebarMenuSubItem = React.forwardRef<HTMLLIElement, React.ComponentProps<"li">>(({ ...props }, ref) => (
|
|
||||||
<li ref={ref} {...props} />
|
|
||||||
));
|
|
||||||
SidebarMenuSubItem.displayName = "SidebarMenuSubItem";
|
|
||||||
|
|
||||||
const SidebarMenuSubButton = React.forwardRef<
|
|
||||||
HTMLAnchorElement,
|
|
||||||
React.ComponentProps<"a"> & {
|
|
||||||
asChild?: boolean;
|
|
||||||
size?: "sm" | "md";
|
|
||||||
isActive?: boolean;
|
|
||||||
}
|
|
||||||
>(({ asChild = false, size = "md", isActive, className, ...props }, ref) => {
|
|
||||||
const Comp = asChild ? Slot : "a";
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Comp
|
|
||||||
ref={ref}
|
|
||||||
data-sidebar="menu-sub-button"
|
|
||||||
data-size={size}
|
|
||||||
data-active={isActive}
|
|
||||||
className={cn(
|
|
||||||
"flex h-7 min-w-0 -translate-x-px items-center gap-2 overflow-hidden rounded-md px-2 text-sidebar-foreground outline-none ring-sidebar-ring aria-disabled:pointer-events-none aria-disabled:opacity-50 hover:bg-sidebar-accent hover:text-sidebar-accent-foreground focus-visible:ring-2 active:bg-sidebar-accent active:text-sidebar-accent-foreground disabled:pointer-events-none disabled:opacity-50 [&>span:last-child]:truncate [&>svg]:size-4 [&>svg]:shrink-0 [&>svg]:text-sidebar-accent-foreground",
|
|
||||||
"data-[active=true]:bg-sidebar-accent data-[active=true]:text-sidebar-accent-foreground",
|
|
||||||
size === "sm" && "text-xs",
|
|
||||||
size === "md" && "text-sm",
|
|
||||||
"group-data-[collapsible=icon]:hidden",
|
|
||||||
className,
|
|
||||||
)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
});
|
|
||||||
SidebarMenuSubButton.displayName = "SidebarMenuSubButton";
|
|
||||||
|
|
||||||
export {
|
|
||||||
Sidebar,
|
|
||||||
SidebarContent,
|
|
||||||
SidebarFooter,
|
|
||||||
SidebarGroup,
|
|
||||||
SidebarGroupAction,
|
|
||||||
SidebarGroupContent,
|
|
||||||
SidebarGroupLabel,
|
|
||||||
SidebarHeader,
|
|
||||||
SidebarInput,
|
|
||||||
SidebarInset,
|
|
||||||
SidebarMenu,
|
|
||||||
SidebarMenuAction,
|
|
||||||
SidebarMenuBadge,
|
|
||||||
SidebarMenuButton,
|
|
||||||
SidebarMenuItem,
|
|
||||||
SidebarMenuSkeleton,
|
|
||||||
SidebarMenuSub,
|
|
||||||
SidebarMenuSubButton,
|
|
||||||
SidebarMenuSubItem,
|
|
||||||
SidebarProvider,
|
|
||||||
SidebarRail,
|
|
||||||
SidebarSeparator,
|
|
||||||
SidebarTrigger,
|
|
||||||
useSidebar,
|
|
||||||
};
|
|
||||||
@@ -1,7 +0,0 @@
|
|||||||
import { cn } from "@/lib/utils";
|
|
||||||
|
|
||||||
function Skeleton({ className, ...props }: React.HTMLAttributes<HTMLDivElement>) {
|
|
||||||
return <div className={cn("animate-pulse rounded-md bg-muted", className)} {...props} />;
|
|
||||||
}
|
|
||||||
|
|
||||||
export { Skeleton };
|
|
||||||
@@ -1,23 +0,0 @@
|
|||||||
import * as React from "react";
|
|
||||||
import * as SliderPrimitive from "@radix-ui/react-slider";
|
|
||||||
|
|
||||||
import { cn } from "@/lib/utils";
|
|
||||||
|
|
||||||
const Slider = React.forwardRef<
|
|
||||||
React.ElementRef<typeof SliderPrimitive.Root>,
|
|
||||||
React.ComponentPropsWithoutRef<typeof SliderPrimitive.Root>
|
|
||||||
>(({ className, ...props }, ref) => (
|
|
||||||
<SliderPrimitive.Root
|
|
||||||
ref={ref}
|
|
||||||
className={cn("relative flex w-full touch-none select-none items-center", className)}
|
|
||||||
{...props}
|
|
||||||
>
|
|
||||||
<SliderPrimitive.Track className="relative h-2 w-full grow overflow-hidden rounded-full bg-secondary">
|
|
||||||
<SliderPrimitive.Range className="absolute h-full bg-primary" />
|
|
||||||
</SliderPrimitive.Track>
|
|
||||||
<SliderPrimitive.Thumb className="block h-5 w-5 rounded-full border-2 border-primary bg-background ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50" />
|
|
||||||
</SliderPrimitive.Root>
|
|
||||||
));
|
|
||||||
Slider.displayName = SliderPrimitive.Root.displayName;
|
|
||||||
|
|
||||||
export { Slider };
|
|
||||||
@@ -1,27 +0,0 @@
|
|||||||
import * as React from "react";
|
|
||||||
import * as SwitchPrimitives from "@radix-ui/react-switch";
|
|
||||||
|
|
||||||
import { cn } from "@/lib/utils";
|
|
||||||
|
|
||||||
const Switch = React.forwardRef<
|
|
||||||
React.ElementRef<typeof SwitchPrimitives.Root>,
|
|
||||||
React.ComponentPropsWithoutRef<typeof SwitchPrimitives.Root>
|
|
||||||
>(({ className, ...props }, ref) => (
|
|
||||||
<SwitchPrimitives.Root
|
|
||||||
className={cn(
|
|
||||||
"peer inline-flex h-6 w-11 shrink-0 cursor-pointer items-center rounded-full border-2 border-transparent transition-colors data-[state=checked]:bg-primary data-[state=unchecked]:bg-input focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background disabled:cursor-not-allowed disabled:opacity-50",
|
|
||||||
className,
|
|
||||||
)}
|
|
||||||
{...props}
|
|
||||||
ref={ref}
|
|
||||||
>
|
|
||||||
<SwitchPrimitives.Thumb
|
|
||||||
className={cn(
|
|
||||||
"pointer-events-none block h-5 w-5 rounded-full bg-background shadow-lg ring-0 transition-transform data-[state=checked]:translate-x-5 data-[state=unchecked]:translate-x-0",
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
</SwitchPrimitives.Root>
|
|
||||||
));
|
|
||||||
Switch.displayName = SwitchPrimitives.Root.displayName;
|
|
||||||
|
|
||||||
export { Switch };
|
|
||||||
@@ -1,72 +0,0 @@
|
|||||||
import * as React from "react";
|
|
||||||
|
|
||||||
import { cn } from "@/lib/utils";
|
|
||||||
|
|
||||||
const Table = React.forwardRef<HTMLTableElement, React.HTMLAttributes<HTMLTableElement>>(
|
|
||||||
({ className, ...props }, ref) => (
|
|
||||||
<div className="relative w-full overflow-auto">
|
|
||||||
<table ref={ref} className={cn("w-full caption-bottom text-sm", className)} {...props} />
|
|
||||||
</div>
|
|
||||||
),
|
|
||||||
);
|
|
||||||
Table.displayName = "Table";
|
|
||||||
|
|
||||||
const TableHeader = React.forwardRef<HTMLTableSectionElement, React.HTMLAttributes<HTMLTableSectionElement>>(
|
|
||||||
({ className, ...props }, ref) => <thead ref={ref} className={cn("[&_tr]:border-b", className)} {...props} />,
|
|
||||||
);
|
|
||||||
TableHeader.displayName = "TableHeader";
|
|
||||||
|
|
||||||
const TableBody = React.forwardRef<HTMLTableSectionElement, React.HTMLAttributes<HTMLTableSectionElement>>(
|
|
||||||
({ className, ...props }, ref) => (
|
|
||||||
<tbody ref={ref} className={cn("[&_tr:last-child]:border-0", className)} {...props} />
|
|
||||||
),
|
|
||||||
);
|
|
||||||
TableBody.displayName = "TableBody";
|
|
||||||
|
|
||||||
const TableFooter = React.forwardRef<HTMLTableSectionElement, React.HTMLAttributes<HTMLTableSectionElement>>(
|
|
||||||
({ className, ...props }, ref) => (
|
|
||||||
<tfoot ref={ref} className={cn("border-t bg-muted/50 font-medium [&>tr]:last:border-b-0", className)} {...props} />
|
|
||||||
),
|
|
||||||
);
|
|
||||||
TableFooter.displayName = "TableFooter";
|
|
||||||
|
|
||||||
const TableRow = React.forwardRef<HTMLTableRowElement, React.HTMLAttributes<HTMLTableRowElement>>(
|
|
||||||
({ className, ...props }, ref) => (
|
|
||||||
<tr
|
|
||||||
ref={ref}
|
|
||||||
className={cn("border-b transition-colors data-[state=selected]:bg-muted hover:bg-muted/50", className)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
),
|
|
||||||
);
|
|
||||||
TableRow.displayName = "TableRow";
|
|
||||||
|
|
||||||
const TableHead = React.forwardRef<HTMLTableCellElement, React.ThHTMLAttributes<HTMLTableCellElement>>(
|
|
||||||
({ className, ...props }, ref) => (
|
|
||||||
<th
|
|
||||||
ref={ref}
|
|
||||||
className={cn(
|
|
||||||
"h-12 px-4 text-left align-middle font-medium text-muted-foreground [&:has([role=checkbox])]:pr-0",
|
|
||||||
className,
|
|
||||||
)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
),
|
|
||||||
);
|
|
||||||
TableHead.displayName = "TableHead";
|
|
||||||
|
|
||||||
const TableCell = React.forwardRef<HTMLTableCellElement, React.TdHTMLAttributes<HTMLTableCellElement>>(
|
|
||||||
({ className, ...props }, ref) => (
|
|
||||||
<td ref={ref} className={cn("p-4 align-middle [&:has([role=checkbox])]:pr-0", className)} {...props} />
|
|
||||||
),
|
|
||||||
);
|
|
||||||
TableCell.displayName = "TableCell";
|
|
||||||
|
|
||||||
const TableCaption = React.forwardRef<HTMLTableCaptionElement, React.HTMLAttributes<HTMLTableCaptionElement>>(
|
|
||||||
({ className, ...props }, ref) => (
|
|
||||||
<caption ref={ref} className={cn("mt-4 text-sm text-muted-foreground", className)} {...props} />
|
|
||||||
),
|
|
||||||
);
|
|
||||||
TableCaption.displayName = "TableCaption";
|
|
||||||
|
|
||||||
export { Table, TableHeader, TableBody, TableFooter, TableHead, TableRow, TableCell, TableCaption };
|
|
||||||
+39
-46
@@ -1,53 +1,46 @@
|
|||||||
import * as React from "react";
|
import * as React from "react";
|
||||||
import * as TabsPrimitive from "@radix-ui/react-tabs";
|
|
||||||
|
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
|
|
||||||
const Tabs = TabsPrimitive.Root;
|
const TabsContext = React.createContext<{ value: string; setValue: (v: string) => void }>({ value: "", setValue: () => {} });
|
||||||
|
|
||||||
const TabsList = React.forwardRef<
|
export function Tabs({ defaultValue = "", value: controlled, onValueChange, className, children, ...props }: React.HTMLAttributes<HTMLDivElement> & { defaultValue?: string; value?: string; onValueChange?: (v: string) => void }) {
|
||||||
React.ElementRef<typeof TabsPrimitive.List>,
|
const [internal, setInternal] = React.useState(defaultValue);
|
||||||
React.ComponentPropsWithoutRef<typeof TabsPrimitive.List>
|
const value = controlled ?? internal;
|
||||||
>(({ className, ...props }, ref) => (
|
const setValue = (v: string) => { setInternal(v); onValueChange?.(v); };
|
||||||
<TabsPrimitive.List
|
return (
|
||||||
ref={ref}
|
<TabsContext.Provider value={{ value, setValue }}>
|
||||||
className={cn(
|
<div className={cn("w-full", className)} {...props}>{children}</div>
|
||||||
"inline-flex h-10 items-center justify-center rounded-md bg-muted p-1 text-muted-foreground",
|
</TabsContext.Provider>
|
||||||
className,
|
);
|
||||||
)}
|
}
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
));
|
|
||||||
TabsList.displayName = TabsPrimitive.List.displayName;
|
|
||||||
|
|
||||||
const TabsTrigger = React.forwardRef<
|
export function TabsList({ className, ...props }: React.HTMLAttributes<HTMLDivElement>) {
|
||||||
React.ElementRef<typeof TabsPrimitive.Trigger>,
|
return <div className={cn("inline-flex items-center justify-center rounded-lg bg-slate-100 p-1", className)} {...props} />;
|
||||||
React.ComponentPropsWithoutRef<typeof TabsPrimitive.Trigger>
|
}
|
||||||
>(({ className, ...props }, ref) => (
|
|
||||||
<TabsPrimitive.Trigger
|
|
||||||
ref={ref}
|
|
||||||
className={cn(
|
|
||||||
"inline-flex items-center justify-center whitespace-nowrap rounded-sm px-3 py-1.5 text-sm font-medium ring-offset-background transition-all data-[state=active]:bg-background data-[state=active]:text-foreground data-[state=active]:shadow-sm focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50",
|
|
||||||
className,
|
|
||||||
)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
));
|
|
||||||
TabsTrigger.displayName = TabsPrimitive.Trigger.displayName;
|
|
||||||
|
|
||||||
const TabsContent = React.forwardRef<
|
export function TabsTrigger({ value, className, children, ...props }: React.ButtonHTMLAttributes<HTMLButtonElement> & { value: string }) {
|
||||||
React.ElementRef<typeof TabsPrimitive.Content>,
|
const ctx = React.useContext(TabsContext);
|
||||||
React.ComponentPropsWithoutRef<typeof TabsPrimitive.Content>
|
const active = ctx.value === value;
|
||||||
>(({ className, ...props }, ref) => (
|
return (
|
||||||
<TabsPrimitive.Content
|
<button
|
||||||
ref={ref}
|
type="button"
|
||||||
className={cn(
|
onClick={() => ctx.setValue(value)}
|
||||||
"mt-2 ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2",
|
className={cn(
|
||||||
className,
|
"inline-flex items-center justify-center rounded-md px-3 py-1.5 text-sm font-medium transition-colors",
|
||||||
)}
|
active ? "bg-white text-slate-950 shadow-sm" : "text-slate-500 hover:text-slate-900",
|
||||||
{...props}
|
className
|
||||||
/>
|
)}
|
||||||
));
|
{...props}
|
||||||
TabsContent.displayName = TabsPrimitive.Content.displayName;
|
>
|
||||||
|
{children}
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
export { Tabs, TabsList, TabsTrigger, TabsContent };
|
export function TabsContent({ value, className, children, ...props }: React.HTMLAttributes<HTMLDivElement> & { value: string }) {
|
||||||
|
const ctx = React.useContext(TabsContext);
|
||||||
|
if (ctx.value !== value) return null;
|
||||||
|
return <div className={cn("mt-2", className)} {...props}>{children}</div>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Tabs;
|
||||||
|
|||||||
@@ -1,21 +1,18 @@
|
|||||||
import * as React from "react";
|
import * as React from "react";
|
||||||
|
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
|
|
||||||
export interface TextareaProps extends React.TextareaHTMLAttributes<HTMLTextAreaElement> {}
|
export const Textarea = React.forwardRef<HTMLTextAreaElement, React.TextareaHTMLAttributes<HTMLTextAreaElement>>(
|
||||||
|
({ className, ...props }, ref) => (
|
||||||
const Textarea = React.forwardRef<HTMLTextAreaElement, TextareaProps>(({ className, ...props }, ref) => {
|
|
||||||
return (
|
|
||||||
<textarea
|
<textarea
|
||||||
className={cn(
|
|
||||||
"flex min-h-[80px] w-full rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50",
|
|
||||||
className,
|
|
||||||
)}
|
|
||||||
ref={ref}
|
ref={ref}
|
||||||
|
className={cn(
|
||||||
|
"flex min-h-[80px] w-full rounded-md border border-slate-300 bg-white px-3 py-2 text-sm placeholder:text-slate-400 focus:outline-none focus:ring-2 focus:ring-indigo-500 disabled:opacity-50",
|
||||||
|
className
|
||||||
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
);
|
)
|
||||||
});
|
);
|
||||||
Textarea.displayName = "Textarea";
|
Textarea.displayName = "Textarea";
|
||||||
|
|
||||||
export { Textarea };
|
export default Textarea;
|
||||||
|
|||||||
@@ -1,111 +0,0 @@
|
|||||||
import * as React from "react";
|
|
||||||
import * as ToastPrimitives from "@radix-ui/react-toast";
|
|
||||||
import { cva, type VariantProps } from "class-variance-authority";
|
|
||||||
import { X } from "lucide-react";
|
|
||||||
|
|
||||||
import { cn } from "@/lib/utils";
|
|
||||||
|
|
||||||
const ToastProvider = ToastPrimitives.Provider;
|
|
||||||
|
|
||||||
const ToastViewport = React.forwardRef<
|
|
||||||
React.ElementRef<typeof ToastPrimitives.Viewport>,
|
|
||||||
React.ComponentPropsWithoutRef<typeof ToastPrimitives.Viewport>
|
|
||||||
>(({ className, ...props }, ref) => (
|
|
||||||
<ToastPrimitives.Viewport
|
|
||||||
ref={ref}
|
|
||||||
className={cn(
|
|
||||||
"fixed top-0 z-[100] flex max-h-screen w-full flex-col-reverse p-4 sm:bottom-0 sm:right-0 sm:top-auto sm:flex-col md:max-w-[420px]",
|
|
||||||
className,
|
|
||||||
)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
));
|
|
||||||
ToastViewport.displayName = ToastPrimitives.Viewport.displayName;
|
|
||||||
|
|
||||||
const toastVariants = cva(
|
|
||||||
"group pointer-events-auto relative flex w-full items-center justify-between space-x-4 overflow-hidden rounded-md border p-6 pr-8 shadow-lg transition-all data-[swipe=cancel]:translate-x-0 data-[swipe=end]:translate-x-[var(--radix-toast-swipe-end-x)] data-[swipe=move]:translate-x-[var(--radix-toast-swipe-move-x)] data-[swipe=move]:transition-none data-[state=open]:animate-in data-[state=closed]:animate-out data-[swipe=end]:animate-out data-[state=closed]:fade-out-80 data-[state=closed]:slide-out-to-right-full data-[state=open]:slide-in-from-top-full data-[state=open]:sm:slide-in-from-bottom-full",
|
|
||||||
{
|
|
||||||
variants: {
|
|
||||||
variant: {
|
|
||||||
default: "border bg-background text-foreground",
|
|
||||||
destructive: "destructive group border-destructive bg-destructive text-destructive-foreground",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
defaultVariants: {
|
|
||||||
variant: "default",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
const Toast = React.forwardRef<
|
|
||||||
React.ElementRef<typeof ToastPrimitives.Root>,
|
|
||||||
React.ComponentPropsWithoutRef<typeof ToastPrimitives.Root> & VariantProps<typeof toastVariants>
|
|
||||||
>(({ className, variant, ...props }, ref) => {
|
|
||||||
return <ToastPrimitives.Root ref={ref} className={cn(toastVariants({ variant }), className)} {...props} />;
|
|
||||||
});
|
|
||||||
Toast.displayName = ToastPrimitives.Root.displayName;
|
|
||||||
|
|
||||||
const ToastAction = React.forwardRef<
|
|
||||||
React.ElementRef<typeof ToastPrimitives.Action>,
|
|
||||||
React.ComponentPropsWithoutRef<typeof ToastPrimitives.Action>
|
|
||||||
>(({ className, ...props }, ref) => (
|
|
||||||
<ToastPrimitives.Action
|
|
||||||
ref={ref}
|
|
||||||
className={cn(
|
|
||||||
"inline-flex h-8 shrink-0 items-center justify-center rounded-md border bg-transparent px-3 text-sm font-medium ring-offset-background transition-colors group-[.destructive]:border-muted/40 hover:bg-secondary group-[.destructive]:hover:border-destructive/30 group-[.destructive]:hover:bg-destructive group-[.destructive]:hover:text-destructive-foreground focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 group-[.destructive]:focus:ring-destructive disabled:pointer-events-none disabled:opacity-50",
|
|
||||||
className,
|
|
||||||
)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
));
|
|
||||||
ToastAction.displayName = ToastPrimitives.Action.displayName;
|
|
||||||
|
|
||||||
const ToastClose = React.forwardRef<
|
|
||||||
React.ElementRef<typeof ToastPrimitives.Close>,
|
|
||||||
React.ComponentPropsWithoutRef<typeof ToastPrimitives.Close>
|
|
||||||
>(({ className, ...props }, ref) => (
|
|
||||||
<ToastPrimitives.Close
|
|
||||||
ref={ref}
|
|
||||||
className={cn(
|
|
||||||
"absolute right-2 top-2 rounded-md p-1 text-foreground/50 opacity-0 transition-opacity group-hover:opacity-100 group-[.destructive]:text-red-300 hover:text-foreground group-[.destructive]:hover:text-red-50 focus:opacity-100 focus:outline-none focus:ring-2 group-[.destructive]:focus:ring-red-400 group-[.destructive]:focus:ring-offset-red-600",
|
|
||||||
className,
|
|
||||||
)}
|
|
||||||
toast-close=""
|
|
||||||
{...props}
|
|
||||||
>
|
|
||||||
<X className="h-4 w-4" />
|
|
||||||
</ToastPrimitives.Close>
|
|
||||||
));
|
|
||||||
ToastClose.displayName = ToastPrimitives.Close.displayName;
|
|
||||||
|
|
||||||
const ToastTitle = React.forwardRef<
|
|
||||||
React.ElementRef<typeof ToastPrimitives.Title>,
|
|
||||||
React.ComponentPropsWithoutRef<typeof ToastPrimitives.Title>
|
|
||||||
>(({ className, ...props }, ref) => (
|
|
||||||
<ToastPrimitives.Title ref={ref} className={cn("text-sm font-semibold", className)} {...props} />
|
|
||||||
));
|
|
||||||
ToastTitle.displayName = ToastPrimitives.Title.displayName;
|
|
||||||
|
|
||||||
const ToastDescription = React.forwardRef<
|
|
||||||
React.ElementRef<typeof ToastPrimitives.Description>,
|
|
||||||
React.ComponentPropsWithoutRef<typeof ToastPrimitives.Description>
|
|
||||||
>(({ className, ...props }, ref) => (
|
|
||||||
<ToastPrimitives.Description ref={ref} className={cn("text-sm opacity-90", className)} {...props} />
|
|
||||||
));
|
|
||||||
ToastDescription.displayName = ToastPrimitives.Description.displayName;
|
|
||||||
|
|
||||||
type ToastProps = React.ComponentPropsWithoutRef<typeof Toast>;
|
|
||||||
|
|
||||||
type ToastActionElement = React.ReactElement<typeof ToastAction>;
|
|
||||||
|
|
||||||
export {
|
|
||||||
type ToastProps,
|
|
||||||
type ToastActionElement,
|
|
||||||
ToastProvider,
|
|
||||||
ToastViewport,
|
|
||||||
Toast,
|
|
||||||
ToastTitle,
|
|
||||||
ToastDescription,
|
|
||||||
ToastClose,
|
|
||||||
ToastAction,
|
|
||||||
};
|
|
||||||
@@ -1,24 +0,0 @@
|
|||||||
import { useToast } from "@/hooks/use-toast";
|
|
||||||
import { Toast, ToastClose, ToastDescription, ToastProvider, ToastTitle, ToastViewport } from "@/components/ui/toast";
|
|
||||||
|
|
||||||
export function Toaster() {
|
|
||||||
const { toasts } = useToast();
|
|
||||||
|
|
||||||
return (
|
|
||||||
<ToastProvider>
|
|
||||||
{toasts.map(function ({ id, title, description, action, ...props }) {
|
|
||||||
return (
|
|
||||||
<Toast key={id} {...props}>
|
|
||||||
<div className="grid gap-1">
|
|
||||||
{title && <ToastTitle>{title}</ToastTitle>}
|
|
||||||
{description && <ToastDescription>{description}</ToastDescription>}
|
|
||||||
</div>
|
|
||||||
{action}
|
|
||||||
<ToastClose />
|
|
||||||
</Toast>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
<ToastViewport />
|
|
||||||
</ToastProvider>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,49 +0,0 @@
|
|||||||
import * as React from "react";
|
|
||||||
import * as ToggleGroupPrimitive from "@radix-ui/react-toggle-group";
|
|
||||||
import { type VariantProps } from "class-variance-authority";
|
|
||||||
|
|
||||||
import { cn } from "@/lib/utils";
|
|
||||||
import { toggleVariants } from "@/components/ui/toggle";
|
|
||||||
|
|
||||||
const ToggleGroupContext = React.createContext<VariantProps<typeof toggleVariants>>({
|
|
||||||
size: "default",
|
|
||||||
variant: "default",
|
|
||||||
});
|
|
||||||
|
|
||||||
const ToggleGroup = React.forwardRef<
|
|
||||||
React.ElementRef<typeof ToggleGroupPrimitive.Root>,
|
|
||||||
React.ComponentPropsWithoutRef<typeof ToggleGroupPrimitive.Root> & VariantProps<typeof toggleVariants>
|
|
||||||
>(({ className, variant, size, children, ...props }, ref) => (
|
|
||||||
<ToggleGroupPrimitive.Root ref={ref} className={cn("flex items-center justify-center gap-1", className)} {...props}>
|
|
||||||
<ToggleGroupContext.Provider value={{ variant, size }}>{children}</ToggleGroupContext.Provider>
|
|
||||||
</ToggleGroupPrimitive.Root>
|
|
||||||
));
|
|
||||||
|
|
||||||
ToggleGroup.displayName = ToggleGroupPrimitive.Root.displayName;
|
|
||||||
|
|
||||||
const ToggleGroupItem = React.forwardRef<
|
|
||||||
React.ElementRef<typeof ToggleGroupPrimitive.Item>,
|
|
||||||
React.ComponentPropsWithoutRef<typeof ToggleGroupPrimitive.Item> & VariantProps<typeof toggleVariants>
|
|
||||||
>(({ className, children, variant, size, ...props }, ref) => {
|
|
||||||
const context = React.useContext(ToggleGroupContext);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<ToggleGroupPrimitive.Item
|
|
||||||
ref={ref}
|
|
||||||
className={cn(
|
|
||||||
toggleVariants({
|
|
||||||
variant: context.variant || variant,
|
|
||||||
size: context.size || size,
|
|
||||||
}),
|
|
||||||
className,
|
|
||||||
)}
|
|
||||||
{...props}
|
|
||||||
>
|
|
||||||
{children}
|
|
||||||
</ToggleGroupPrimitive.Item>
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
ToggleGroupItem.displayName = ToggleGroupPrimitive.Item.displayName;
|
|
||||||
|
|
||||||
export { ToggleGroup, ToggleGroupItem };
|
|
||||||
@@ -1,37 +0,0 @@
|
|||||||
import * as React from "react";
|
|
||||||
import * as TogglePrimitive from "@radix-ui/react-toggle";
|
|
||||||
import { cva, type VariantProps } from "class-variance-authority";
|
|
||||||
|
|
||||||
import { cn } from "@/lib/utils";
|
|
||||||
|
|
||||||
const toggleVariants = cva(
|
|
||||||
"inline-flex items-center justify-center rounded-md text-sm font-medium ring-offset-background transition-colors hover:bg-muted hover:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 data-[state=on]:bg-accent data-[state=on]:text-accent-foreground",
|
|
||||||
{
|
|
||||||
variants: {
|
|
||||||
variant: {
|
|
||||||
default: "bg-transparent",
|
|
||||||
outline: "border border-input bg-transparent hover:bg-accent hover:text-accent-foreground",
|
|
||||||
},
|
|
||||||
size: {
|
|
||||||
default: "h-10 px-3",
|
|
||||||
sm: "h-9 px-2.5",
|
|
||||||
lg: "h-11 px-5",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
defaultVariants: {
|
|
||||||
variant: "default",
|
|
||||||
size: "default",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
const Toggle = React.forwardRef<
|
|
||||||
React.ElementRef<typeof TogglePrimitive.Root>,
|
|
||||||
React.ComponentPropsWithoutRef<typeof TogglePrimitive.Root> & VariantProps<typeof toggleVariants>
|
|
||||||
>(({ className, variant, size, ...props }, ref) => (
|
|
||||||
<TogglePrimitive.Root ref={ref} className={cn(toggleVariants({ variant, size, className }))} {...props} />
|
|
||||||
));
|
|
||||||
|
|
||||||
Toggle.displayName = TogglePrimitive.Root.displayName;
|
|
||||||
|
|
||||||
export { Toggle, toggleVariants };
|
|
||||||
@@ -1,28 +0,0 @@
|
|||||||
import * as React from "react";
|
|
||||||
import * as TooltipPrimitive from "@radix-ui/react-tooltip";
|
|
||||||
|
|
||||||
import { cn } from "@/lib/utils";
|
|
||||||
|
|
||||||
const TooltipProvider = TooltipPrimitive.Provider;
|
|
||||||
|
|
||||||
const Tooltip = TooltipPrimitive.Root;
|
|
||||||
|
|
||||||
const TooltipTrigger = TooltipPrimitive.Trigger;
|
|
||||||
|
|
||||||
const TooltipContent = React.forwardRef<
|
|
||||||
React.ElementRef<typeof TooltipPrimitive.Content>,
|
|
||||||
React.ComponentPropsWithoutRef<typeof TooltipPrimitive.Content>
|
|
||||||
>(({ className, sideOffset = 4, ...props }, ref) => (
|
|
||||||
<TooltipPrimitive.Content
|
|
||||||
ref={ref}
|
|
||||||
sideOffset={sideOffset}
|
|
||||||
className={cn(
|
|
||||||
"z-50 overflow-hidden rounded-md border bg-popover px-3 py-1.5 text-sm text-popover-foreground shadow-md animate-in fade-in-0 zoom-in-95 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
|
|
||||||
className,
|
|
||||||
)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
));
|
|
||||||
TooltipContent.displayName = TooltipPrimitive.Content.displayName;
|
|
||||||
|
|
||||||
export { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider };
|
|
||||||
@@ -1,3 +0,0 @@
|
|||||||
import { useToast, toast } from "@/hooks/use-toast";
|
|
||||||
|
|
||||||
export { useToast, toast };
|
|
||||||
@@ -1,234 +0,0 @@
|
|||||||
// ---------------------------------------------------------------------------
|
|
||||||
// AuthContext — pre-built auth provider wired to AnyDB users collection
|
|
||||||
// ---------------------------------------------------------------------------
|
|
||||||
// PRE-INSTALLED in every AUTO_DB React project with auth: true.
|
|
||||||
// CodeWriterAgent just wraps App in <AuthProvider> and uses:
|
|
||||||
// import { useAuth } from "@/contexts/AuthContext"
|
|
||||||
// const { user, login, signup, logout, loading } = useAuth()
|
|
||||||
//
|
|
||||||
// Interface already knows:
|
|
||||||
// - users collection has: email (unique), name, avatar_url, plan, role, created_at
|
|
||||||
// - login: findOne by email + password hash
|
|
||||||
// - signup: insertOne with default plan='free'
|
|
||||||
// - session: localStorage token with user object
|
|
||||||
// ---------------------------------------------------------------------------
|
|
||||||
|
|
||||||
import React, { createContext, useContext, useState, useEffect, useCallback } from "react";
|
|
||||||
import anydb from "@/lib/anydb";
|
|
||||||
// Toast helper — uses window alert as fallback if useToast not available
|
|
||||||
const notify = (title: string, desc?: string) => {
|
|
||||||
try {
|
|
||||||
// Try to dispatch a custom event that a Toaster component can pick up
|
|
||||||
window.dispatchEvent(new CustomEvent('app-toast', { detail: { title, description: desc } }));
|
|
||||||
} catch {
|
|
||||||
console.log(`[Auth] ${title}: ${desc || ''}`);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
interface User {
|
|
||||||
_id: string;
|
|
||||||
email: string;
|
|
||||||
name: string;
|
|
||||||
avatar_url?: string;
|
|
||||||
plan?: string;
|
|
||||||
role?: string;
|
|
||||||
created_at?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface AuthContextType {
|
|
||||||
user: User | null;
|
|
||||||
loading: boolean;
|
|
||||||
error: string | null;
|
|
||||||
login: (email: string, password: string) => Promise<boolean>;
|
|
||||||
signup: (name: string, email: string, password: string) => Promise<boolean>;
|
|
||||||
logout: () => void;
|
|
||||||
updateProfile: (updates: Partial<User>) => Promise<boolean>;
|
|
||||||
isAuthenticated: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
const AuthContext = createContext<AuthContextType | undefined>(undefined);
|
|
||||||
|
|
||||||
const AUTH_KEY = "anydb_auth_user";
|
|
||||||
|
|
||||||
export function AuthProvider({ children }: { children: React.ReactNode }) {
|
|
||||||
const [user, setUser] = useState<User | null>(null);
|
|
||||||
const [loading, setLoading] = useState(true);
|
|
||||||
const [error, setError] = useState<string | null>(null);
|
|
||||||
const toast = (opts: { title: string; description?: string; variant?: string }) => notify(opts.title, opts.description);
|
|
||||||
|
|
||||||
// Restore session from localStorage on mount
|
|
||||||
useEffect(() => {
|
|
||||||
try {
|
|
||||||
const stored = localStorage.getItem(AUTH_KEY);
|
|
||||||
if (stored) {
|
|
||||||
setUser(JSON.parse(stored));
|
|
||||||
}
|
|
||||||
} catch {
|
|
||||||
localStorage.removeItem(AUTH_KEY);
|
|
||||||
}
|
|
||||||
setLoading(false);
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const login = useCallback(async (email: string, password: string): Promise<boolean> => {
|
|
||||||
setLoading(true);
|
|
||||||
setError(null);
|
|
||||||
try {
|
|
||||||
const col = await anydb.collection("users");
|
|
||||||
const result = await col.find({ email: email.toLowerCase() }, { limit: 1 });
|
|
||||||
const found = result.data?.[0];
|
|
||||||
|
|
||||||
if (!found) {
|
|
||||||
setError("No account found with that email");
|
|
||||||
toast({ title: "Login failed", description: "No account found with that email", variant: "destructive" });
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Simple password check (in production, use bcrypt on server)
|
|
||||||
if (found.password_hash !== password && found.password !== password) {
|
|
||||||
setError("Incorrect password");
|
|
||||||
toast({ title: "Login failed", description: "Incorrect password", variant: "destructive" });
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
const { password_hash, password: _pw, ...safeUser } = found;
|
|
||||||
setUser(safeUser);
|
|
||||||
localStorage.setItem(AUTH_KEY, JSON.stringify(safeUser));
|
|
||||||
toast({ title: "Welcome back!", description: `Signed in as ${safeUser.name || safeUser.email}` });
|
|
||||||
return true;
|
|
||||||
} catch (err: any) {
|
|
||||||
setError(err.message || "Login failed");
|
|
||||||
toast({ title: "Error", description: "Something went wrong. Please try again.", variant: "destructive" });
|
|
||||||
return false;
|
|
||||||
} finally {
|
|
||||||
setLoading(false);
|
|
||||||
}
|
|
||||||
}, [toast]);
|
|
||||||
|
|
||||||
const signup = useCallback(async (name: string, email: string, password: string): Promise<boolean> => {
|
|
||||||
setLoading(true);
|
|
||||||
setError(null);
|
|
||||||
try {
|
|
||||||
const col = await anydb.collection("users");
|
|
||||||
|
|
||||||
// Check if email already exists
|
|
||||||
const existing = await col.find({ email: email.toLowerCase() }, { limit: 1 });
|
|
||||||
if (existing.data?.length > 0) {
|
|
||||||
setError("An account with that email already exists");
|
|
||||||
toast({ title: "Signup failed", description: "An account with that email already exists", variant: "destructive" });
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Create user
|
|
||||||
const newUser = await col.insertOne({
|
|
||||||
name,
|
|
||||||
email: email.toLowerCase(),
|
|
||||||
password_hash: password, // In production: hash with bcrypt on server
|
|
||||||
plan: "free",
|
|
||||||
role: "user",
|
|
||||||
avatar_url: "",
|
|
||||||
created_at: new Date().toISOString(),
|
|
||||||
});
|
|
||||||
|
|
||||||
const safeUser: User = {
|
|
||||||
_id: newUser._id || newUser.insertedId,
|
|
||||||
name,
|
|
||||||
email: email.toLowerCase(),
|
|
||||||
plan: "free",
|
|
||||||
role: "user",
|
|
||||||
created_at: new Date().toISOString(),
|
|
||||||
};
|
|
||||||
|
|
||||||
setUser(safeUser);
|
|
||||||
localStorage.setItem(AUTH_KEY, JSON.stringify(safeUser));
|
|
||||||
toast({ title: "Account created!", description: `Welcome, ${name}!` });
|
|
||||||
return true;
|
|
||||||
} catch (err: any) {
|
|
||||||
setError(err.message || "Signup failed");
|
|
||||||
toast({ title: "Error", description: "Could not create account. Please try again.", variant: "destructive" });
|
|
||||||
return false;
|
|
||||||
} finally {
|
|
||||||
setLoading(false);
|
|
||||||
}
|
|
||||||
}, [toast]);
|
|
||||||
|
|
||||||
const logout = useCallback(() => {
|
|
||||||
setUser(null);
|
|
||||||
localStorage.removeItem(AUTH_KEY);
|
|
||||||
toast({ title: "Signed out", description: "You have been signed out." });
|
|
||||||
}, [toast]);
|
|
||||||
|
|
||||||
const updateProfile = useCallback(async (updates: Partial<User>): Promise<boolean> => {
|
|
||||||
if (!user?._id) return false;
|
|
||||||
try {
|
|
||||||
const col = await anydb.collection("users");
|
|
||||||
await col.updateOne({ _id: user._id }, updates);
|
|
||||||
const updatedUser = { ...user, ...updates };
|
|
||||||
setUser(updatedUser);
|
|
||||||
localStorage.setItem(AUTH_KEY, JSON.stringify(updatedUser));
|
|
||||||
toast({ title: "Profile updated" });
|
|
||||||
return true;
|
|
||||||
} catch (err: any) {
|
|
||||||
toast({ title: "Error", description: "Could not update profile", variant: "destructive" });
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}, [user, toast]);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<AuthContext.Provider value={{
|
|
||||||
user,
|
|
||||||
loading,
|
|
||||||
error,
|
|
||||||
login,
|
|
||||||
signup,
|
|
||||||
logout,
|
|
||||||
updateProfile,
|
|
||||||
isAuthenticated: !!user,
|
|
||||||
}}>
|
|
||||||
{children}
|
|
||||||
</AuthContext.Provider>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Use auth state and actions.
|
|
||||||
*
|
|
||||||
* Usage:
|
|
||||||
* const { user, login, signup, logout, isAuthenticated, loading } = useAuth()
|
|
||||||
* if (!isAuthenticated) return <Navigate to="/login" />
|
|
||||||
* await login(email, password)
|
|
||||||
* await signup(name, email, password)
|
|
||||||
* logout()
|
|
||||||
*/
|
|
||||||
export function useAuth(): AuthContextType {
|
|
||||||
const context = useContext(AuthContext);
|
|
||||||
if (!context) {
|
|
||||||
throw new Error("useAuth must be used within an AuthProvider");
|
|
||||||
}
|
|
||||||
return context;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Protected route wrapper. Redirects to /login if not authenticated.
|
|
||||||
*
|
|
||||||
* Usage in App.tsx:
|
|
||||||
* <Route path="/dashboard" element={<ProtectedRoute><Dashboard /></ProtectedRoute>} />
|
|
||||||
*/
|
|
||||||
export function ProtectedRoute({ children }: { children: React.ReactNode }) {
|
|
||||||
const { isAuthenticated, loading } = useAuth();
|
|
||||||
|
|
||||||
if (loading) {
|
|
||||||
return (
|
|
||||||
<div className="min-h-screen flex items-center justify-center">
|
|
||||||
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-primary" />
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!isAuthenticated) {
|
|
||||||
// Redirect to login
|
|
||||||
window.location.href = "/login";
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return <>{children}</>;
|
|
||||||
}
|
|
||||||
@@ -1,292 +0,0 @@
|
|||||||
// ---------------------------------------------------------------------------
|
|
||||||
// useCollection + useMutation — pre-built AnyDB data hooks
|
|
||||||
// ---------------------------------------------------------------------------
|
|
||||||
// PRE-INSTALLED in every AUTO_DB React project. CodeWriterAgent just imports:
|
|
||||||
// import { useCollection, useMutation } from "@/hooks/use-anydb"
|
|
||||||
// const { data, loading, error, refetch } = useCollection('products', { sort: { created_at: -1 }, limit: 20 })
|
|
||||||
// const { mutate, loading: saving } = useMutation('products')
|
|
||||||
// ---------------------------------------------------------------------------
|
|
||||||
|
|
||||||
import { useState, useEffect, useCallback } from "react";
|
|
||||||
|
|
||||||
// AnyDB import — always available (stub if not connected, real client if connected)
|
|
||||||
import anydb from "@/lib/anydb";
|
|
||||||
|
|
||||||
// Seed data fallback — pre-populated per category so the site looks functional
|
|
||||||
// even before AnyDB is connected. Replace with real data by connecting AnyDB.
|
|
||||||
const SEED_DATA: Record<string, any[]> = {
|
|
||||||
users: [
|
|
||||||
{ _id: "u1", name: "Alice Johnson", email: "alice@example.com", plan: "pro", role: "admin", avatar_url: "", created_at: "2025-01-15T10:00:00Z" },
|
|
||||||
{ _id: "u2", name: "Bob Smith", email: "bob@example.com", plan: "free", role: "user", avatar_url: "", created_at: "2025-02-20T14:30:00Z" },
|
|
||||||
{ _id: "u3", name: "Carol Williams", email: "carol@example.com", plan: "enterprise", role: "user", avatar_url: "", created_at: "2025-03-10T09:15:00Z" },
|
|
||||||
],
|
|
||||||
products: [
|
|
||||||
{ _id: "p1", name: "Classic Leather Jacket", price_cents: 18900, category: "clothing", stock: 45, rating: 4.8, created_at: "2025-01-10T00:00:00Z" },
|
|
||||||
{ _id: "p2", name: "Premium Denim Jeans", price_cents: 8900, category: "clothing", stock: 120, rating: 4.6, created_at: "2025-01-15T00:00:00Z" },
|
|
||||||
{ _id: "p3", name: "Cashmere Sweater", price_cents: 12900, category: "clothing", stock: 30, rating: 4.9, created_at: "2025-02-01T00:00:00Z" },
|
|
||||||
{ _id: "p4", name: "Silk Blend Shirt", price_cents: 6900, category: "clothing", stock: 85, rating: 4.5, created_at: "2025-02-15T00:00:00Z" },
|
|
||||||
],
|
|
||||||
orders: [
|
|
||||||
{ _id: "o1", customer: "Alice Johnson", total_cents: 28900, status: "completed", created_at: "2025-03-01T10:00:00Z" },
|
|
||||||
{ _id: "o2", customer: "Bob Smith", total_cents: 8900, status: "processing", created_at: "2025-03-05T14:00:00Z" },
|
|
||||||
{ _id: "o3", customer: "Carol Williams", total_cents: 45800, status: "completed", created_at: "2025-03-08T09:00:00Z" },
|
|
||||||
{ _id: "o4", customer: "David Lee", total_cents: 12900, status: "pending", created_at: "2025-03-10T16:00:00Z" },
|
|
||||||
{ _id: "o5", customer: "Eve Chen", total_cents: 6900, status: "cancelled", created_at: "2025-03-12T11:00:00Z" },
|
|
||||||
],
|
|
||||||
metrics: [
|
|
||||||
{ _id: "m1", name: "revenue", value: 48500, unit: "dollars", recorded_at: "2025-03-01T00:00:00Z" },
|
|
||||||
{ _id: "m2", name: "users", value: 2340, unit: "count", recorded_at: "2025-03-01T00:00:00Z" },
|
|
||||||
{ _id: "m3", name: "conversion", value: 3.2, unit: "percent", recorded_at: "2025-03-01T00:00:00Z" },
|
|
||||||
{ _id: "m4", name: "uptime", value: 99.99, unit: "percent", recorded_at: "2025-03-01T00:00:00Z" },
|
|
||||||
],
|
|
||||||
posts: [
|
|
||||||
{ _id: "post1", title: "Getting Started with Our Platform", slug: "getting-started", excerpt: "Learn how to set up your account and start building.", category: "Tutorial", author_id: "u1", status: "published", published_at: "2025-02-15T10:00:00Z" },
|
|
||||||
{ _id: "post2", title: "Advanced Analytics Deep Dive", slug: "analytics-deep-dive", excerpt: "Explore powerful analytics features for data-driven decisions.", category: "Technology", author_id: "u1", status: "published", published_at: "2025-03-01T10:00:00Z" },
|
|
||||||
{ _id: "post3", title: "Best Practices for Team Collaboration", slug: "team-collaboration", excerpt: "Tips for getting the most out of collaborative features.", category: "Business", author_id: "u2", status: "published", published_at: "2025-03-10T10:00:00Z" },
|
|
||||||
],
|
|
||||||
subscribers: [],
|
|
||||||
contact_messages: [],
|
|
||||||
};
|
|
||||||
|
|
||||||
// Seed-data collection simulator
|
|
||||||
function createSeedCollection(name: string) {
|
|
||||||
const data = SEED_DATA[name] || [];
|
|
||||||
return {
|
|
||||||
find: async (filter: any = {}, opts: any = {}) => {
|
|
||||||
let results = [...data];
|
|
||||||
// Basic filter
|
|
||||||
if (Object.keys(filter).length > 0) {
|
|
||||||
results = results.filter(item =>
|
|
||||||
Object.entries(filter).every(([k, v]) => item[k] === v)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
// Sort
|
|
||||||
if (opts.sort) {
|
|
||||||
const [key, dir] = Object.entries(opts.sort)[0] as [string, number];
|
|
||||||
results.sort((a, b) => dir === 1 ? (a[key] > b[key] ? 1 : -1) : (a[key] < b[key] ? 1 : -1));
|
|
||||||
}
|
|
||||||
const total = results.length;
|
|
||||||
// Pagination
|
|
||||||
if (opts.skip) results = results.slice(opts.skip);
|
|
||||||
if (opts.limit) results = results.slice(0, opts.limit);
|
|
||||||
return { data: results, total, page: 1, limit: opts.limit || 50, pages: 1 };
|
|
||||||
},
|
|
||||||
insertOne: async (doc: any) => {
|
|
||||||
const newDoc = { ...doc, _id: `seed_${Date.now()}` };
|
|
||||||
data.push(newDoc);
|
|
||||||
return newDoc;
|
|
||||||
},
|
|
||||||
updateOne: async (filter: any, update: any) => {
|
|
||||||
const idx = data.findIndex(d => Object.entries(filter).every(([k, v]) => d[k] === v));
|
|
||||||
if (idx >= 0) Object.assign(data[idx], update);
|
|
||||||
return { modifiedCount: idx >= 0 ? 1 : 0 };
|
|
||||||
},
|
|
||||||
deleteOne: async (filter: any) => {
|
|
||||||
const idx = data.findIndex(d => Object.entries(filter).every(([k, v]) => d[k] === v));
|
|
||||||
if (idx >= 0) data.splice(idx, 1);
|
|
||||||
return { deletedCount: idx >= 0 ? 1 : 0 };
|
|
||||||
},
|
|
||||||
aggregate: async (pipeline: any[]) => {
|
|
||||||
// Basic count aggregation support
|
|
||||||
if (pipeline.some(s => s.$count)) return [{ value: data.length }];
|
|
||||||
if (pipeline.some(s => s.$group)) {
|
|
||||||
const group = pipeline.find(s => s.$group);
|
|
||||||
if (group.$group._id === null) {
|
|
||||||
const sumField = Object.entries(group.$group).find(([k, v]: any) => v?.$sum);
|
|
||||||
if (sumField) {
|
|
||||||
const field = (sumField[1] as any).$sum?.replace('$', '');
|
|
||||||
const total = data.reduce((acc, d) => acc + (d[field] || 0), 0);
|
|
||||||
return [{ value: total }];
|
|
||||||
}
|
|
||||||
return [{ value: data.length }];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return data;
|
|
||||||
},
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
// Unified collection getter — tries AnyDB first, falls back to seed data
|
|
||||||
async function getCollection(name: string) {
|
|
||||||
try {
|
|
||||||
const col = await anydb.collection(name);
|
|
||||||
// Test if it's a real connection (stub returns empty instantly)
|
|
||||||
const test = await col.find({}, { limit: 1 });
|
|
||||||
if (test && test.data !== undefined) return col;
|
|
||||||
} catch {
|
|
||||||
// AnyDB not connected — use seed data
|
|
||||||
}
|
|
||||||
return createSeedCollection(name);
|
|
||||||
}
|
|
||||||
|
|
||||||
interface CollectionOptions {
|
|
||||||
filter?: Record<string, any>;
|
|
||||||
sort?: Record<string, 1 | -1>;
|
|
||||||
limit?: number;
|
|
||||||
skip?: number;
|
|
||||||
autoFetch?: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface CollectionResult<T = any> {
|
|
||||||
data: T[];
|
|
||||||
total: number;
|
|
||||||
loading: boolean;
|
|
||||||
error: string | null;
|
|
||||||
refetch: () => Promise<void>;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Fetch data from an AnyDB collection with loading/error states.
|
|
||||||
*
|
|
||||||
* Usage:
|
|
||||||
* const { data: products, loading, refetch } = useCollection('products', { limit: 20 })
|
|
||||||
* const { data: orders } = useCollection('orders', { filter: { status: 'active' }, sort: { created_at: -1 } })
|
|
||||||
*/
|
|
||||||
export function useCollection<T = any>(
|
|
||||||
collectionName: string,
|
|
||||||
options: CollectionOptions = {}
|
|
||||||
): CollectionResult<T> {
|
|
||||||
const { filter = {}, sort, limit = 50, skip = 0, autoFetch = true } = options;
|
|
||||||
const [data, setData] = useState<T[]>([]);
|
|
||||||
const [total, setTotal] = useState(0);
|
|
||||||
const [loading, setLoading] = useState(true);
|
|
||||||
const [error, setError] = useState<string | null>(null);
|
|
||||||
|
|
||||||
const fetchData = useCallback(async () => {
|
|
||||||
setLoading(true);
|
|
||||||
setError(null);
|
|
||||||
try {
|
|
||||||
const col = await getCollection(collectionName);
|
|
||||||
const result = await col.find(filter, { sort, limit, skip });
|
|
||||||
setData((result.data as T[]) || []);
|
|
||||||
setTotal(result.total || 0);
|
|
||||||
} catch (err: any) {
|
|
||||||
setError(err.message || "Failed to fetch data");
|
|
||||||
console.error(`[useCollection] ${collectionName}:`, err);
|
|
||||||
} finally {
|
|
||||||
setLoading(false);
|
|
||||||
}
|
|
||||||
}, [collectionName, JSON.stringify(filter), JSON.stringify(sort), limit, skip]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (autoFetch) fetchData();
|
|
||||||
}, [fetchData, autoFetch]);
|
|
||||||
|
|
||||||
return { data, total, loading, error, refetch: fetchData };
|
|
||||||
}
|
|
||||||
|
|
||||||
interface MutationResult {
|
|
||||||
mutate: (document: Record<string, any>) => Promise<any>;
|
|
||||||
update: (filter: Record<string, any>, updates: Record<string, any>) => Promise<any>;
|
|
||||||
remove: (filter: Record<string, any>) => Promise<any>;
|
|
||||||
loading: boolean;
|
|
||||||
error: string | null;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Write to an AnyDB collection (insert, update, delete).
|
|
||||||
*
|
|
||||||
* Usage:
|
|
||||||
* const { mutate, loading } = useMutation('contact_messages')
|
|
||||||
* await mutate({ name, email, message }) // insertOne
|
|
||||||
* await update({ _id: id }, { status: 'read' }) // updateOne
|
|
||||||
* await remove({ _id: id }) // deleteOne
|
|
||||||
*/
|
|
||||||
export function useMutation(collectionName: string): MutationResult {
|
|
||||||
const [loading, setLoading] = useState(false);
|
|
||||||
const [error, setError] = useState<string | null>(null);
|
|
||||||
|
|
||||||
const mutate = useCallback(async (document: Record<string, any>) => {
|
|
||||||
setLoading(true);
|
|
||||||
setError(null);
|
|
||||||
try {
|
|
||||||
const col = await getCollection(collectionName);
|
|
||||||
const result = await col.insertOne({
|
|
||||||
...document,
|
|
||||||
created_at: new Date().toISOString(),
|
|
||||||
});
|
|
||||||
return result;
|
|
||||||
} catch (err: any) {
|
|
||||||
setError(err.message || "Failed to save");
|
|
||||||
throw err;
|
|
||||||
} finally {
|
|
||||||
setLoading(false);
|
|
||||||
}
|
|
||||||
}, [collectionName]);
|
|
||||||
|
|
||||||
const update = useCallback(async (filter: Record<string, any>, updates: Record<string, any>) => {
|
|
||||||
setLoading(true);
|
|
||||||
setError(null);
|
|
||||||
try {
|
|
||||||
const col = await getCollection(collectionName);
|
|
||||||
const result = await col.updateOne(filter, updates);
|
|
||||||
return result;
|
|
||||||
} catch (err: any) {
|
|
||||||
setError(err.message || "Failed to update");
|
|
||||||
throw err;
|
|
||||||
} finally {
|
|
||||||
setLoading(false);
|
|
||||||
}
|
|
||||||
}, [collectionName]);
|
|
||||||
|
|
||||||
const remove = useCallback(async (filter: Record<string, any>) => {
|
|
||||||
setLoading(true);
|
|
||||||
setError(null);
|
|
||||||
try {
|
|
||||||
const col = await getCollection(collectionName);
|
|
||||||
const result = await col.deleteOne(filter);
|
|
||||||
return result;
|
|
||||||
} catch (err: any) {
|
|
||||||
setError(err.message || "Failed to delete");
|
|
||||||
throw err;
|
|
||||||
} finally {
|
|
||||||
setLoading(false);
|
|
||||||
}
|
|
||||||
}, [collectionName]);
|
|
||||||
|
|
||||||
return { mutate, update, remove, loading, error };
|
|
||||||
}
|
|
||||||
|
|
||||||
interface AggregateResult<T = any> {
|
|
||||||
data: T[];
|
|
||||||
loading: boolean;
|
|
||||||
error: string | null;
|
|
||||||
refetch: () => Promise<void>;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Run an aggregation pipeline on an AnyDB collection.
|
|
||||||
*
|
|
||||||
* Usage:
|
|
||||||
* const { data: stats } = useAggregate('orders', [
|
|
||||||
* { $group: { _id: '$status', count: { $sum: 1 }, total: { $sum: '$total_cents' } } }
|
|
||||||
* ])
|
|
||||||
*/
|
|
||||||
export function useAggregate<T = any>(
|
|
||||||
collectionName: string,
|
|
||||||
pipeline: Record<string, any>[]
|
|
||||||
): AggregateResult<T> {
|
|
||||||
const [data, setData] = useState<T[]>([]);
|
|
||||||
const [loading, setLoading] = useState(true);
|
|
||||||
const [error, setError] = useState<string | null>(null);
|
|
||||||
|
|
||||||
const fetchData = useCallback(async () => {
|
|
||||||
setLoading(true);
|
|
||||||
setError(null);
|
|
||||||
try {
|
|
||||||
const col = await getCollection(collectionName);
|
|
||||||
const result = await col.aggregate(pipeline);
|
|
||||||
setData((result as T[]) || []);
|
|
||||||
} catch (err: any) {
|
|
||||||
setError(err.message || "Aggregation failed");
|
|
||||||
} finally {
|
|
||||||
setLoading(false);
|
|
||||||
}
|
|
||||||
}, [collectionName, JSON.stringify(pipeline)]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
fetchData();
|
|
||||||
}, [fetchData]);
|
|
||||||
|
|
||||||
return { data, loading, error, refetch: fetchData };
|
|
||||||
}
|
|
||||||
@@ -1,19 +0,0 @@
|
|||||||
import * as React from "react";
|
|
||||||
|
|
||||||
const MOBILE_BREAKPOINT = 768;
|
|
||||||
|
|
||||||
export function useIsMobile() {
|
|
||||||
const [isMobile, setIsMobile] = React.useState<boolean | undefined>(undefined);
|
|
||||||
|
|
||||||
React.useEffect(() => {
|
|
||||||
const mql = window.matchMedia(`(max-width: ${MOBILE_BREAKPOINT - 1}px)`);
|
|
||||||
const onChange = () => {
|
|
||||||
setIsMobile(window.innerWidth < MOBILE_BREAKPOINT);
|
|
||||||
};
|
|
||||||
mql.addEventListener("change", onChange);
|
|
||||||
setIsMobile(window.innerWidth < MOBILE_BREAKPOINT);
|
|
||||||
return () => mql.removeEventListener("change", onChange);
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
return !!isMobile;
|
|
||||||
}
|
|
||||||
@@ -1,161 +0,0 @@
|
|||||||
// ---------------------------------------------------------------------------
|
|
||||||
// useChannel + usePresence + useHistory — pre-built OddSockets realtime hooks
|
|
||||||
// ---------------------------------------------------------------------------
|
|
||||||
// PRE-INSTALLED in every React project (realtime leg of the tyga backend
|
|
||||||
// bundle). Same stub->real-client-on-connect pattern as use-anydb/use-tygarun:
|
|
||||||
//
|
|
||||||
// import { useChannel, usePresence, useHistory } from "@/hooks/use-oddsockets"
|
|
||||||
// const { messages, publish, connected } = useChannel("room:lobby")
|
|
||||||
// const { members, updateState } = usePresence("room:lobby")
|
|
||||||
// const { history, loading } = useHistory("room:lobby", { count: 50 })
|
|
||||||
//
|
|
||||||
// Until OddSockets is connected, hooks run against the local demo bus in
|
|
||||||
// lib/oddsockets.ts so realtime UIs render live. `connected` reflects
|
|
||||||
// isConfigured() and `error` surfaces a broken real connect — the demo state
|
|
||||||
// never silently masks a bad key.
|
|
||||||
// ---------------------------------------------------------------------------
|
|
||||||
|
|
||||||
import { useState, useEffect, useCallback, useRef } from "react";
|
|
||||||
|
|
||||||
import oddsockets, { isConfigured, type ChannelMessage, type PresenceMember } from "@/lib/oddsockets";
|
|
||||||
|
|
||||||
interface ChannelResult {
|
|
||||||
messages: ChannelMessage[];
|
|
||||||
publish: (data: any) => Promise<void>;
|
|
||||||
connected: boolean;
|
|
||||||
error: string | null;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Subscribe to a realtime channel. Accumulates incoming messages and exposes a
|
|
||||||
* publish() for sending. `connected` is true only when OddSockets is really
|
|
||||||
* connected (false in demo mode).
|
|
||||||
*/
|
|
||||||
export function useChannel(channelName: string): ChannelResult {
|
|
||||||
const [messages, setMessages] = useState<ChannelMessage[]>([]);
|
|
||||||
const [error, setError] = useState<string | null>(null);
|
|
||||||
const channelRef = useRef(oddsockets.channel(channelName));
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
const ch = oddsockets.channel(channelName);
|
|
||||||
channelRef.current = ch;
|
|
||||||
let off = () => {};
|
|
||||||
try {
|
|
||||||
off = ch.subscribe((msg) => setMessages((prev) => [...prev, msg]));
|
|
||||||
} catch (err: any) {
|
|
||||||
setError(err.message || "Failed to subscribe");
|
|
||||||
console.error(`[useChannel] ${channelName}:`, err);
|
|
||||||
}
|
|
||||||
return () => {
|
|
||||||
try { off(); } catch { /* noop */ }
|
|
||||||
};
|
|
||||||
}, [channelName]);
|
|
||||||
|
|
||||||
const publish = useCallback(async (data: any) => {
|
|
||||||
setError(null);
|
|
||||||
try {
|
|
||||||
await channelRef.current.publish(data);
|
|
||||||
} catch (err: any) {
|
|
||||||
setError(err.message || "Failed to publish");
|
|
||||||
throw err;
|
|
||||||
}
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
return { messages, publish, connected: isConfigured(), error };
|
|
||||||
}
|
|
||||||
|
|
||||||
interface PresenceResult {
|
|
||||||
members: PresenceMember[];
|
|
||||||
updateState: (state: Record<string, any>) => Promise<void>;
|
|
||||||
connected: boolean;
|
|
||||||
error: string | null;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Track presence on a channel (who's here + their state), with updateState()
|
|
||||||
* to publish this client's own state (e.g. { typing: true }).
|
|
||||||
*/
|
|
||||||
export function usePresence(channelName: string): PresenceResult {
|
|
||||||
const [members, setMembers] = useState<PresenceMember[]>([]);
|
|
||||||
const [error, setError] = useState<string | null>(null);
|
|
||||||
const channelRef = useRef(oddsockets.channel(channelName));
|
|
||||||
|
|
||||||
const refresh = useCallback(async () => {
|
|
||||||
try {
|
|
||||||
const list = await channelRef.current.getPresence();
|
|
||||||
setMembers(list || []);
|
|
||||||
} catch (err: any) {
|
|
||||||
setError(err.message || "Failed to read presence");
|
|
||||||
}
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
const ch = oddsockets.channel(channelName);
|
|
||||||
channelRef.current = ch;
|
|
||||||
let off = () => {};
|
|
||||||
try {
|
|
||||||
// re-read presence on every channel message (join/leave/state changes)
|
|
||||||
off = ch.subscribe(() => { refresh(); });
|
|
||||||
} catch (err: any) {
|
|
||||||
setError(err.message || "Failed to subscribe to presence");
|
|
||||||
}
|
|
||||||
refresh();
|
|
||||||
return () => {
|
|
||||||
try { off(); } catch { /* noop */ }
|
|
||||||
};
|
|
||||||
}, [channelName, refresh]);
|
|
||||||
|
|
||||||
const updateState = useCallback(async (state: Record<string, any>) => {
|
|
||||||
setError(null);
|
|
||||||
try {
|
|
||||||
await channelRef.current.updateState(state);
|
|
||||||
await refresh();
|
|
||||||
} catch (err: any) {
|
|
||||||
setError(err.message || "Failed to update state");
|
|
||||||
throw err;
|
|
||||||
}
|
|
||||||
}, [refresh]);
|
|
||||||
|
|
||||||
return { members, updateState, connected: isConfigured(), error };
|
|
||||||
}
|
|
||||||
|
|
||||||
interface HistoryResult {
|
|
||||||
history: ChannelMessage[];
|
|
||||||
loading: boolean;
|
|
||||||
error: string | null;
|
|
||||||
refetch: () => Promise<void>;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Fetch recent messages for a channel (backlog), with loading/error states.
|
|
||||||
*/
|
|
||||||
export function useHistory(
|
|
||||||
channelName: string,
|
|
||||||
options: { count?: number; start?: string; end?: string } = {}
|
|
||||||
): HistoryResult {
|
|
||||||
const { count = 50, start, end } = options;
|
|
||||||
const [history, setHistory] = useState<ChannelMessage[]>([]);
|
|
||||||
const [loading, setLoading] = useState(true);
|
|
||||||
const [error, setError] = useState<string | null>(null);
|
|
||||||
|
|
||||||
const fetchHistory = useCallback(async () => {
|
|
||||||
setLoading(true);
|
|
||||||
setError(null);
|
|
||||||
try {
|
|
||||||
const ch = oddsockets.channel(channelName);
|
|
||||||
const result = await ch.getHistory({ count, start, end });
|
|
||||||
setHistory(result || []);
|
|
||||||
} catch (err: any) {
|
|
||||||
setError(err.message || "Failed to fetch history");
|
|
||||||
console.error(`[useHistory] ${channelName}:`, err);
|
|
||||||
} finally {
|
|
||||||
setLoading(false);
|
|
||||||
}
|
|
||||||
}, [channelName, count, start, end]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
fetchHistory();
|
|
||||||
}, [fetchHistory]);
|
|
||||||
|
|
||||||
return { history, loading, error, refetch: fetchHistory };
|
|
||||||
}
|
|
||||||
@@ -1,186 +0,0 @@
|
|||||||
import * as React from "react";
|
|
||||||
|
|
||||||
import type { ToastActionElement, ToastProps } from "@/components/ui/toast";
|
|
||||||
|
|
||||||
const TOAST_LIMIT = 1;
|
|
||||||
const TOAST_REMOVE_DELAY = 1000000;
|
|
||||||
|
|
||||||
type ToasterToast = ToastProps & {
|
|
||||||
id: string;
|
|
||||||
title?: React.ReactNode;
|
|
||||||
description?: React.ReactNode;
|
|
||||||
action?: ToastActionElement;
|
|
||||||
};
|
|
||||||
|
|
||||||
const actionTypes = {
|
|
||||||
ADD_TOAST: "ADD_TOAST",
|
|
||||||
UPDATE_TOAST: "UPDATE_TOAST",
|
|
||||||
DISMISS_TOAST: "DISMISS_TOAST",
|
|
||||||
REMOVE_TOAST: "REMOVE_TOAST",
|
|
||||||
} as const;
|
|
||||||
|
|
||||||
let count = 0;
|
|
||||||
|
|
||||||
function genId() {
|
|
||||||
count = (count + 1) % Number.MAX_SAFE_INTEGER;
|
|
||||||
return count.toString();
|
|
||||||
}
|
|
||||||
|
|
||||||
type ActionType = typeof actionTypes;
|
|
||||||
|
|
||||||
type Action =
|
|
||||||
| {
|
|
||||||
type: ActionType["ADD_TOAST"];
|
|
||||||
toast: ToasterToast;
|
|
||||||
}
|
|
||||||
| {
|
|
||||||
type: ActionType["UPDATE_TOAST"];
|
|
||||||
toast: Partial<ToasterToast>;
|
|
||||||
}
|
|
||||||
| {
|
|
||||||
type: ActionType["DISMISS_TOAST"];
|
|
||||||
toastId?: ToasterToast["id"];
|
|
||||||
}
|
|
||||||
| {
|
|
||||||
type: ActionType["REMOVE_TOAST"];
|
|
||||||
toastId?: ToasterToast["id"];
|
|
||||||
};
|
|
||||||
|
|
||||||
interface State {
|
|
||||||
toasts: ToasterToast[];
|
|
||||||
}
|
|
||||||
|
|
||||||
const toastTimeouts = new Map<string, ReturnType<typeof setTimeout>>();
|
|
||||||
|
|
||||||
const addToRemoveQueue = (toastId: string) => {
|
|
||||||
if (toastTimeouts.has(toastId)) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const timeout = setTimeout(() => {
|
|
||||||
toastTimeouts.delete(toastId);
|
|
||||||
dispatch({
|
|
||||||
type: "REMOVE_TOAST",
|
|
||||||
toastId: toastId,
|
|
||||||
});
|
|
||||||
}, TOAST_REMOVE_DELAY);
|
|
||||||
|
|
||||||
toastTimeouts.set(toastId, timeout);
|
|
||||||
};
|
|
||||||
|
|
||||||
export const reducer = (state: State, action: Action): State => {
|
|
||||||
switch (action.type) {
|
|
||||||
case "ADD_TOAST":
|
|
||||||
return {
|
|
||||||
...state,
|
|
||||||
toasts: [action.toast, ...state.toasts].slice(0, TOAST_LIMIT),
|
|
||||||
};
|
|
||||||
|
|
||||||
case "UPDATE_TOAST":
|
|
||||||
return {
|
|
||||||
...state,
|
|
||||||
toasts: state.toasts.map((t) => (t.id === action.toast.id ? { ...t, ...action.toast } : t)),
|
|
||||||
};
|
|
||||||
|
|
||||||
case "DISMISS_TOAST": {
|
|
||||||
const { toastId } = action;
|
|
||||||
|
|
||||||
// ! Side effects ! - This could be extracted into a dismissToast() action,
|
|
||||||
// but I'll keep it here for simplicity
|
|
||||||
if (toastId) {
|
|
||||||
addToRemoveQueue(toastId);
|
|
||||||
} else {
|
|
||||||
state.toasts.forEach((toast) => {
|
|
||||||
addToRemoveQueue(toast.id);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
...state,
|
|
||||||
toasts: state.toasts.map((t) =>
|
|
||||||
t.id === toastId || toastId === undefined
|
|
||||||
? {
|
|
||||||
...t,
|
|
||||||
open: false,
|
|
||||||
}
|
|
||||||
: t,
|
|
||||||
),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
case "REMOVE_TOAST":
|
|
||||||
if (action.toastId === undefined) {
|
|
||||||
return {
|
|
||||||
...state,
|
|
||||||
toasts: [],
|
|
||||||
};
|
|
||||||
}
|
|
||||||
return {
|
|
||||||
...state,
|
|
||||||
toasts: state.toasts.filter((t) => t.id !== action.toastId),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const listeners: Array<(state: State) => void> = [];
|
|
||||||
|
|
||||||
let memoryState: State = { toasts: [] };
|
|
||||||
|
|
||||||
function dispatch(action: Action) {
|
|
||||||
memoryState = reducer(memoryState, action);
|
|
||||||
listeners.forEach((listener) => {
|
|
||||||
listener(memoryState);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
type Toast = Omit<ToasterToast, "id">;
|
|
||||||
|
|
||||||
function toast({ ...props }: Toast) {
|
|
||||||
const id = genId();
|
|
||||||
|
|
||||||
const update = (props: ToasterToast) =>
|
|
||||||
dispatch({
|
|
||||||
type: "UPDATE_TOAST",
|
|
||||||
toast: { ...props, id },
|
|
||||||
});
|
|
||||||
const dismiss = () => dispatch({ type: "DISMISS_TOAST", toastId: id });
|
|
||||||
|
|
||||||
dispatch({
|
|
||||||
type: "ADD_TOAST",
|
|
||||||
toast: {
|
|
||||||
...props,
|
|
||||||
id,
|
|
||||||
open: true,
|
|
||||||
onOpenChange: (open) => {
|
|
||||||
if (!open) dismiss();
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
return {
|
|
||||||
id: id,
|
|
||||||
dismiss,
|
|
||||||
update,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
function useToast() {
|
|
||||||
const [state, setState] = React.useState<State>(memoryState);
|
|
||||||
|
|
||||||
React.useEffect(() => {
|
|
||||||
listeners.push(setState);
|
|
||||||
return () => {
|
|
||||||
const index = listeners.indexOf(setState);
|
|
||||||
if (index > -1) {
|
|
||||||
listeners.splice(index, 1);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}, [state]);
|
|
||||||
|
|
||||||
return {
|
|
||||||
...state,
|
|
||||||
toast,
|
|
||||||
dismiss: (toastId?: string) => dispatch({ type: "DISMISS_TOAST", toastId }),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
export { useToast, toast };
|
|
||||||
@@ -1,330 +0,0 @@
|
|||||||
// ---------------------------------------------------------------------------
|
|
||||||
// useSaas + useSaasMutation — ready-made SaaS API hooks (tyga.run)
|
|
||||||
// ---------------------------------------------------------------------------
|
|
||||||
// PRE-INSTALLED in every React project. Works for ANY tyga.run SaaS module —
|
|
||||||
// scheduling (bookings/eventtypes/availability), crm, ecommerce, billing,
|
|
||||||
// notifications, support, surveys... CodeWriterAgent just imports:
|
|
||||||
//
|
|
||||||
// import { useSaas, useSaasMutation } from "@/hooks/use-tygarun"
|
|
||||||
// const { data: bookings, loading } = useSaas("scheduling", "/bookings", { query: { status: "confirmed" } })
|
|
||||||
// const { mutate: book, loading: booking } = useSaasMutation("scheduling", "/bookings")
|
|
||||||
// await book({ eventTypeId, customer: { name, email }, scheduledAt })
|
|
||||||
//
|
|
||||||
// Falls back to per-module seed data until tyga.run is connected, so the
|
|
||||||
// generated site looks alive out of the box (same pattern as use-anydb.ts).
|
|
||||||
// ---------------------------------------------------------------------------
|
|
||||||
|
|
||||||
import { useState, useEffect, useCallback } from "react";
|
|
||||||
import tygarun, { isConfigured, auth } from "@/lib/tygarun";
|
|
||||||
|
|
||||||
// Seed data keyed by "module:endpoint-prefix" — extend freely.
|
|
||||||
const SEED_DATA: Record<string, any> = {
|
|
||||||
"scheduling:/bookings": {
|
|
||||||
bookings: [
|
|
||||||
{ id: "bk1", eventTypeName: "30 Minute Consultation", customer: { name: "Alice Johnson", email: "alice@example.com" }, status: "confirmed", scheduledAt: "2026-07-10T10:00:00Z", duration: 30 },
|
|
||||||
{ id: "bk2", eventTypeName: "Discovery Call", customer: { name: "Bob Smith", email: "bob@example.com" }, status: "pending", scheduledAt: "2026-07-11T14:30:00Z", duration: 15 },
|
|
||||||
{ id: "bk3", eventTypeName: "Strategy Session", customer: { name: "Carol Williams", email: "carol@example.com" }, status: "completed", scheduledAt: "2026-07-02T09:00:00Z", duration: 60 },
|
|
||||||
],
|
|
||||||
pagination: { page: 1, limit: 20, total: 3, totalPages: 1 },
|
|
||||||
},
|
|
||||||
"scheduling:/eventtypes": {
|
|
||||||
eventTypes: [
|
|
||||||
{ id: "evt1", name: "30 Minute Consultation", slug: "30min-consult", duration: 30, isActive: true, isPublic: true, description: "Quick intro call to discuss your needs." },
|
|
||||||
{ id: "evt2", name: "Strategy Session", slug: "strategy-session", duration: 60, isActive: true, isPublic: true, description: "Deep-dive planning session." },
|
|
||||||
],
|
|
||||||
},
|
|
||||||
"scheduling:/availability": {
|
|
||||||
availability: {
|
|
||||||
timezone: "Europe/London",
|
|
||||||
workingHours: [
|
|
||||||
{ day: "monday", start: "09:00", end: "17:00" },
|
|
||||||
{ day: "tuesday", start: "09:00", end: "17:00" },
|
|
||||||
{ day: "wednesday", start: "09:00", end: "17:00" },
|
|
||||||
{ day: "thursday", start: "09:00", end: "17:00" },
|
|
||||||
{ day: "friday", start: "09:00", end: "15:00" },
|
|
||||||
],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
"crm:/contacts": {
|
|
||||||
contacts: [
|
|
||||||
{ id: "c1", name: "Alice Johnson", email: "alice@example.com", company: "Acme Corp", stage: "customer" },
|
|
||||||
{ id: "c2", name: "Bob Smith", email: "bob@example.com", company: "Globex", stage: "lead" },
|
|
||||||
],
|
|
||||||
},
|
|
||||||
"ecommerce:/products": {
|
|
||||||
products: [
|
|
||||||
{ id: "p1", name: "Classic Leather Jacket", priceCents: 18900, stock: 45, rating: 4.8 },
|
|
||||||
{ id: "p2", name: "Premium Denim Jeans", priceCents: 8900, stock: 120, rating: 4.6 },
|
|
||||||
],
|
|
||||||
},
|
|
||||||
"ecommerce:/catalog": {
|
|
||||||
products: [
|
|
||||||
{ id: "p1", name: "Classic Leather Jacket", priceCents: 18900, stock: 45, rating: 4.8, image: "" },
|
|
||||||
{ id: "p2", name: "Premium Denim Jeans", priceCents: 8900, stock: 120, rating: 4.6, image: "" },
|
|
||||||
{ id: "p3", name: "Merino Wool Sweater", priceCents: 12900, stock: 60, rating: 4.9, image: "" },
|
|
||||||
],
|
|
||||||
},
|
|
||||||
"event-ticketing:/availability": {
|
|
||||||
events: [
|
|
||||||
{ id: "ev1", name: "Live at The Fillmore", date: "2026-08-14T20:00:00Z", venue: "The Fillmore", tiers: [{ id: "t1", name: "General", priceCents: 3500, remaining: 220 }, { id: "t2", name: "VIP", priceCents: 9500, remaining: 30 }] },
|
|
||||||
{ id: "ev2", name: "Summer Sessions", date: "2026-09-02T19:30:00Z", venue: "Open Air Park", tiers: [{ id: "t3", name: "Early Bird", priceCents: 2500, remaining: 0 }, { id: "t4", name: "Standard", priceCents: 4000, remaining: 150 }] },
|
|
||||||
],
|
|
||||||
},
|
|
||||||
"surveys:/": {
|
|
||||||
surveys: [
|
|
||||||
{ id: "sv1", title: "Dark mode", votes: 128, status: "planned", description: "Full dark theme across the app." },
|
|
||||||
{ id: "sv2", title: "Mobile app", votes: 342, status: "in-progress", description: "Native iOS + Android apps." },
|
|
||||||
{ id: "sv3", title: "API access", votes: 87, status: "under-review", description: "Public REST + webhooks." },
|
|
||||||
],
|
|
||||||
},
|
|
||||||
"community:/comments": {
|
|
||||||
comments: [
|
|
||||||
{ id: "cm1", author: "Jamie R.", body: "Would love to see this shipped!", createdAt: "2026-07-01T12:00:00Z" },
|
|
||||||
{ id: "cm2", author: "Priya S.", body: "+1, this blocks our team.", createdAt: "2026-07-03T09:20:00Z" },
|
|
||||||
],
|
|
||||||
},
|
|
||||||
"file-management:/list": {
|
|
||||||
files: [
|
|
||||||
{ id: "f1", name: "gallery-01.jpg", url: "", contentType: "image/jpeg", size: 245000 },
|
|
||||||
{ id: "f2", name: "gallery-02.jpg", url: "", contentType: "image/jpeg", size: 312000 },
|
|
||||||
{ id: "f3", name: "gallery-03.jpg", url: "", contentType: "image/jpeg", size: 198000 },
|
|
||||||
],
|
|
||||||
},
|
|
||||||
"billing:/plans": {
|
|
||||||
plans: [
|
|
||||||
{ id: "starter", name: "Starter", priceCents: 0, interval: "month", features: ["1 project", "Community support"] },
|
|
||||||
{ id: "pro", name: "Pro", priceCents: 2900, interval: "month", features: ["Unlimited projects", "Priority support", "Custom domain"] },
|
|
||||||
{ id: "team", name: "Team", priceCents: 9900, interval: "month", features: ["Everything in Pro", "5 seats", "SSO"] },
|
|
||||||
],
|
|
||||||
},
|
|
||||||
"notifications:/": { notifications: [] },
|
|
||||||
"support:/tickets": { tickets: [] },
|
|
||||||
};
|
|
||||||
|
|
||||||
function seedFor(module: string, endpoint: string): any {
|
|
||||||
const path = endpoint.startsWith("/") ? endpoint : `/${endpoint}`;
|
|
||||||
// Longest matching prefix wins: "scheduling:/bookings/stats" → "scheduling:/bookings"
|
|
||||||
const keys = Object.keys(SEED_DATA)
|
|
||||||
.filter((k) => `${module}:${path}`.startsWith(k))
|
|
||||||
.sort((a, b) => b.length - a.length);
|
|
||||||
return keys.length ? SEED_DATA[keys[0]] : { data: [] };
|
|
||||||
}
|
|
||||||
|
|
||||||
// Extract the payload array/object from a tyga.run response regardless of
|
|
||||||
// envelope key ({ bookings: [...] }, { data: [...] }, plain array...)
|
|
||||||
export function unwrap<T = any>(res: any): T {
|
|
||||||
if (Array.isArray(res)) return res as T;
|
|
||||||
if (res?.data !== undefined) return res.data as T;
|
|
||||||
const arrKey = Object.keys(res || {}).find((k) => Array.isArray(res[k]));
|
|
||||||
return (arrKey ? res[arrKey] : res) as T;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface SaasOptions {
|
|
||||||
query?: Record<string, any>;
|
|
||||||
autoFetch?: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface SaasResult<T = any> {
|
|
||||||
data: T;
|
|
||||||
raw: any;
|
|
||||||
loading: boolean;
|
|
||||||
error: string | null;
|
|
||||||
connected: boolean;
|
|
||||||
refetch: () => Promise<void>;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Read from ANY tyga.run SaaS module with loading/error states + seed fallback.
|
|
||||||
*
|
|
||||||
* const { data: bookings } = useSaas("scheduling", "/bookings", { query: { status: "confirmed" } })
|
|
||||||
* const { data: eventTypes } = useSaas("scheduling", "/eventtypes")
|
|
||||||
* const { data: contacts } = useSaas("crm", "/contacts")
|
|
||||||
*/
|
|
||||||
export function useSaas<T = any>(
|
|
||||||
module: string,
|
|
||||||
endpoint = "/",
|
|
||||||
options: SaasOptions = {}
|
|
||||||
): SaasResult<T> {
|
|
||||||
const { query = {}, autoFetch = true } = options;
|
|
||||||
const [raw, setRaw] = useState<any>(null);
|
|
||||||
const [loading, setLoading] = useState(true);
|
|
||||||
const [error, setError] = useState<string | null>(null);
|
|
||||||
const connected = isConfigured();
|
|
||||||
|
|
||||||
const fetchData = useCallback(async () => {
|
|
||||||
setLoading(true);
|
|
||||||
setError(null);
|
|
||||||
try {
|
|
||||||
if (!connected) {
|
|
||||||
setRaw(seedFor(module, endpoint));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const res = await tygarun(module, endpoint, { query });
|
|
||||||
setRaw(res);
|
|
||||||
} catch (err: any) {
|
|
||||||
// Connected but call failed → show seed so the UI never breaks
|
|
||||||
setRaw(seedFor(module, endpoint));
|
|
||||||
setError(err.message || "Request failed");
|
|
||||||
console.error(`[useSaas] ${module}${endpoint}:`, err);
|
|
||||||
} finally {
|
|
||||||
setLoading(false);
|
|
||||||
}
|
|
||||||
}, [module, endpoint, JSON.stringify(query), connected]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (autoFetch) fetchData();
|
|
||||||
}, [fetchData, autoFetch]);
|
|
||||||
|
|
||||||
return { data: unwrap<T>(raw), raw, loading, error, connected, refetch: fetchData };
|
|
||||||
}
|
|
||||||
|
|
||||||
interface SaasMutationResult {
|
|
||||||
mutate: (body?: Record<string, any>, endpointOverride?: string) => Promise<any>;
|
|
||||||
update: (id: string, body: Record<string, any>) => Promise<any>;
|
|
||||||
remove: (id: string) => Promise<any>;
|
|
||||||
loading: boolean;
|
|
||||||
error: string | null;
|
|
||||||
connected: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Write to ANY tyga.run SaaS module.
|
|
||||||
*
|
|
||||||
* const { mutate: book } = useSaasMutation("scheduling", "/bookings")
|
|
||||||
* await book({ eventTypeId: "evt1", customer: { name, email }, scheduledAt })
|
|
||||||
*
|
|
||||||
* const { mutate: addContact, update, remove } = useSaasMutation("crm", "/contacts")
|
|
||||||
* await update("c1", { stage: "customer" }); await remove("c2")
|
|
||||||
*/
|
|
||||||
export function useSaasMutation(module: string, endpoint = "/"): SaasMutationResult {
|
|
||||||
const [loading, setLoading] = useState(false);
|
|
||||||
const [error, setError] = useState<string | null>(null);
|
|
||||||
const connected = isConfigured();
|
|
||||||
|
|
||||||
const run = useCallback(async (method: "POST" | "PUT" | "DELETE", path: string, body?: any) => {
|
|
||||||
setLoading(true);
|
|
||||||
setError(null);
|
|
||||||
try {
|
|
||||||
if (!connected) {
|
|
||||||
// Demo mode — pretend success so forms feel functional pre-connection
|
|
||||||
return { success: true, demo: true, id: `demo_${Date.now()}`, ...(body || {}) };
|
|
||||||
}
|
|
||||||
return await tygarun(module, path, { method, body });
|
|
||||||
} catch (err: any) {
|
|
||||||
setError(err.message || "Request failed");
|
|
||||||
throw err;
|
|
||||||
} finally {
|
|
||||||
setLoading(false);
|
|
||||||
}
|
|
||||||
}, [module, connected]);
|
|
||||||
|
|
||||||
const base = endpoint.replace(/\/+$/, "") || "";
|
|
||||||
return {
|
|
||||||
mutate: (body = {}, endpointOverride) => run("POST", endpointOverride || endpoint, body),
|
|
||||||
update: (id, body) => run("PUT", `${base}/${id}`, body),
|
|
||||||
remove: (id) => run("DELETE", `${base}/${id}`),
|
|
||||||
loading,
|
|
||||||
error,
|
|
||||||
connected,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
// ── useAuth — capability: login (tyga.run auth module) ──────────────────────
|
|
||||||
// Reactive wrapper over lib/tygarun's auth object. Gates protected UI (voting,
|
|
||||||
// booking, checkout) behind sign-in. Demo mode: any signin "succeeds" with a
|
|
||||||
// fake user so gated flows are explorable pre-connection.
|
|
||||||
interface AuthUser { email: string; firstName?: string; lastName?: string; [k: string]: any }
|
|
||||||
interface AuthResult {
|
|
||||||
user: AuthUser | null;
|
|
||||||
isSignedIn: boolean;
|
|
||||||
loading: boolean;
|
|
||||||
error: string | null;
|
|
||||||
connected: boolean;
|
|
||||||
signup: (data: { email: string; password: string; [k: string]: any }) => Promise<any>;
|
|
||||||
signin: (email: string, password: string, rememberMe?: boolean) => Promise<any>;
|
|
||||||
signout: () => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function useAuth(): AuthResult {
|
|
||||||
const connected = isConfigured();
|
|
||||||
const [user, setUser] = useState<AuthUser | null>(null);
|
|
||||||
const [loading, setLoading] = useState(false);
|
|
||||||
const [error, setError] = useState<string | null>(null);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
// Rehydrate: a stored token means a prior sign-in this session.
|
|
||||||
if (auth.isSignedIn() && !user) setUser({ email: "member@example.com" });
|
|
||||||
}, []); // eslint-disable-line react-hooks/exhaustive-deps
|
|
||||||
|
|
||||||
const wrap = useCallback(async (fn: () => Promise<any>, demoUser: AuthUser) => {
|
|
||||||
setLoading(true);
|
|
||||||
setError(null);
|
|
||||||
try {
|
|
||||||
if (!connected) { setUser(demoUser); return { success: true, demo: true, user: demoUser }; }
|
|
||||||
const res = await fn();
|
|
||||||
setUser(res?.user || { email: demoUser.email });
|
|
||||||
return res;
|
|
||||||
} catch (err: any) {
|
|
||||||
setError(err.message || "Authentication failed");
|
|
||||||
throw err;
|
|
||||||
} finally {
|
|
||||||
setLoading(false);
|
|
||||||
}
|
|
||||||
}, [connected]);
|
|
||||||
|
|
||||||
return {
|
|
||||||
user,
|
|
||||||
isSignedIn: Boolean(user) || auth.isSignedIn(),
|
|
||||||
loading,
|
|
||||||
error,
|
|
||||||
connected,
|
|
||||||
signup: (data) => wrap(() => auth.signup(data), { email: data.email, firstName: data.firstName, lastName: data.lastName }),
|
|
||||||
signin: (email, password, rememberMe = false) => wrap(() => auth.signin(email, password, rememberMe), { email }),
|
|
||||||
signout: () => { auth.signout(); setUser(null); },
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
// ── useCart — capability: store (client-side cart + tyga.run checkout) ───────
|
|
||||||
// Cart lives in React state (+ localStorage); checkout hits ecommerce. Demo
|
|
||||||
// mode returns a fake checkout URL so the flow is walkable pre-connection.
|
|
||||||
export interface CartItem { id: string; name: string; priceCents: number; quantity: number; [k: string]: any }
|
|
||||||
|
|
||||||
export function useCart() {
|
|
||||||
const connected = isConfigured();
|
|
||||||
const CART_KEY = "tygarun_cart";
|
|
||||||
const [items, setItems] = useState<CartItem[]>(() => {
|
|
||||||
try { return JSON.parse(localStorage.getItem(CART_KEY) || "[]"); } catch { return []; }
|
|
||||||
});
|
|
||||||
const [loading, setLoading] = useState(false);
|
|
||||||
|
|
||||||
const persist = useCallback((next: CartItem[]) => {
|
|
||||||
setItems(next);
|
|
||||||
try { localStorage.setItem(CART_KEY, JSON.stringify(next)); } catch { /* SSR */ }
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const add = useCallback((item: Omit<CartItem, "quantity">, qty = 1) => {
|
|
||||||
persist((() => {
|
|
||||||
const existing = items.find((i) => i.id === item.id);
|
|
||||||
return existing
|
|
||||||
? items.map((i) => (i.id === item.id ? { ...i, quantity: i.quantity + qty } : i))
|
|
||||||
: [...items, { ...item, quantity: qty }];
|
|
||||||
})());
|
|
||||||
}, [items, persist]);
|
|
||||||
|
|
||||||
const remove = useCallback((id: string) => persist(items.filter((i) => i.id !== id)), [items, persist]);
|
|
||||||
const clear = useCallback(() => persist([]), [persist]);
|
|
||||||
const count = items.reduce((n, i) => n + i.quantity, 0);
|
|
||||||
const totalCents = items.reduce((n, i) => n + i.priceCents * i.quantity, 0);
|
|
||||||
|
|
||||||
const checkout = useCallback(async () => {
|
|
||||||
setLoading(true);
|
|
||||||
try {
|
|
||||||
if (!connected) return { url: "#demo-checkout", demo: true };
|
|
||||||
return await tygarun("ecommerce", "/checkout", { method: "POST", body: { items } });
|
|
||||||
} finally {
|
|
||||||
setLoading(false);
|
|
||||||
}
|
|
||||||
}, [connected, items]);
|
|
||||||
|
|
||||||
return { items, count, totalCents, add, remove, clear, checkout, loading, connected };
|
|
||||||
}
|
|
||||||
@@ -1,21 +0,0 @@
|
|||||||
// ---------------------------------------------------------------------------
|
|
||||||
// AnyDB Client Stub — auto-included in every AUTO_DB React project
|
|
||||||
// ---------------------------------------------------------------------------
|
|
||||||
// When AnyDB is connected, this file is replaced by the real client
|
|
||||||
// generated by anydbService.getClientCode(). Until then, this stub
|
|
||||||
// returns empty results so components don't crash.
|
|
||||||
// ---------------------------------------------------------------------------
|
|
||||||
|
|
||||||
const STUB_COLLECTION = {
|
|
||||||
find: async (_filter?: any, _opts?: any) => ({ data: [], total: 0, page: 1, limit: 50, pages: 0 }),
|
|
||||||
insertOne: async (doc: any) => ({ ...doc, _id: `stub_${Date.now()}` }),
|
|
||||||
updateOne: async (_filter: any, _update: any) => ({ modifiedCount: 0 }),
|
|
||||||
deleteOne: async (_filter: any) => ({ deletedCount: 0 }),
|
|
||||||
aggregate: async (_pipeline: any[]) => [],
|
|
||||||
};
|
|
||||||
|
|
||||||
const anydb = {
|
|
||||||
collection: async (_name: string) => STUB_COLLECTION,
|
|
||||||
};
|
|
||||||
|
|
||||||
export default anydb;
|
|
||||||
@@ -1,121 +0,0 @@
|
|||||||
// ---------------------------------------------------------------------------
|
|
||||||
// OddSockets Client — realtime pub/sub + presence for generated sites
|
|
||||||
// ---------------------------------------------------------------------------
|
|
||||||
// The realtime leg of the tyga backend bundle (alongside lib/anydb.ts data +
|
|
||||||
// lib/tygarun.ts SaaS). Channels, presence and history via OddSockets.
|
|
||||||
//
|
|
||||||
// This stub ships UNCONFIGURED. When the project connects OddSockets, the
|
|
||||||
// builder replaces ODDSOCKETS_CONFIG with real values (like lib/tygarun.ts)
|
|
||||||
// and this file is swapped for the real client (oddsocketsService.getClientCode,
|
|
||||||
// which imports `oddsockets-js`). Until then isConfigured() is false and the
|
|
||||||
// hooks fall back to a LOCAL in-memory demo bus so the UI is alive out of the
|
|
||||||
// box. The demo bus is clearly ephemeral — a broken real connect must surface
|
|
||||||
// as an error, never be masked by the demo state.
|
|
||||||
// ---------------------------------------------------------------------------
|
|
||||||
|
|
||||||
const ODDSOCKETS_CONFIG = {
|
|
||||||
// Always connect the manager LB — it assigns the worker and the SDK follows
|
|
||||||
// the handoff. NEVER hardcode a worker URL.
|
|
||||||
connectUrl: "__ODDSOCKETS_CONNECT_URL__", // e.g. https://connect.oddsockets.tyga.network
|
|
||||||
// Per-site key (Authorization: Bearer ak_live_...). Isolation boundary = the
|
|
||||||
// key owner (account). Provisioned per site by our reseller rail on connect.
|
|
||||||
apiKey: "__ODDSOCKETS_KEY__",
|
|
||||||
};
|
|
||||||
|
|
||||||
export function isConfigured(): boolean {
|
|
||||||
return !ODDSOCKETS_CONFIG.apiKey.startsWith("__");
|
|
||||||
}
|
|
||||||
|
|
||||||
export const CONNECT_URL = ODDSOCKETS_CONFIG.connectUrl;
|
|
||||||
|
|
||||||
export interface PresenceMember {
|
|
||||||
id: string;
|
|
||||||
state?: Record<string, any>;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface ChannelMessage {
|
|
||||||
id: string;
|
|
||||||
data: any;
|
|
||||||
at: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface OddSocketsChannel {
|
|
||||||
subscribe(cb: (msg: ChannelMessage) => void, opts?: Record<string, any>): () => void;
|
|
||||||
publish(data: any, opts?: Record<string, any>): Promise<void>;
|
|
||||||
getHistory(opts?: { count?: number; start?: string; end?: string }): Promise<ChannelMessage[]>;
|
|
||||||
getPresence(): Promise<PresenceMember[]>;
|
|
||||||
updateState(state: Record<string, any>): Promise<void>;
|
|
||||||
unsubscribe(): void;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface OddSocketsClient {
|
|
||||||
channel(name: string): OddSocketsChannel;
|
|
||||||
connected: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
// ── Local in-memory demo bus (used until OddSockets is connected) ────────────
|
|
||||||
// Simulates pub/sub + presence within the tab so realtime UIs render live in
|
|
||||||
// demo mode. Not shared across clients — purely local, seed/demo only.
|
|
||||||
type Listener = (msg: ChannelMessage) => void;
|
|
||||||
|
|
||||||
function createDemoBus(): OddSocketsClient {
|
|
||||||
const listeners: Record<string, Set<Listener>> = {};
|
|
||||||
const history: Record<string, ChannelMessage[]> = {};
|
|
||||||
const presence: Record<string, Record<string, PresenceMember>> = {};
|
|
||||||
const selfId = `demo_${Math.random().toString(36).slice(2, 8)}`;
|
|
||||||
|
|
||||||
function channel(name: string): OddSocketsChannel {
|
|
||||||
listeners[name] = listeners[name] || new Set();
|
|
||||||
history[name] = history[name] || [];
|
|
||||||
presence[name] = presence[name] || {};
|
|
||||||
|
|
||||||
return {
|
|
||||||
subscribe(cb, _opts) {
|
|
||||||
listeners[name].add(cb);
|
|
||||||
// announce demo self-presence
|
|
||||||
presence[name][selfId] = { id: selfId, state: {} };
|
|
||||||
return () => listeners[name].delete(cb);
|
|
||||||
},
|
|
||||||
async publish(data, _opts) {
|
|
||||||
const msg: ChannelMessage = {
|
|
||||||
id: `m_${Date.now()}_${Math.random().toString(36).slice(2, 6)}`,
|
|
||||||
data,
|
|
||||||
at: new Date().toISOString(),
|
|
||||||
};
|
|
||||||
history[name].push(msg);
|
|
||||||
if (history[name].length > 200) history[name].shift();
|
|
||||||
listeners[name].forEach((cb) => cb(msg));
|
|
||||||
},
|
|
||||||
async getHistory(opts = {}) {
|
|
||||||
const all = history[name] || [];
|
|
||||||
return opts.count ? all.slice(-opts.count) : all.slice();
|
|
||||||
},
|
|
||||||
async getPresence() {
|
|
||||||
return Object.values(presence[name] || {});
|
|
||||||
},
|
|
||||||
async updateState(state) {
|
|
||||||
presence[name][selfId] = { id: selfId, state };
|
|
||||||
},
|
|
||||||
unsubscribe() {
|
|
||||||
listeners[name].clear();
|
|
||||||
delete presence[name][selfId];
|
|
||||||
},
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
return { channel, connected: false };
|
|
||||||
}
|
|
||||||
|
|
||||||
// Singleton client — demo bus in the stub; the real client (oddsockets-js)
|
|
||||||
// replaces this whole file on connect via oddsocketsService.getClientCode.
|
|
||||||
const client: OddSocketsClient = createDemoBus();
|
|
||||||
|
|
||||||
export default client;
|
|
||||||
|
|
||||||
// Usage:
|
|
||||||
// import oddsockets, { isConfigured } from '@/lib/oddsockets';
|
|
||||||
// const ch = oddsockets.channel('room:lobby');
|
|
||||||
// const off = ch.subscribe((m) => console.log(m.data));
|
|
||||||
// await ch.publish({ text: 'hello' });
|
|
||||||
// await ch.updateState({ typing: true });
|
|
||||||
// const members = await ch.getPresence();
|
|
||||||
@@ -1,89 +0,0 @@
|
|||||||
// ---------------------------------------------------------------------------
|
|
||||||
// safe-icons.ts — Verified lucide-react icon re-exports
|
|
||||||
// ---------------------------------------------------------------------------
|
|
||||||
// PRE-INSTALLED in every React project. CodeWriterAgent imports from here
|
|
||||||
// instead of directly from lucide-react, preventing non-existent icon errors.
|
|
||||||
// import { ArrowRight, Star, Menu, X } from "@/lib/safe-icons"
|
|
||||||
// ---------------------------------------------------------------------------
|
|
||||||
|
|
||||||
export {
|
|
||||||
// Navigation & UI
|
|
||||||
Menu, X, ChevronDown, ChevronUp, ChevronLeft, ChevronRight,
|
|
||||||
ChevronsUpDown, ArrowRight, ArrowLeft, ArrowUp, ArrowDown,
|
|
||||||
ArrowUpRight, ExternalLink, MoreHorizontal, MoreVertical,
|
|
||||||
Search, Filter, SlidersHorizontal, Loader2, RefreshCw,
|
|
||||||
|
|
||||||
// Actions
|
|
||||||
Plus, Minus, Check, CheckCircle, CheckCircle2, XCircle,
|
|
||||||
Copy, Download, Upload, Share2, Send, Save, Trash2,
|
|
||||||
Edit, PenLine, Pencil, Eye, EyeOff, LogIn, LogOut,
|
|
||||||
Settings, Settings2, Sliders,
|
|
||||||
|
|
||||||
// Communication
|
|
||||||
Mail, Phone, MessageCircle, MessageSquare, Bell, BellRing,
|
|
||||||
Inbox, Send as SendIcon, AtSign,
|
|
||||||
|
|
||||||
// Media
|
|
||||||
Image, Camera, Video, Music, Play, Pause, Volume2, Mic,
|
|
||||||
PlayCircle, Film,
|
|
||||||
|
|
||||||
// Social
|
|
||||||
Github, Twitter, Linkedin, Instagram, Facebook, Youtube,
|
|
||||||
Globe, Globe2, Rss,
|
|
||||||
|
|
||||||
// Business
|
|
||||||
Briefcase, Building2, CreditCard, DollarSign, ShoppingCart,
|
|
||||||
ShoppingBag, Package, Receipt, Wallet, PiggyBank,
|
|
||||||
TrendingUp, TrendingDown, BarChart3, PieChart, LineChart,
|
|
||||||
|
|
||||||
// People
|
|
||||||
User, Users, UserPlus, UserCheck, UserCircle, Contact,
|
|
||||||
Heart, ThumbsUp, Star, Award, Trophy, Medal, Crown,
|
|
||||||
|
|
||||||
// Content
|
|
||||||
FileText, File, Folder, BookOpen, Book, Newspaper,
|
|
||||||
Bookmark, Tag, Hash, Quote, Type, AlignLeft,
|
|
||||||
|
|
||||||
// Tech
|
|
||||||
Code, Code2, Terminal, Database, Server, Cloud, Wifi,
|
|
||||||
Smartphone, Monitor, Laptop, Globe as GlobeIcon,
|
|
||||||
Cpu, Zap, Bolt, Plug, Key, Lock, Unlock, Shield,
|
|
||||||
ShieldCheck, Bug, Rocket, Wand2,
|
|
||||||
|
|
||||||
// Layout
|
|
||||||
LayoutDashboard, LayoutGrid, Grid, Layers, Sidebar,
|
|
||||||
PanelLeft, PanelRight, Columns2, Rows2, Table,
|
|
||||||
|
|
||||||
// Time & Calendar
|
|
||||||
Clock, Calendar, CalendarDays, Timer, Hourglass,
|
|
||||||
Sunrise, Sunset, Moon, Sun,
|
|
||||||
|
|
||||||
// Location
|
|
||||||
MapPin, Map, Compass, Navigation, Home, Building2 as BuildingIcon,
|
|
||||||
Flag, Route,
|
|
||||||
|
|
||||||
// Health & Science
|
|
||||||
Heart as HeartIcon, HeartPulse, Activity, Stethoscope,
|
|
||||||
Pill, Syringe, Thermometer, Microscope, Atom, Brain,
|
|
||||||
|
|
||||||
// Nature
|
|
||||||
Leaf, TreePine, Trees, Flower, Sprout, Mountain,
|
|
||||||
Waves, Wind, Snowflake, Sun as SunIcon, Cloud as CloudIcon,
|
|
||||||
|
|
||||||
// Food & Drink
|
|
||||||
Utensils, Coffee, Pizza, Wine, CookingPot, Cherry,
|
|
||||||
Apple, Cake, IceCream,
|
|
||||||
|
|
||||||
// Transport
|
|
||||||
Car, Plane, Train, Bus, Bike, Truck, Ship, Sailboat,
|
|
||||||
|
|
||||||
// Education
|
|
||||||
GraduationCap, School, Library, BookOpen as BookOpenIcon,
|
|
||||||
Lightbulb, Puzzle, Target,
|
|
||||||
|
|
||||||
// Misc
|
|
||||||
Sparkles, Flame, Gift, Ticket, Scissors, Wrench,
|
|
||||||
Hammer, Paintbrush, Palette, Gem, Diamond, Ribbon,
|
|
||||||
PartyPopper, Smile, Frown, AlertTriangle, Info,
|
|
||||||
HelpCircle, CircleAlert, AlertCircle,
|
|
||||||
} from "lucide-react";
|
|
||||||
@@ -1,115 +0,0 @@
|
|||||||
// ---------------------------------------------------------------------------
|
|
||||||
// TygaRun Client — ready-made SaaS APIs for generated sites
|
|
||||||
// ---------------------------------------------------------------------------
|
|
||||||
// Universal client for ALL sdk.tyga.run modules: auth, scheduling (bookings/
|
|
||||||
// eventtypes/availability), crm, ecommerce, billing, notifications, webhooks...
|
|
||||||
// Where Lovable gives you raw tables, tyga.run gives you ready-made APIs.
|
|
||||||
//
|
|
||||||
// This stub ships unconfigured. When the project connects tyga.run, the
|
|
||||||
// builder replaces TYGARUN_CONFIG with real values (like lib/anydb.ts).
|
|
||||||
// Until then isConfigured() is false and hooks fall back to seed data.
|
|
||||||
// ---------------------------------------------------------------------------
|
|
||||||
|
|
||||||
const TYGARUN_CONFIG = {
|
|
||||||
baseUrl: "__TYGARUN_SDK_URL__", // e.g. https://sdk.tyga.run
|
|
||||||
// Browser-safe `tc_pub_` publishable key (Supabase-anon model): origin-locked,
|
|
||||||
// sandbox-pinned, module-restricted server-side. Safe to ship in public code.
|
|
||||||
publishableKey: "__TYGARUN_ANON_KEY__",
|
|
||||||
sandbox: "__TYGARUN_SANDBOX__", // "true" | "false"
|
|
||||||
};
|
|
||||||
|
|
||||||
export function isConfigured(): boolean {
|
|
||||||
return !TYGARUN_CONFIG.publishableKey.startsWith("__");
|
|
||||||
}
|
|
||||||
|
|
||||||
const TOKEN_KEY = "tygarun_token";
|
|
||||||
|
|
||||||
export function getToken(): string | null {
|
|
||||||
try { return localStorage.getItem(TOKEN_KEY); } catch { return null; }
|
|
||||||
}
|
|
||||||
export function setToken(token: string | null) {
|
|
||||||
try {
|
|
||||||
if (token) localStorage.setItem(TOKEN_KEY, token);
|
|
||||||
else localStorage.removeItem(TOKEN_KEY);
|
|
||||||
} catch { /* SSR / private mode */ }
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface TygaRunError extends Error {
|
|
||||||
statusCode?: number;
|
|
||||||
body?: any;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Universal call to any tyga.run module endpoint.
|
|
||||||
* tygarun("scheduling", "/bookings", { query: { page: 1 } })
|
|
||||||
* tygarun("auth", "/signin", { method: "POST", body: { email, password } })
|
|
||||||
* tygarun("crm", "/contacts", { method: "POST", body: contact })
|
|
||||||
*/
|
|
||||||
export async function tygarun<T = any>(
|
|
||||||
module: string,
|
|
||||||
endpoint = "/",
|
|
||||||
opts: {
|
|
||||||
method?: "GET" | "POST" | "PUT" | "PATCH" | "DELETE";
|
|
||||||
body?: any;
|
|
||||||
query?: Record<string, any>;
|
|
||||||
headers?: Record<string, string>;
|
|
||||||
} = {}
|
|
||||||
): Promise<T> {
|
|
||||||
if (!isConfigured()) {
|
|
||||||
const err = new Error("tyga.run not connected") as TygaRunError;
|
|
||||||
err.statusCode = 0;
|
|
||||||
throw err;
|
|
||||||
}
|
|
||||||
const { method = "GET", body, query = {}, headers = {} } = opts;
|
|
||||||
const path = endpoint.startsWith("/") ? endpoint : `/${endpoint}`;
|
|
||||||
const url = new URL(`${TYGARUN_CONFIG.baseUrl.replace(/\/+$/, "")}/${module}${path}`);
|
|
||||||
Object.entries(query).forEach(([k, v]) => {
|
|
||||||
if (v !== null && v !== undefined) url.searchParams.append(k, String(v));
|
|
||||||
});
|
|
||||||
|
|
||||||
const token = getToken();
|
|
||||||
const res = await fetch(url.toString(), {
|
|
||||||
method,
|
|
||||||
headers: {
|
|
||||||
"Content-Type": "application/json",
|
|
||||||
Accept: "application/json",
|
|
||||||
// Publishable key ALWAYS authenticates the app (never the user).
|
|
||||||
Authorization: `Bearer ${TYGARUN_CONFIG.publishableKey}`,
|
|
||||||
// End-user identity (their JWT from auth.signin) rides separately.
|
|
||||||
...(token ? { "X-User-Token": token } : {}),
|
|
||||||
"X-Sandbox-Mode": TYGARUN_CONFIG.sandbox,
|
|
||||||
...headers,
|
|
||||||
},
|
|
||||||
body: body && method !== "GET" && method !== "DELETE" ? JSON.stringify(body) : undefined,
|
|
||||||
});
|
|
||||||
|
|
||||||
const text = await res.text();
|
|
||||||
let json: any = {};
|
|
||||||
try { json = text ? JSON.parse(text) : {}; } catch { /* non-JSON error body */ }
|
|
||||||
|
|
||||||
if (!res.ok) {
|
|
||||||
const err = new Error(json.error || json.message || `tyga.run ${module} error ${res.status}`) as TygaRunError;
|
|
||||||
err.statusCode = res.status;
|
|
||||||
err.body = json;
|
|
||||||
throw err;
|
|
||||||
}
|
|
||||||
return json as T;
|
|
||||||
}
|
|
||||||
|
|
||||||
// ── Auth convenience (JWT stored for user-scoped calls) ─────────────────────
|
|
||||||
export const auth = {
|
|
||||||
async signup(data: { email: string; password: string; firstName?: string; lastName?: string; [k: string]: any }) {
|
|
||||||
const res = await tygarun<any>("auth", "/signup", { method: "POST", body: data });
|
|
||||||
if (res.token) setToken(res.token);
|
|
||||||
return res;
|
|
||||||
},
|
|
||||||
async signin(email: string, password: string, rememberMe = false) {
|
|
||||||
const res = await tygarun<any>("auth", "/signin", { method: "POST", body: { email, password, rememberMe } });
|
|
||||||
if (res.token) setToken(res.token);
|
|
||||||
return res;
|
|
||||||
},
|
|
||||||
signout() { setToken(null); },
|
|
||||||
isSignedIn(): boolean { return Boolean(getToken()); },
|
|
||||||
};
|
|
||||||
|
|
||||||
export default tygarun;
|
|
||||||
+2
-5
@@ -1,6 +1,3 @@
|
|||||||
import { type ClassValue, clsx } from "clsx";
|
export function cn(...inputs: Array<string | number | null | undefined | false>) {
|
||||||
import { twMerge } from "tailwind-merge";
|
return inputs.filter(Boolean).join(" ");
|
||||||
|
|
||||||
export function cn(...inputs: ClassValue[]) {
|
|
||||||
return twMerge(clsx(inputs));
|
|
||||||
}
|
}
|
||||||
+19
-19
@@ -7,23 +7,23 @@ import Header from "@/components/Header";
|
|||||||
import Footer from "@/components/Footer";
|
import Footer from "@/components/Footer";
|
||||||
|
|
||||||
const milestones = [
|
const milestones = [
|
||||||
{ year: "1987", title: "The Beginning", desc: "Chef Bellini opens Aurum with just 20 seats in Mayfair." },
|
{ year: "2019", title: "The Beginning", desc: "Saffron Table opens its doors in Shoreditch with a vision to bring authentic Persian flavours to London." },
|
||||||
{ year: "1995", title: "First Michelin Star", desc: "Aurum earns its first Michelin star, cementing its place among London’s finest." },
|
{ year: "2021", title: "Best New Restaurant", desc: "Named London’s Best New Restaurant by Time Out for our innovative mezze concept." },
|
||||||
{ year: "2008", title: "Second Star & Expansion", desc: "A second star and a major renovation doubles capacity with private dining rooms." },
|
{ year: "2023", title: "Charcoal Grill Expansion", desc: "Our signature charcoal grill becomes the heart of the kitchen, elevating every dish." },
|
||||||
{ year: "2024", title: "World’s 50 Best", desc: "Named #32 on the World’s 50 Best Restaurants list for the third time." },
|
{ year: "2025", title: "Cocktail Recognition", desc: "Our saffron cocktail program wins Best Bar Program from Restaurant Magazine." },
|
||||||
];
|
];
|
||||||
|
|
||||||
const values = [
|
const values = [
|
||||||
{ icon: Leaf, title: "Ingredients", desc: "We source the finest seasonal ingredients from trusted local farms and sustainable producers worldwide." },
|
{ icon: Leaf, title: "Ingredients", desc: "We source premium saffron directly from Iran and seasonal produce from local British farms." },
|
||||||
{ icon: Star, title: "Technique", desc: "Classical training meets modern innovation. Every dish reflects decades of accumulated mastery and creativity." },
|
{ icon: Star, title: "Technique", desc: "Our charcoal grill and traditional Persian methods meet modern culinary innovation." },
|
||||||
{ icon: Heart, title: "Hospitality", desc: "Every guest is family. Our service team ensures each visit is warm, personal, and truly memorable." },
|
{ icon: Heart, title: "Hospitality", desc: "Every guest is family. We bring the warmth of Persian hospitality to Shoreditch." },
|
||||||
];
|
];
|
||||||
|
|
||||||
const awards = [
|
const awards = [
|
||||||
{ name: "2 Michelin Stars", year: "2008–Present" },
|
{ name: "Best New Restaurant London", year: "2021" },
|
||||||
{ name: "World’s 50 Best Restaurants", year: "2022, 2023, 2024" },
|
{ name: "Best Bar Program", year: "2025" },
|
||||||
{ name: "Best Fine Dining London", year: "2024" },
|
{ name: "Sustainable Restaurant Award", year: "2024" },
|
||||||
{ name: "Sustainability Award", year: "2023" },
|
{ name: "Top 100 UK Restaurants", year: "2023, 2024" },
|
||||||
];
|
];
|
||||||
|
|
||||||
export default function AboutPage() {
|
export default function AboutPage() {
|
||||||
@@ -44,9 +44,9 @@ export default function AboutPage() {
|
|||||||
<section className="py-24">
|
<section className="py-24">
|
||||||
<div className="mx-auto max-w-7xl px-6 grid lg:grid-cols-2 gap-16 items-center">
|
<div className="mx-auto max-w-7xl px-6 grid lg:grid-cols-2 gap-16 items-center">
|
||||||
<div>
|
<div>
|
||||||
<h2 className="text-3xl font-serif tracking-wide mb-6 animate-fade-up">A Legacy of Excellence</h2>
|
<h2 className="text-3xl font-serif tracking-wide mb-6 animate-fade-up">A Story of Flavour & Hospitality</h2>
|
||||||
<p className="text-muted-foreground leading-relaxed mb-4">For over three decades, Aurum has been a sanctuary for those who appreciate the art of fine dining. Founded by Chef Marcus Bellini in 1987, our kitchen has always been guided by one philosophy: let exceptional ingredients speak for themselves.</p>
|
<p className="text-muted-foreground leading-relaxed mb-4">Saffron Table was born from a love of Persian hospitality and Mediterranean abundance. Founded in 2019, we bring the warmth of ancient traditions to creative Shoreditch.</p>
|
||||||
<p className="text-muted-foreground leading-relaxed">Every morning, our team sources the freshest produce from local farms, the finest seafood from sustainable fisheries, and rare ingredients from trusted purveyors around the world.</p>
|
<p className="text-muted-foreground leading-relaxed">Our charcoal grill is the heart of our kitchen, while our saffron infusion program creates cocktails that transport you to the bustling markets of Tehran and the shores of the Mediterranean.</p>
|
||||||
</div>
|
</div>
|
||||||
<div className="relative">
|
<div className="relative">
|
||||||
<div className="aspect-[4/5] rounded-sm bg-gradient-to-br from-amber-800/30 via-stone-700/30 to-amber-900/30 relative overflow-hidden">
|
<div className="aspect-[4/5] rounded-sm bg-gradient-to-br from-amber-800/30 via-stone-700/30 to-amber-900/30 relative overflow-hidden">
|
||||||
@@ -71,10 +71,10 @@ export default function AboutPage() {
|
|||||||
</div>
|
</div>
|
||||||
<div className="order-1 lg:order-2">
|
<div className="order-1 lg:order-2">
|
||||||
<Badge variant="secondary" className="mb-4 text-xs bg-amber-900/20 text-amber-700 dark:text-amber-400 border-0">Executive Chef</Badge>
|
<Badge variant="secondary" className="mb-4 text-xs bg-amber-900/20 text-amber-700 dark:text-amber-400 border-0">Executive Chef</Badge>
|
||||||
<h2 className="text-3xl font-serif tracking-wide mb-2">Chef Marcus Bellini</h2>
|
<h2 className="text-3xl font-serif tracking-wide mb-2">Chef Yasmin Kashani</h2>
|
||||||
<p className="text-muted-foreground leading-relaxed mb-4">Born in Turin and trained in the kitchens of Paris, Chef Bellini brings over 40 years of culinary mastery to every dish. His style blends classic French technique with Italian soul.</p>
|
<p className="text-muted-foreground leading-relaxed mb-4">Born in Tehran and raised in London, Chef Kashani brings the flavours of her Persian heritage together with Mediterranean influences. Her charcoal-grilled creations have redefined modern mezze.</p>
|
||||||
<p className="text-muted-foreground leading-relaxed">Under his leadership, Aurum has earned two Michelin stars and been named among the World’s 50 Best Restaurants three times.</p>
|
<p className="text-muted-foreground leading-relaxed">Under her leadership, Saffron Table has become known for London’s most authentic Persian-Mediterranean fusion experience.</p>
|
||||||
<blockquote className="text-lg font-serif italic text-muted-foreground border-l-2 border-amber-700/30 pl-4 my-6">"The finest ingredients deserve the simplest preparation. Let quality speak for itself."</blockquote>
|
<blockquote className="text-lg font-serif italic text-muted-foreground border-l-2 border-amber-700/30 pl-4 my-6">"Food is memory. Every dish should taste like coming home."</blockquote>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
@@ -134,7 +134,7 @@ export default function AboutPage() {
|
|||||||
{/* ── CTA ── */}
|
{/* ── CTA ── */}
|
||||||
<section className="py-24 bg-gradient-to-b from-stone-950 to-amber-950/30">
|
<section className="py-24 bg-gradient-to-b from-stone-950 to-amber-950/30">
|
||||||
<div className="mx-auto max-w-3xl px-6 text-center">
|
<div className="mx-auto max-w-3xl px-6 text-center">
|
||||||
<h2 className="text-3xl font-serif tracking-wide text-white mb-4">Experience Aurum</h2>
|
<h2 className="text-3xl font-serif tracking-wide text-white mb-4">Experience Saffron Table</h2>
|
||||||
<p className="text-stone-400 mb-8">We look forward to welcoming you</p>
|
<p className="text-stone-400 mb-8">We look forward to welcoming you</p>
|
||||||
<Button asChild size="lg" className="rounded-none px-10 text-sm tracking-widest uppercase bg-amber-700 hover:bg-amber-600 text-white active:scale-[0.98]">
|
<Button asChild size="lg" className="rounded-none px-10 text-sm tracking-widest uppercase bg-amber-700 hover:bg-amber-600 text-white active:scale-[0.98]">
|
||||||
<Link to="/reservations">Book a Table <ArrowRight className="ml-2 h-4 w-4" /></Link>
|
<Link to="/reservations">Book a Table <ArrowRight className="ml-2 h-4 w-4" /></Link>
|
||||||
|
|||||||
@@ -10,15 +10,15 @@ import Header from "@/components/Header";
|
|||||||
import Footer from "@/components/Footer";
|
import Footer from "@/components/Footer";
|
||||||
|
|
||||||
const rooms = [
|
const rooms = [
|
||||||
{ name: "The Cellar", capacity: "12", desc: "An intimate underground space with exposed brick walls and a curated wine display. Perfect for exclusive dinners.", gradient: "from-amber-800/30 to-stone-700/30" },
|
{ name: "The Saffron Room", capacity: "12", desc: "An intimate space adorned with Persian artwork and warm terracotta tones. Perfect for exclusive dinners.", gradient: "from-amber-800/30 to-stone-700/30" },
|
||||||
{ name: "The Gallery", capacity: "24", desc: "Elegant room featuring rotating art installations with floor-to-ceiling windows overlooking the garden.", gradient: "from-stone-700/30 to-amber-900/30" },
|
{ name: "The Mezze Terrace", capacity: "24", desc: "Beautiful private dining area overlooking Shoreditch High Street with natural light throughout.", gradient: "from-stone-700/30 to-amber-900/30" },
|
||||||
{ name: "The Terrace", capacity: "40", desc: "Our largest private space with retractable roof, perfect for celebrations and corporate gatherings.", gradient: "from-amber-900/30 to-stone-800/30" },
|
{ name: "The Courtyard", capacity: "40", desc: "Our largest private space with retractable roof, perfect for celebrations and corporate gatherings.", gradient: "from-amber-900/30 to-stone-800/30" },
|
||||||
];
|
];
|
||||||
|
|
||||||
const events = [
|
const events = [
|
||||||
{ title: "Wine Dinner Series", desc: "Monthly five-course dinner paired with wines from a featured vineyard. An evening of discovery and indulgence.", badge: "Monthly" },
|
{ title: "Persian Wine & Mezze Nights", desc: "Monthly five-course mezze dinner paired with Persian wines. An evening of discovery and indulgence.", badge: "Monthly" },
|
||||||
{ title: "Chef’s Table Experience", desc: "An exclusive 8-seat experience at the kitchen counter with a bespoke tasting menu narrated by the chef.", badge: "Quarterly" },
|
{ title: "Charcoal Grill Masterclass", desc: "Learn our charcoal grilling techniques from the chef in a hands-on class with lunch and cocktails included.", badge: "Quarterly" },
|
||||||
{ title: "Cooking Masterclass", desc: "Learn signature techniques from our executive chef in a hands-on class with lunch and wine included.", badge: "Seasonal" },
|
{ title: "Saffron Cocktail Lab", desc: "An exclusive workshop learning to create signature saffron cocktails with our head bartender.", badge: "Seasonal" },
|
||||||
];
|
];
|
||||||
|
|
||||||
export default function EventsPage() {
|
export default function EventsPage() {
|
||||||
@@ -86,7 +86,7 @@ export default function EventsPage() {
|
|||||||
<div className="mx-auto max-w-3xl px-6 text-center">
|
<div className="mx-auto max-w-3xl px-6 text-center">
|
||||||
<ChefHat className="h-10 w-10 mx-auto text-amber-600 mb-4" />
|
<ChefHat className="h-10 w-10 mx-auto text-amber-600 mb-4" />
|
||||||
<h2 className="text-3xl font-serif tracking-wide mb-4">Off-Site Catering</h2>
|
<h2 className="text-3xl font-serif tracking-wide mb-4">Off-Site Catering</h2>
|
||||||
<p className="text-muted-foreground mb-8 max-w-xl mx-auto">Bring the Aurum experience to your venue. Our catering team creates bespoke menus for events of any size, from intimate dinner parties to grand celebrations.</p>
|
<p className="text-muted-foreground mb-8 max-w-xl mx-auto">Bring the Saffron Table experience to your venue. Our catering team creates bespoke Persian-Mediterranean menus for events of any size, from intimate dinner parties to grand celebrations.</p>
|
||||||
<Button className="rounded-none bg-amber-700 hover:bg-amber-600 text-white tracking-widest uppercase text-xs px-8 active:scale-[0.98]">
|
<Button className="rounded-none bg-amber-700 hover:bg-amber-600 text-white tracking-widest uppercase text-xs px-8 active:scale-[0.98]">
|
||||||
Request a Quote <ArrowRight className="ml-2 h-4 w-4" />
|
Request a Quote <ArrowRight className="ml-2 h-4 w-4" />
|
||||||
</Button>
|
</Button>
|
||||||
|
|||||||
+16
-16
@@ -9,15 +9,15 @@ import Header from "@/components/Header";
|
|||||||
import Footer from "@/components/Footer";
|
import Footer from "@/components/Footer";
|
||||||
|
|
||||||
const highlights = [
|
const highlights = [
|
||||||
{ name: "Wagyu Beef Tenderloin", desc: "A5 wagyu, bone marrow butter, roasted root vegetables, red wine jus", price: "$62", gradient: "from-amber-800/30 to-stone-700/30" },
|
{ name: "Charcoal-Grilled Lamb Kofta", desc: "Hand-spiced lamb with saffron rice, grilled tomatoes, and mint yogurt", price: "£24", gradient: "from-amber-800/30 to-stone-700/30" },
|
||||||
{ name: "Seared Scallops", desc: "Pan-seared diver scallops, cauliflower purée, truffle vinaigrette", price: "$24", gradient: "from-stone-700/30 to-amber-900/30" },
|
{ name: "Saffron Salmon", desc: "Wild-caught salmon with saffron beurre blanc, fennel, and pomegranate", price: "£22", gradient: "from-stone-700/30 to-amber-900/30" },
|
||||||
{ name: "Valrhona Chocolate Fondant", desc: "Warm dark chocolate cake, vanilla bean ice cream, gold leaf", price: "$16", gradient: "from-amber-900/30 to-stone-800/30" },
|
{ name: "Persian Rose Panna Cotta", desc: "Creamy vanilla panna cotta with rose water, pistachios, and honey", price: "£12", gradient: "from-amber-900/30 to-stone-800/30" },
|
||||||
];
|
];
|
||||||
|
|
||||||
const testimonials = [
|
const testimonials = [
|
||||||
{ name: "James & Katherine", text: "An extraordinary dining experience. Every dish was a masterpiece. The tasting menu was the highlight of our trip.", source: "Google Reviews" },
|
{ name: "Maya & Tariq", text: "The charcoal-grilled mezze took us straight back to Tehran. Authentic flavours with a modern London twist.", source: "Google Reviews" },
|
||||||
{ name: "Sophie Laurent", text: "Impeccable service and the most beautiful presentation I have ever seen. The wine pairing was exquisite.", source: "TripAdvisor" },
|
{ name: "Elena Rossi", text: "Best saffron cocktails in Shoreditch! The Persian-Mediterranean fusion is absolutely divine.", source: "TripAdvisor" },
|
||||||
{ name: "Michael Chen", text: "Worth every penny. The wagyu was the best I have had outside of Japan. Already planning our return visit.", source: "Yelp" },
|
{ name: "Jack Thompson", text: "Warm hospitality and incredible food. The lamb kofta is a must-try. We'll be back weekly.", source: "Yelp" },
|
||||||
];
|
];
|
||||||
|
|
||||||
export default function IndexPage() {
|
export default function IndexPage() {
|
||||||
@@ -39,10 +39,10 @@ export default function IndexPage() {
|
|||||||
<span>—</span><span>✦</span><span>—</span>
|
<span>—</span><span>✦</span><span>—</span>
|
||||||
</div>
|
</div>
|
||||||
<Badge variant="secondary" className="mb-6 px-4 py-1.5 text-xs tracking-widest uppercase bg-amber-900/30 text-amber-300 border-amber-700/30 backdrop-blur">
|
<Badge variant="secondary" className="mb-6 px-4 py-1.5 text-xs tracking-widest uppercase bg-amber-900/30 text-amber-300 border-amber-700/30 backdrop-blur">
|
||||||
Est. 1987
|
Est. 2019
|
||||||
</Badge>
|
</Badge>
|
||||||
<h1 className="text-4xl sm:text-5xl md:text-6xl lg:text-7xl font-serif tracking-wide text-white leading-[1.1]">A Culinary Journey Through Timeless Flavors</h1>
|
<h1 className="text-4xl sm:text-5xl md:text-6xl lg:text-7xl font-serif tracking-wide text-white leading-[1.1]">Charcoal-Grilled Mezze & Saffron Cocktails</h1>
|
||||||
<p className="mx-auto mt-6 max-w-xl text-lg text-stone-400 leading-relaxed">Experience refined dining where heritage meets innovation. Every dish tells a story crafted with passion, precision, and the finest seasonal ingredients.</p>
|
<p className="mx-auto mt-6 max-w-xl text-lg text-stone-400 leading-relaxed">Experience the vibrant flavours of Persia and the Mediterranean in the heart of Shoreditch. Every dish tells a story of ancient traditions and modern creativity.</p>
|
||||||
<div className="mt-10 flex flex-col sm:flex-row items-center justify-center gap-4">
|
<div className="mt-10 flex flex-col sm:flex-row items-center justify-center gap-4">
|
||||||
<Button asChild size="lg" className="rounded-none px-10 text-sm tracking-widest uppercase bg-amber-700 hover:bg-amber-600 text-white shadow-lg shadow-amber-900/30 active:scale-[0.98]">
|
<Button asChild size="lg" className="rounded-none px-10 text-sm tracking-widest uppercase bg-amber-700 hover:bg-amber-600 text-white shadow-lg shadow-amber-900/30 active:scale-[0.98]">
|
||||||
<Link to="/reservations">Reserve a Table <ArrowRight className="ml-2 h-4 w-4" /></Link>
|
<Link to="/reservations">Reserve a Table <ArrowRight className="ml-2 h-4 w-4" /></Link>
|
||||||
@@ -95,19 +95,19 @@ export default function IndexPage() {
|
|||||||
<span>—</span><Star className="h-4 w-4" /><span>—</span>
|
<span>—</span><Star className="h-4 w-4" /><span>—</span>
|
||||||
</div>
|
</div>
|
||||||
<h2 className="text-3xl sm:text-4xl font-serif tracking-wide mb-6">Our Story</h2>
|
<h2 className="text-3xl sm:text-4xl font-serif tracking-wide mb-6">Our Story</h2>
|
||||||
<p className="text-muted-foreground leading-relaxed mb-4">For over three decades, Aurum has been a sanctuary for those who appreciate the art of fine dining. Founded by Chef Marcus Bellini, our kitchen is guided by one philosophy: let exceptional ingredients speak for themselves.</p>
|
<p className="text-muted-foreground leading-relaxed mb-4">Born from a love of Persian hospitality and Mediterranean abundance, Saffron Table brings the warmth of ancient traditions to creative Shoreditch. Our charcoal grill is the heart of our kitchen.</p>
|
||||||
<div className="grid grid-cols-3 gap-6 mt-10 mb-8">
|
<div className="grid grid-cols-3 gap-6 mt-10 mb-8">
|
||||||
<div className="text-center">
|
<div className="text-center">
|
||||||
<div className="text-3xl font-serif font-bold text-amber-700 dark:text-amber-400">35+</div>
|
<div className="text-3xl font-serif font-bold text-amber-700 dark:text-amber-400">5+</div>
|
||||||
<div className="text-sm text-muted-foreground mt-1">Years</div>
|
<div className="text-sm text-muted-foreground mt-1">Years</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="text-center">
|
<div className="text-center">
|
||||||
<div className="text-3xl font-serif font-bold text-amber-700 dark:text-amber-400">2</div>
|
<div className="text-3xl font-serif font-bold text-amber-700 dark:text-amber-400">15</div>
|
||||||
<div className="text-sm text-muted-foreground mt-1">Michelin Stars</div>
|
<div className="text-sm text-muted-foreground mt-1">Mezze Dishes</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="text-center">
|
<div className="text-center">
|
||||||
<div className="text-3xl font-serif font-bold text-amber-700 dark:text-amber-400">400+</div>
|
<div className="text-3xl font-serif font-bold text-amber-700 dark:text-amber-400">8</div>
|
||||||
<div className="text-sm text-muted-foreground mt-1">Wines</div>
|
<div className="text-sm text-muted-foreground mt-1">Saffron Cocktails</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<Button asChild variant="outline" className="rounded-none tracking-widest uppercase text-xs px-8 active:scale-[0.98]">
|
<Button asChild variant="outline" className="rounded-none tracking-widest uppercase text-xs px-8 active:scale-[0.98]">
|
||||||
@@ -131,7 +131,7 @@ export default function IndexPage() {
|
|||||||
<div className="mx-auto max-w-7xl px-6">
|
<div className="mx-auto max-w-7xl px-6">
|
||||||
<div className="text-center mb-16 animate-fade-up">
|
<div className="text-center mb-16 animate-fade-up">
|
||||||
<h2 className="text-3xl sm:text-4xl font-serif tracking-wide">Gallery</h2>
|
<h2 className="text-3xl sm:text-4xl font-serif tracking-wide">Gallery</h2>
|
||||||
<p className="mt-3 text-muted-foreground">A glimpse into the Aurum experience</p>
|
<p className="mt-3 text-muted-foreground">A glimpse into the Saffron Table experience</p>
|
||||||
</div>
|
</div>
|
||||||
<div className="grid grid-cols-2 lg:grid-cols-3 gap-4">
|
<div className="grid grid-cols-2 lg:grid-cols-3 gap-4">
|
||||||
{[0, 1, 2, 3, 4, 5].map((i) => (
|
{[0, 1, 2, 3, 4, 5].map((i) => (
|
||||||
|
|||||||
+21
-21
@@ -11,34 +11,34 @@ import { useSaas } from "@/hooks/use-tygarun";
|
|||||||
|
|
||||||
const menuItems = {
|
const menuItems = {
|
||||||
starters: [
|
starters: [
|
||||||
{ name: "Burrata & Heirloom Tomato", desc: "Creamy burrata, vine-ripened tomatoes, basil oil, aged balsamic", price: "$18", badges: ["V"] },
|
{ name: "Warm Hummus & Charcoal Pita", desc: "Creamy hummus topped with saffron oil, paprika, and pine nuts", price: "£10", badges: ["V", "GF"] },
|
||||||
{ name: "Seared Scallops", desc: "Pan-seared diver scallops, cauliflower purée, truffle vinaigrette", price: "$24", badges: ["GF"] },
|
{ name: "Grilled Halloumi & Fig", desc: "Charcoal-grilled Cypriot halloumi with fresh figs and honey drizzle", price: "£12", badges: ["V"] },
|
||||||
{ name: "Duck Confit Terrine", desc: "Slow-cooked duck leg, pistachio, cornichon, sourdough toast", price: "$19", badges: [] },
|
{ name: "Persian Spiced Salmon", desc: "Cured salmon with saffron, lime, and fresh dill", price: "£14", badges: ["GF"] },
|
||||||
{ name: "Wild Mushroom Risotto", desc: "Arborio rice, porcini, chanterelle, shaved parmesan, herb oil", price: "$16", badges: ["V", "GF"] },
|
{ name: "Stuffed Vine Leaves", desc: "Dolma filled with rice, herbs, and pomegranate molasses", price: "£10", badges: ["V", "GF"] },
|
||||||
],
|
],
|
||||||
mains: [
|
mains: [
|
||||||
{ name: "Wagyu Beef Tenderloin", desc: "A5 wagyu, bone marrow butter, roasted root vegetables, red wine jus", price: "$62", badges: ["GF"] },
|
{ name: "Charcoal Lamb Kofta", desc: "Hand-spiced lamb skewers with saffron rice and grilled tomatoes", price: "£24", badges: ["GF"] },
|
||||||
{ name: "Pan-Roasted Halibut", desc: "Wild halibut, saffron beurre blanc, fennel, olive tapenade", price: "$44", badges: [] },
|
{ name: "Saffron Chicken Breast", desc: "Charcoal-grilled chicken with saffron yogurt sauce and pistachios", price: "£22", badges: ["GF"] },
|
||||||
{ name: "Rack of Lamb", desc: "Herb-crusted lamb, pommes dauphine, mint gremolata", price: "$52", badges: [] },
|
{ name: "Mediterranean Sea Bass", desc: "Grilled sea bass with herb butter, capers, and lemon", price: "£26", badges: ["GF"] },
|
||||||
{ name: "Truffle Tagliatelle", desc: "Hand-rolled pasta, black truffle cream, pecorino, fresh herbs", price: "$36", badges: ["V"] },
|
{ name: "Persian Lamb Shank", desc: "Slow-cooked lamb shank with dried lime and barberry rice", price: "£28", badges: ["GF"] },
|
||||||
{ name: "Roasted Duck Breast", desc: "Cherry-glazed duck, confit potatoes, wilted greens, port reduction", price: "$48", badges: ["GF"] },
|
{ name: "Vegetarian Mezze Platter", desc: "Selection of grilled vegetables, dips, and fresh bread", price: "£18", badges: ["V", "GF"] },
|
||||||
],
|
],
|
||||||
desserts: [
|
desserts: [
|
||||||
{ name: "Valrhona Chocolate Fondant", desc: "Warm dark chocolate cake, vanilla bean ice cream, gold leaf", price: "$16", badges: ["V"] },
|
{ name: "Persian Rose Panna Cotta", desc: "Creamy vanilla panna cotta with rose water, pistachios, and honey", price: "£12", badges: ["V"] },
|
||||||
{ name: "Crème Brûlée", desc: "Tahitian vanilla custard, caramelized sugar, shortbread", price: "$14", badges: ["GF"] },
|
{ name: "Saffron Crème Brûlée", desc: "Classic custard infused with premium saffron", price: "£11", badges: ["GF", "V"] },
|
||||||
{ name: "Tarte Tatin", desc: "Caramelized apple, puff pastry, calvados cream", price: "$15", badges: [] },
|
{ name: "Pistachio Baklava", desc: "Layers of flaky pastry filled with pistachios and honey syrup", price: "£10", badges: ["V"] },
|
||||||
{ name: "Lemon Posset", desc: "Set cream, fresh berries, lavender meringue", price: "$13", badges: ["V", "GF"] },
|
{ name: "Fig & Walnut Tart", desc: "Fresh figs with walnuts on a buttery pastry base", price: "£11", badges: ["V"] },
|
||||||
],
|
],
|
||||||
drinks: [
|
drinks: [
|
||||||
{ name: "Signature Old Fashioned", desc: "Woodford Reserve, demerara, Angostura, flamed orange peel", price: "$19", badges: [] },
|
{ name: "Saffron Gin Sour", desc: "Saffron-infused gin, lemon juice, and egg white", price: "£14", badges: [] },
|
||||||
{ name: "Garden Spritz", desc: "Elderflower, prosecco, cucumber, fresh mint, soda", price: "$16", badges: [] },
|
{ name: "Persian Sunrise", desc: "Pomegranate juice, orange blossom water, and prosecco", price: "£12", badges: [] },
|
||||||
{ name: "Sommelier Wine Pairing", desc: "Five-course curated wine flight matched to your meal", price: "$65", badges: [] },
|
{ name: "Cardamom Espresso Martini", desc: "Vodka, espresso, and cardamom syrup", price: "£13", badges: [] },
|
||||||
{ name: "Espresso Martini", desc: "Ketel One, fresh espresso, Kahlúa, vanilla", price: "$18", badges: [] },
|
{ name: "Rose & Raspberry Spritz", desc: "Rose water, raspberries, elderflower, and soda", price: "£11", badges: [] },
|
||||||
],
|
],
|
||||||
};
|
};
|
||||||
|
|
||||||
const tastingCourses = [
|
const tastingCourses = [
|
||||||
"Amuse-bouche · Seasonal selection", "Seared Scallops · Cauliflower, truffle", "Wagyu Beef · Root vegetables, red wine jus", "Artisan Cheese · Honeycomb, fig compote", "Chocolate Fondant · Vanilla ice cream"
|
"Mezze Selection · Hummus, dolma, grilled vegetables", "Charcoal Salmon · Saffron beurre blanc, fennel", "Lamb Kofta · Saffron rice, grilled tomatoes, mint yogurt", "Persian Cheese · Honeycomb, walnuts, pomegranate", "Rose Panna Cotta · Pistachios and honey"
|
||||||
];
|
];
|
||||||
|
|
||||||
const renderItems = (items: { name: string; desc: string; price: string; badges: string[] }[]) =>
|
const renderItems = (items: { name: string; desc: string; price: string; badges: string[] }[]) =>
|
||||||
@@ -69,7 +69,7 @@ export default function MenuPage() {
|
|||||||
.map((p: any) => ({
|
.map((p: any) => ({
|
||||||
name: p.name,
|
name: p.name,
|
||||||
desc: p.description || "",
|
desc: p.description || "",
|
||||||
price: typeof p.priceCents === "number" ? "$" + (p.priceCents / 100).toFixed(0) : (p.price || ""),
|
price: typeof p.priceCents === "number" ? "£" + (p.priceCents / 100).toFixed(0) : (p.price || ""),
|
||||||
badges: Array.isArray(p.badges) ? p.badges : [],
|
badges: Array.isArray(p.badges) ? p.badges : [],
|
||||||
}))
|
}))
|
||||||
: [];
|
: [];
|
||||||
@@ -142,8 +142,8 @@ export default function MenuPage() {
|
|||||||
{tastingCourses.map((c, i) => (
|
{tastingCourses.map((c, i) => (
|
||||||
<div key={i} className="py-2 text-sm font-serif text-muted-foreground border-b border-dotted border-stone-200 dark:border-stone-800 last:border-0">{c}</div>
|
<div key={i} className="py-2 text-sm font-serif text-muted-foreground border-b border-dotted border-stone-200 dark:border-stone-800 last:border-0">{c}</div>
|
||||||
))}
|
))}
|
||||||
<div className="pt-4 text-2xl font-serif font-bold text-amber-700 dark:text-amber-400">$135 per person</div>
|
<div className="pt-4 text-2xl font-serif font-bold text-amber-700 dark:text-amber-400">£55 per person</div>
|
||||||
<p className="text-xs text-muted-foreground">Wine pairing available for $85</p>
|
<p className="text-xs text-muted-foreground">Cocktail pairing available for £35</p>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
<p className="mt-6 text-sm text-muted-foreground italic flex items-center justify-center gap-2">
|
<p className="mt-6 text-sm text-muted-foreground italic flex items-center justify-center gap-2">
|
||||||
|
|||||||
@@ -47,7 +47,7 @@ export default function ReservationsPage() {
|
|||||||
<span>—</span><Wine className="h-4 w-4" /><span>—</span>
|
<span>—</span><Wine className="h-4 w-4" /><span>—</span>
|
||||||
</div>
|
</div>
|
||||||
<h1 className="text-4xl sm:text-5xl font-serif tracking-wide text-white animate-fade-up">Make a Reservation</h1>
|
<h1 className="text-4xl sm:text-5xl font-serif tracking-wide text-white animate-fade-up">Make a Reservation</h1>
|
||||||
<p className="mt-4 text-stone-400 animate-fade-up">Join us for an unforgettable evening of exceptional cuisine</p>
|
<p className="mt-4 text-stone-400 animate-fade-up">Join us for charcoal-grilled mezze and saffron cocktails in the heart of Shoreditch</p>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
@@ -122,9 +122,9 @@ export default function ReservationsPage() {
|
|||||||
{/* ── SIDEBAR INFO ── */}
|
{/* ── SIDEBAR INFO ── */}
|
||||||
<div className="space-y-8">
|
<div className="space-y-8">
|
||||||
{[
|
{[
|
||||||
{ icon: Clock, title: "Opening Hours", lines: ["Tuesday – Thursday: 6:00 PM – 10:00 PM", "Friday – Saturday: 6:00 PM – 11:00 PM", "Sunday – Monday: Closed"] },
|
{ icon: Clock, title: "Opening Hours", lines: ["Tuesday – Thursday: 5:00 PM – 10:00 PM", "Friday – Saturday: 5:00 PM – 11:00 PM", "Sunday: 5:00 PM – 10:00 PM", "Monday: Closed"] },
|
||||||
{ icon: Shirt, title: "Dress Code", lines: ["Smart casual. Jackets preferred for gentlemen in the main dining room."] },
|
{ icon: Shirt, title: "Dress Code", lines: ["Casual chic. We want you to feel comfortable while enjoying your meal."] },
|
||||||
{ icon: Car, title: "Parking", lines: ["Complimentary valet parking available. Underground garage on Golden Lane."] },
|
{ icon: Car, title: "Location", lines: ["127 Shoreditch High Street, London E1 6JE", "2 minutes walk from Shoreditch High Street Overground", "8 minutes from Old Street station"] },
|
||||||
{ icon: AlertCircle, title: "Cancellation Policy", lines: ["Cancellations accepted up to 24 hours before your reservation. Late cancellations may incur a fee."] },
|
{ icon: AlertCircle, title: "Cancellation Policy", lines: ["Cancellations accepted up to 24 hours before your reservation. Late cancellations may incur a fee."] },
|
||||||
].map((info, i) => (
|
].map((info, i) => (
|
||||||
<div key={i}>
|
<div key={i}>
|
||||||
|
|||||||
Reference in New Issue
Block a user