15 Commits

Author SHA1 Message Date
jyswee 328f429ea0 bake build infra: nginx.conf (deterministic /ship) 2026-08-21 20:41:18 +00:00
jyswee bca343d7c8 bake build infra: Dockerfile (deterministic /ship) 2026-08-21 20:41:17 +00:00
jyswee 1f9ef169c8 bake build infra: src/components/ui/sonner.tsx (deterministic /ship) 2026-08-21 20:41:16 +00:00
jyswee 16e66c1c95 bake build infra: src/index.css (deterministic /ship) 2026-08-21 20:41:15 +00:00
jyswee bb2adc762d bake build infra: src/main.tsx (deterministic /ship) 2026-08-21 20:41:14 +00:00
jyswee 029e2aa074 bake build infra: .gitignore (deterministic /ship) 2026-08-21 20:41:14 +00:00
jyswee 7b1292dfa1 bake build infra: index.html (deterministic /ship) 2026-08-21 20:41:12 +00:00
jyswee d57534480b bake build infra: postcss.config.js (deterministic /ship) 2026-08-21 20:41:11 +00:00
jyswee 532acc5eaf bake build infra: tailwind.config.ts (deterministic /ship) 2026-08-21 20:41:10 +00:00
jyswee 098d1dd250 bake build infra: tsconfig.node.json (deterministic /ship) 2026-08-21 20:41:09 +00:00
jyswee b711454af5 bake build infra: tsconfig.json (deterministic /ship) 2026-08-21 20:41:08 +00:00
jyswee b7d9cd767d bake build infra: vite.config.ts (deterministic /ship) 2026-08-21 20:41:07 +00:00
jyswee bb990079b4 bake build infra: package.json (deterministic /ship) 2026-08-21 20:41:06 +00:00
joewee cf8f85aac8 hctx: Rebrand complete 2026-08-21 14:35:15 +00:00
joewee 923d105fab Rebrand to Saffron Table - Persian-Mediterranean restaurant in Shoreditch
- Updated brand name from Aurum to Saffron Table across all pages
- Changed location from Mayfair to Shoreditch (127 Shoreditch High Street)
- Created Persian-Mediterranean menu with GBP prices
- Updated hero tagline: "Charcoal-Grilled Mezze & Saffron Cocktails"
- New menu items: hummus, lamb kofta, saffron salmon, Persian desserts
- Saffron cocktail program added
- Updated chef bio: Chef Yasmin Kashani
- New testimonials reflecting Persian-Mediterranean cuisine
- Updated opening hours: Tue-Sun 5pm-11pm
- Updated events and private dining rooms
- Modern stats: 5+ years, 15 mezze dishes, 8 saffron cocktails

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-08-21 14:35:05 +00:00
78 changed files with 515 additions and 6205 deletions
+20
View File
@@ -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
+4
View File
@@ -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"
+6
View File
@@ -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"
+169
View File
@@ -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 46 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`)
+32 -2
View File
@@ -1,3 +1,33 @@
# template-restaurant
# saffron-table
Template: restaurant — Aurum
Website for Saffron Table — a modern PersianMediterranean 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
View File
@@ -4,7 +4,7 @@
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Urestaurant — vibeasite template</title>
<title>Saffron Table — PersianMediterranean, Shoreditch</title>
</head>
<body>
<div id="root"></div>
+8 -13
View File
@@ -1,5 +1,5 @@
{
"name": "template-restaurant",
"name": "vite-react-app",
"private": true,
"version": "0.0.0",
"type": "module",
@@ -9,7 +9,12 @@
"preview": "vite preview"
},
"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-alert-dialog": "^1.1.14",
"@radix-ui/react-aspect-ratio": "^1.1.7",
@@ -37,30 +42,20 @@
"@radix-ui/react-toggle": "^1.1.9",
"@radix-ui/react-toggle-group": "^1.1.10",
"@radix-ui/react-tooltip": "^1.2.7",
"@tanstack/react-query": "^5.83.0",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"cmdk": "^1.1.1",
"date-fns": "^3.6.0",
"embla-carousel-react": "^8.6.0",
"input-otp": "^1.4.2",
"lucide-react": "^0.462.0",
"next-themes": "^0.3.0",
"react": "^18.3.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-router-dom": "^6.30.1",
"recharts": "^2.15.4",
"sonner": "^1.7.4",
"tailwind-merge": "^2.6.0",
"tailwindcss-animate": "^1.0.7",
"vaul": "^0.9.9",
"zod": "^3.25.76"
"vaul": "^0.9.9"
},
"devDependencies": {
"@types/node": "^22.16.5",
"@types/react": "^18.3.23",
"@types/react-dom": "^18.3.7",
"@vitejs/plugin-react-swc": "^3.11.0",
-195
View File
@@ -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>
);
}
-214
View File
@@ -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>
);
}
+6 -6
View File
@@ -8,12 +8,12 @@ export default function Footer() {
<div className="mx-auto max-w-7xl px-6">
<div className="grid md:grid-cols-2 gap-12">
<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="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-center gap-3"><Clock className="h-4 w-4 text-amber-500" /><span>TueSat 6: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"><Mail className="h-4 w-4 text-amber-500" /><span>reservations@aurum.com</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>TueSun 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 7490 8956</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 className="flex flex-col items-start md:items-end justify-between">
@@ -27,7 +27,7 @@ export default function Footer() {
</Button>
</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>
</footer>
);
+1 -1
View File
@@ -25,7 +25,7 @@ export default function Header() {
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"}`}>
<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">
{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>
-124
View File
@@ -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>
);
}
-125
View File
@@ -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>
);
}
-19
View File
@@ -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>
);
}
-52
View File
@@ -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 };
-104
View File
@@ -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,
};
-43
View File
@@ -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 };
-5
View File
@@ -1,5 +0,0 @@
import * as AspectRatioPrimitive from "@radix-ui/react-aspect-ratio";
const AspectRatio = AspectRatioPrimitive.Root;
export { AspectRatio };
-38
View File
@@ -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
View File
@@ -1,29 +1,33 @@
import * as React from "react";
import { cva, type VariantProps } from "class-variance-authority";
import { cn } from "@/lib/utils";
const badgeVariants = cva(
"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",
{
variants: {
variant: {
default: "border-transparent bg-primary text-primary-foreground hover:bg-primary/80",
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",
},
},
);
const badgeStyles: Record<string, string> = {
default: "bg-indigo-600 text-white",
secondary: "bg-slate-100 text-slate-900",
outline: "border border-slate-300 text-slate-900",
destructive: "bg-red-600 text-white",
success: "bg-emerald-100 text-emerald-700",
};
export interface BadgeProps extends React.HTMLAttributes<HTMLDivElement>, VariantProps<typeof badgeVariants> {}
function Badge({ className, variant, ...props }: BadgeProps) {
return <div className={cn(badgeVariants({ variant }), className)} {...props} />;
export interface BadgeProps extends React.HTMLAttributes<HTMLSpanElement> {
variant?: keyof typeof badgeStyles;
}
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;
-90
View File
@@ -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,
};
+43 -41
View File
@@ -1,51 +1,53 @@
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";
const buttonVariants = cva(
"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",
{
variants: {
variant: {
default: "bg-primary text-primary-foreground hover:bg-primary/90 hover:shadow-md",
destructive: "bg-destructive text-destructive-foreground hover:bg-destructive/90",
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",
},
},
);
const variantClasses: Record<string, string> = {
default: "bg-indigo-600 text-white hover:bg-indigo-700",
secondary: "bg-slate-100 text-slate-900 hover:bg-slate-200",
outline: "border border-slate-300 bg-transparent hover:bg-slate-50 text-slate-900",
ghost: "bg-transparent hover:bg-slate-100 text-slate-900",
destructive: "bg-red-600 text-white hover:bg-red-700",
link: "bg-transparent underline-offset-4 hover:underline text-indigo-600",
};
export interface ButtonProps
extends React.ButtonHTMLAttributes<HTMLButtonElement>,
VariantProps<typeof buttonVariants> {
const sizeClasses: Record<string, string> = {
default: "h-10 px-4 py-2 text-sm",
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;
}
const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
({ className, variant, size, asChild = false, ...props }, ref) => {
const Comp = asChild ? Slot : "button";
return <Comp className={cn(buttonVariants({ variant, size, className }))} ref={ref} {...props} />;
},
export const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
({ className, variant = "default", size = "default", asChild, children, ...props }, ref) => {
const classes = cn(
"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";
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;
-54
View File
@@ -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
View File
@@ -1,43 +1,31 @@
import * as React from "react";
import { cn } from "@/lib/utils";
const Card = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(({ className, ...props }, ref) => (
<div ref={ref} className={cn("rounded-lg border bg-card text-card-foreground shadow-sm", className)} {...props} />
));
export const Card = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(
({ 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";
const CardHeader = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(
({ className, ...props }, ref) => (
<div ref={ref} className={cn("flex flex-col space-y-1.5 p-6", className)} {...props} />
),
export const CardHeader = ({ className, ...props }: React.HTMLAttributes<HTMLDivElement>) => (
<div className={cn("flex flex-col space-y-1.5 p-6", className)} {...props} />
);
CardHeader.displayName = "CardHeader";
const CardTitle = React.forwardRef<HTMLParagraphElement, React.HTMLAttributes<HTMLHeadingElement>>(
({ className, ...props }, ref) => (
<h3 ref={ref} className={cn("text-2xl font-semibold leading-none tracking-tight", className)} {...props} />
),
export const CardTitle = ({ className, ...props }: React.HTMLAttributes<HTMLHeadingElement>) => (
<h3 className={cn("text-lg font-semibold leading-none tracking-tight", className)} {...props} />
);
CardTitle.displayName = "CardTitle";
const CardDescription = React.forwardRef<HTMLParagraphElement, React.HTMLAttributes<HTMLParagraphElement>>(
({ className, ...props }, ref) => (
<p ref={ref} className={cn("text-sm text-muted-foreground", className)} {...props} />
),
export const CardDescription = ({ className, ...props }: React.HTMLAttributes<HTMLParagraphElement>) => (
<p className={cn("text-sm text-slate-500", className)} {...props} />
);
CardDescription.displayName = "CardDescription";
const CardContent = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(
({ className, ...props }, ref) => <div ref={ref} className={cn("p-6 pt-0", className)} {...props} />,
export const CardContent = ({ className, ...props }: React.HTMLAttributes<HTMLDivElement>) => (
<div className={cn("p-6 pt-0", className)} {...props} />
);
CardContent.displayName = "CardContent";
const CardFooter = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(
({ className, ...props }, ref) => (
<div ref={ref} className={cn("flex items-center p-6 pt-0", className)} {...props} />
),
export const CardFooter = ({ className, ...props }: React.HTMLAttributes<HTMLDivElement>) => (
<div className={cn("flex items-center p-6 pt-0", className)} {...props} />
);
CardFooter.displayName = "CardFooter";
export { Card, CardHeader, CardFooter, CardTitle, CardDescription, CardContent };
export default Card;
-224
View File
@@ -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 };
-303
View File
@@ -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 };
-26
View File
@@ -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 };
-9
View File
@@ -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 };
-132
View File
@@ -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,
};
-178
View File
@@ -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,
};
-95
View File
@@ -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,
};
-87
View File
@@ -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,
};
-179
View File
@@ -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,
};
-129
View File
@@ -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 };
-27
View File
@@ -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 };
-61
View File
@@ -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 };
+7 -10
View File
@@ -1,22 +1,19 @@
import * as React from "react";
import { cn } from "@/lib/utils";
const Input = React.forwardRef<HTMLInputElement, React.ComponentProps<"input">>(
({ className, type, ...props }, ref) => {
return (
export const Input = React.forwardRef<HTMLInputElement, React.InputHTMLAttributes<HTMLInputElement>>(
({ className, type = "text", ...props }, ref) => (
<input
ref={ref}
type={type}
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",
className,
"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
)}
ref={ref}
{...props}
/>
);
},
)
);
Input.displayName = "Input";
export { Input };
export default Input;
-17
View File
@@ -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 };
-207
View File
@@ -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,
};
-120
View File
@@ -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,
};
-81
View File
@@ -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,
};
-29
View File
@@ -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 };
-23
View File
@@ -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 };
-36
View File
@@ -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 };
-37
View File
@@ -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 };
-38
View File
@@ -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 };
-143
View File
@@ -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,
};
+9 -14
View File
@@ -1,20 +1,15 @@
import * as React from "react";
import * as SeparatorPrimitive from "@radix-ui/react-separator";
import { cn } from "@/lib/utils";
const Separator = React.forwardRef<
React.ElementRef<typeof SeparatorPrimitive.Root>,
React.ComponentPropsWithoutRef<typeof SeparatorPrimitive.Root>
>(({ className, orientation = "horizontal", decorative = true, ...props }, ref) => (
<SeparatorPrimitive.Root
ref={ref}
decorative={decorative}
orientation={orientation}
className={cn("shrink-0 bg-border", orientation === "horizontal" ? "h-[1px] w-full" : "h-full w-[1px]", className)}
export const Separator = ({ className, orientation = "horizontal", ...props }: React.HTMLAttributes<HTMLDivElement> & { orientation?: "horizontal" | "vertical" }) => (
<div
className={cn(
"shrink-0 bg-slate-200",
orientation === "horizontal" ? "h-px w-full" : "h-full w-px",
className
)}
{...props}
/>
));
Separator.displayName = SeparatorPrimitive.Root.displayName;
);
export { Separator };
export default Separator;
+40 -97
View File
@@ -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 { 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;
const SheetOverlay = React.forwardRef<
React.ElementRef<typeof SheetPrimitive.Overlay>,
React.ComponentPropsWithoutRef<typeof SheetPrimitive.Overlay>
>(({ className, ...props }, ref) => (
<SheetPrimitive.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}
/>
));
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}>
export function SheetContent({ side = "right", className, children, ...props }: React.HTMLAttributes<HTMLDivElement> & { side?: "left" | "right" | "top" | "bottom" }) {
const ctx = React.useContext(SheetContext);
if (!ctx.open) return null;
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";
return (
<div className="fixed inset-0 z-50">
<div className="absolute inset-0 bg-black/40" onClick={() => ctx.setOpen(false)} />
<div className={cn("absolute bg-white p-6 shadow-xl", pos, className)} {...props}>
<button type="button" className="absolute right-4 top-4 text-slate-500" onClick={() => ctx.setOpen(false)}></button>
{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">
<X className="h-4 w-4" />
<span className="sr-only">Close</span>
</SheetPrimitive.Close>
</SheetPrimitive.Content>
</SheetPortal>
),
</div>
</div>
);
SheetContent.displayName = SheetPrimitive.Content.displayName;
}
const SheetHeader = ({ className, ...props }: React.HTMLAttributes<HTMLDivElement>) => (
<div className={cn("flex flex-col space-y-2 text-center sm:text-left", className)} {...props} />
);
SheetHeader.displayName = "SheetHeader";
export function SheetHeader({ className, ...props }: React.HTMLAttributes<HTMLDivElement>) {
return <div className={cn("flex flex-col space-y-2", className)} {...props} />;
}
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>) => (
<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,
};
export default Sheet;
-637
View File
@@ -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,
};
-7
View File
@@ -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 };
-23
View File
@@ -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 };
-27
View File
@@ -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 };
-72
View File
@@ -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 };
+37 -44
View File
@@ -1,53 +1,46 @@
import * as React from "react";
import * as TabsPrimitive from "@radix-ui/react-tabs";
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<
React.ElementRef<typeof TabsPrimitive.List>,
React.ComponentPropsWithoutRef<typeof TabsPrimitive.List>
>(({ className, ...props }, ref) => (
<TabsPrimitive.List
ref={ref}
export function Tabs({ defaultValue = "", value: controlled, onValueChange, className, children, ...props }: React.HTMLAttributes<HTMLDivElement> & { defaultValue?: string; value?: string; onValueChange?: (v: string) => void }) {
const [internal, setInternal] = React.useState(defaultValue);
const value = controlled ?? internal;
const setValue = (v: string) => { setInternal(v); onValueChange?.(v); };
return (
<TabsContext.Provider value={{ value, setValue }}>
<div className={cn("w-full", className)} {...props}>{children}</div>
</TabsContext.Provider>
);
}
export function TabsList({ className, ...props }: React.HTMLAttributes<HTMLDivElement>) {
return <div className={cn("inline-flex items-center justify-center rounded-lg bg-slate-100 p-1", className)} {...props} />;
}
export function TabsTrigger({ value, className, children, ...props }: React.ButtonHTMLAttributes<HTMLButtonElement> & { value: string }) {
const ctx = React.useContext(TabsContext);
const active = ctx.value === value;
return (
<button
type="button"
onClick={() => ctx.setValue(value)}
className={cn(
"inline-flex h-10 items-center justify-center rounded-md bg-muted p-1 text-muted-foreground",
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",
className
)}
{...props}
/>
));
TabsList.displayName = TabsPrimitive.List.displayName;
>
{children}
</button>
);
}
const TabsTrigger = React.forwardRef<
React.ElementRef<typeof TabsPrimitive.Trigger>,
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;
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>;
}
const TabsContent = React.forwardRef<
React.ElementRef<typeof TabsPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof TabsPrimitive.Content>
>(({ className, ...props }, ref) => (
<TabsPrimitive.Content
ref={ref}
className={cn(
"mt-2 ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2",
className,
)}
{...props}
/>
));
TabsContent.displayName = TabsPrimitive.Content.displayName;
export { Tabs, TabsList, TabsTrigger, TabsContent };
export default Tabs;
+8 -11
View File
@@ -1,21 +1,18 @@
import * as React from "react";
import { cn } from "@/lib/utils";
export interface TextareaProps extends React.TextareaHTMLAttributes<HTMLTextAreaElement> {}
const Textarea = React.forwardRef<HTMLTextAreaElement, TextareaProps>(({ className, ...props }, ref) => {
return (
export const Textarea = React.forwardRef<HTMLTextAreaElement, React.TextareaHTMLAttributes<HTMLTextAreaElement>>(
({ className, ...props }, ref) => (
<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}
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}
/>
)
);
});
Textarea.displayName = "Textarea";
export { Textarea };
export default Textarea;
-111
View File
@@ -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,
};
-24
View File
@@ -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>
);
}
-49
View File
@@ -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 };
-37
View File
@@ -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 };
-28
View File
@@ -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 };
-3
View File
@@ -1,3 +0,0 @@
import { useToast, toast } from "@/hooks/use-toast";
export { useToast, toast };
-234
View File
@@ -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}</>;
}
-292
View File
@@ -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 };
}
-19
View File
@@ -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;
}
-161
View File
@@ -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 };
}
-186
View File
@@ -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 };
-330
View File
@@ -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 };
}
-21
View File
@@ -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;
-121
View File
@@ -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();
-89
View File
@@ -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";
-115
View File
@@ -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
View File
@@ -1,6 +1,3 @@
import { type ClassValue, clsx } from "clsx";
import { twMerge } from "tailwind-merge";
export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs));
export function cn(...inputs: Array<string | number | null | undefined | false>) {
return inputs.filter(Boolean).join(" ");
}
+19 -19
View File
@@ -7,23 +7,23 @@ import Header from "@/components/Header";
import Footer from "@/components/Footer";
const milestones = [
{ year: "1987", title: "The Beginning", desc: "Chef Bellini opens Aurum with just 20 seats in Mayfair." },
{ year: "1995", title: "First Michelin Star", desc: "Aurum earns its first Michelin star, cementing its place among Londons finest." },
{ year: "2008", title: "Second Star & Expansion", desc: "A second star and a major renovation doubles capacity with private dining rooms." },
{ year: "2024", title: "Worlds 50 Best", desc: "Named #32 on the Worlds 50 Best Restaurants list for the third time." },
{ year: "2019", title: "The Beginning", desc: "Saffron Table opens its doors in Shoreditch with a vision to bring authentic Persian flavours to London." },
{ year: "2021", title: "Best New Restaurant", desc: "Named Londons Best New Restaurant by Time Out for our innovative mezze concept." },
{ year: "2023", title: "Charcoal Grill Expansion", desc: "Our signature charcoal grill becomes the heart of the kitchen, elevating every dish." },
{ year: "2025", title: "Cocktail Recognition", desc: "Our saffron cocktail program wins Best Bar Program from Restaurant Magazine." },
];
const values = [
{ icon: Leaf, title: "Ingredients", desc: "We source the finest seasonal ingredients from trusted local farms and sustainable producers worldwide." },
{ icon: Star, title: "Technique", desc: "Classical training meets modern innovation. Every dish reflects decades of accumulated mastery and creativity." },
{ icon: Heart, title: "Hospitality", desc: "Every guest is family. Our service team ensures each visit is warm, personal, and truly memorable." },
{ icon: Leaf, title: "Ingredients", desc: "We source premium saffron directly from Iran and seasonal produce from local British farms." },
{ 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. We bring the warmth of Persian hospitality to Shoreditch." },
];
const awards = [
{ name: "2 Michelin Stars", year: "2008Present" },
{ name: "Worlds 50 Best Restaurants", year: "2022, 2023, 2024" },
{ name: "Best Fine Dining London", year: "2024" },
{ name: "Sustainability Award", year: "2023" },
{ name: "Best New Restaurant London", year: "2021" },
{ name: "Best Bar Program", year: "2025" },
{ name: "Sustainable Restaurant Award", year: "2024" },
{ name: "Top 100 UK Restaurants", year: "2023, 2024" },
];
export default function AboutPage() {
@@ -44,9 +44,9 @@ export default function AboutPage() {
<section className="py-24">
<div className="mx-auto max-w-7xl px-6 grid lg:grid-cols-2 gap-16 items-center">
<div>
<h2 className="text-3xl font-serif tracking-wide mb-6 animate-fade-up">A Legacy of Excellence</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">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>
<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">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">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 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">
@@ -71,10 +71,10 @@ export default function AboutPage() {
</div>
<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>
<h2 className="text-3xl font-serif tracking-wide mb-2">Chef Marcus Bellini</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">Under his leadership, Aurum has earned two Michelin stars and been named among the Worlds 50 Best Restaurants three times.</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>
<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 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 her leadership, Saffron Table has become known for Londons 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">"Food is memory. Every dish should taste like coming home."</blockquote>
</div>
</div>
</section>
@@ -134,7 +134,7 @@ export default function AboutPage() {
{/* ── CTA ── */}
<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">
<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>
<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>
+7 -7
View File
@@ -10,15 +10,15 @@ import Header from "@/components/Header";
import Footer from "@/components/Footer";
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 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 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 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 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 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 = [
{ 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: "Chefs Table Experience", desc: "An exclusive 8-seat experience at the kitchen counter with a bespoke tasting menu narrated by the chef.", 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: "Persian Wine & Mezze Nights", desc: "Monthly five-course mezze dinner paired with Persian wines. An evening of discovery and indulgence.", badge: "Monthly" },
{ 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: "Saffron Cocktail Lab", desc: "An exclusive workshop learning to create signature saffron cocktails with our head bartender.", badge: "Seasonal" },
];
export default function EventsPage() {
@@ -86,7 +86,7 @@ export default function EventsPage() {
<div className="mx-auto max-w-3xl px-6 text-center">
<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>
<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]">
Request a Quote <ArrowRight className="ml-2 h-4 w-4" />
</Button>
+16 -16
View File
@@ -9,15 +9,15 @@ import Header from "@/components/Header";
import Footer from "@/components/Footer";
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: "Seared Scallops", desc: "Pan-seared diver scallops, cauliflower purée, truffle vinaigrette", price: "$24", 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: "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: "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: "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 = [
{ 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: "Sophie Laurent", text: "Impeccable service and the most beautiful presentation I have ever seen. The wine pairing was exquisite.", 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: "Maya & Tariq", text: "The charcoal-grilled mezze took us straight back to Tehran. Authentic flavours with a modern London twist.", source: "Google Reviews" },
{ name: "Elena Rossi", text: "Best saffron cocktails in Shoreditch! The Persian-Mediterranean fusion is absolutely divine.", source: "TripAdvisor" },
{ 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() {
@@ -39,10 +39,10 @@ export default function IndexPage() {
<span></span><span></span><span></span>
</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">
Est. 1987
Est. 2019
</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>
<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>
<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 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">
<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>
@@ -95,19 +95,19 @@ export default function IndexPage() {
<span></span><Star className="h-4 w-4" /><span></span>
</div>
<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="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>
<div className="text-center">
<div className="text-3xl font-serif font-bold text-amber-700 dark:text-amber-400">2</div>
<div className="text-sm text-muted-foreground mt-1">Michelin Stars</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">Mezze Dishes</div>
</div>
<div className="text-center">
<div className="text-3xl font-serif font-bold text-amber-700 dark:text-amber-400">400+</div>
<div className="text-sm text-muted-foreground mt-1">Wines</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">Saffron Cocktails</div>
</div>
</div>
<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="text-center mb-16 animate-fade-up">
<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 className="grid grid-cols-2 lg:grid-cols-3 gap-4">
{[0, 1, 2, 3, 4, 5].map((i) => (
+21 -21
View File
@@ -11,34 +11,34 @@ import { useSaas } from "@/hooks/use-tygarun";
const menuItems = {
starters: [
{ name: "Burrata & Heirloom Tomato", desc: "Creamy burrata, vine-ripened tomatoes, basil oil, aged balsamic", price: "$18", badges: ["V"] },
{ name: "Seared Scallops", desc: "Pan-seared diver scallops, cauliflower purée, truffle vinaigrette", price: "$24", badges: ["GF"] },
{ name: "Duck Confit Terrine", desc: "Slow-cooked duck leg, pistachio, cornichon, sourdough toast", price: "$19", badges: [] },
{ name: "Wild Mushroom Risotto", desc: "Arborio rice, porcini, chanterelle, shaved parmesan, herb oil", price: "$16", badges: ["V", "GF"] },
{ name: "Warm Hummus & Charcoal Pita", desc: "Creamy hummus topped with saffron oil, paprika, and pine nuts", price: "£10", badges: ["V", "GF"] },
{ name: "Grilled Halloumi & Fig", desc: "Charcoal-grilled Cypriot halloumi with fresh figs and honey drizzle", price: "£12", badges: ["V"] },
{ name: "Persian Spiced Salmon", desc: "Cured salmon with saffron, lime, and fresh dill", price: "£14", badges: ["GF"] },
{ name: "Stuffed Vine Leaves", desc: "Dolma filled with rice, herbs, and pomegranate molasses", price: "£10", badges: ["V", "GF"] },
],
mains: [
{ name: "Wagyu Beef Tenderloin", desc: "A5 wagyu, bone marrow butter, roasted root vegetables, red wine jus", price: "$62", badges: ["GF"] },
{ name: "Pan-Roasted Halibut", desc: "Wild halibut, saffron beurre blanc, fennel, olive tapenade", price: "$44", badges: [] },
{ name: "Rack of Lamb", desc: "Herb-crusted lamb, pommes dauphine, mint gremolata", price: "$52", badges: [] },
{ name: "Truffle Tagliatelle", desc: "Hand-rolled pasta, black truffle cream, pecorino, fresh herbs", price: "$36", badges: ["V"] },
{ name: "Roasted Duck Breast", desc: "Cherry-glazed duck, confit potatoes, wilted greens, port reduction", price: "$48", badges: ["GF"] },
{ name: "Charcoal Lamb Kofta", desc: "Hand-spiced lamb skewers with saffron rice and grilled tomatoes", price: "£24", badges: ["GF"] },
{ name: "Saffron Chicken Breast", desc: "Charcoal-grilled chicken with saffron yogurt sauce and pistachios", price: "£22", badges: ["GF"] },
{ name: "Mediterranean Sea Bass", desc: "Grilled sea bass with herb butter, capers, and lemon", price: "£26", badges: ["GF"] },
{ name: "Persian Lamb Shank", desc: "Slow-cooked lamb shank with dried lime and barberry rice", price: "£28", badges: ["GF"] },
{ name: "Vegetarian Mezze Platter", desc: "Selection of grilled vegetables, dips, and fresh bread", price: "£18", badges: ["V", "GF"] },
],
desserts: [
{ name: "Valrhona Chocolate Fondant", desc: "Warm dark chocolate cake, vanilla bean ice cream, gold leaf", price: "$16", badges: ["V"] },
{ name: "Crème Brûlée", desc: "Tahitian vanilla custard, caramelized sugar, shortbread", price: "$14", badges: ["GF"] },
{ name: "Tarte Tatin", desc: "Caramelized apple, puff pastry, calvados cream", price: "$15", badges: [] },
{ name: "Lemon Posset", desc: "Set cream, fresh berries, lavender meringue", price: "$13", badges: ["V", "GF"] },
{ name: "Persian Rose Panna Cotta", desc: "Creamy vanilla panna cotta with rose water, pistachios, and honey", price: "£12", badges: ["V"] },
{ name: "Saffron Crème Brûlée", desc: "Classic custard infused with premium saffron", price: "£11", badges: ["GF", "V"] },
{ name: "Pistachio Baklava", desc: "Layers of flaky pastry filled with pistachios and honey syrup", price: "£10", badges: ["V"] },
{ name: "Fig & Walnut Tart", desc: "Fresh figs with walnuts on a buttery pastry base", price: "£11", badges: ["V"] },
],
drinks: [
{ name: "Signature Old Fashioned", desc: "Woodford Reserve, demerara, Angostura, flamed orange peel", price: "$19", badges: [] },
{ name: "Garden Spritz", desc: "Elderflower, prosecco, cucumber, fresh mint, soda", price: "$16", badges: [] },
{ name: "Sommelier Wine Pairing", desc: "Five-course curated wine flight matched to your meal", price: "$65", badges: [] },
{ name: "Espresso Martini", desc: "Ketel One, fresh espresso, Kahlúa, vanilla", price: "$18", badges: [] },
{ name: "Saffron Gin Sour", desc: "Saffron-infused gin, lemon juice, and egg white", price: "£14", badges: [] },
{ name: "Persian Sunrise", desc: "Pomegranate juice, orange blossom water, and prosecco", price: "£12", badges: [] },
{ name: "Cardamom Espresso Martini", desc: "Vodka, espresso, and cardamom syrup", price: "£13", badges: [] },
{ name: "Rose & Raspberry Spritz", desc: "Rose water, raspberries, elderflower, and soda", price: "£11", badges: [] },
],
};
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[] }[]) =>
@@ -69,7 +69,7 @@ export default function MenuPage() {
.map((p: any) => ({
name: p.name,
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 : [],
}))
: [];
@@ -142,8 +142,8 @@ export default function MenuPage() {
{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 className="pt-4 text-2xl font-serif font-bold text-amber-700 dark:text-amber-400">$135 per person</div>
<p className="text-xs text-muted-foreground">Wine pairing available for $85</p>
<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">Cocktail pairing available for £35</p>
</CardContent>
</Card>
<p className="mt-6 text-sm text-muted-foreground italic flex items-center justify-center gap-2">
+4 -4
View File
@@ -47,7 +47,7 @@ export default function ReservationsPage() {
<span></span><Wine className="h-4 w-4" /><span></span>
</div>
<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>
</section>
@@ -122,9 +122,9 @@ export default function ReservationsPage() {
{/* ── SIDEBAR INFO ── */}
<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: Shirt, title: "Dress Code", lines: ["Smart casual. Jackets preferred for gentlemen in the main dining room."] },
{ icon: Car, title: "Parking", lines: ["Complimentary valet parking available. Underground garage on Golden Lane."] },
{ 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: ["Casual chic. We want you to feel comfortable while enjoying your meal."] },
{ 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."] },
].map((info, i) => (
<div key={i}>