feat(phase2): planned badge, Cmd+K palette, per-project dashboard cards
- Project.status (planned/wired/active) + seed updates - ProjectBadge component - CommandPalette (⌘K) wired in PanelShell with project + nav + sign-out - Dashboard '/' shows per-project cards + KPI strip - Added doner312 to seeded projects
This commit is contained in:
@@ -12,7 +12,9 @@
|
|||||||
"prisma:migrate:deploy": "prisma migrate deploy",
|
"prisma:migrate:deploy": "prisma migrate deploy",
|
||||||
"prisma:seed": "tsx prisma/seed.ts"
|
"prisma:seed": "tsx prisma/seed.ts"
|
||||||
},
|
},
|
||||||
"prisma": { "seed": "tsx prisma/seed.ts" },
|
"prisma": {
|
||||||
|
"seed": "tsx prisma/seed.ts"
|
||||||
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@base-ui/react": "^1.4.1",
|
"@base-ui/react": "^1.4.1",
|
||||||
"@dnd-kit/core": "^6.3.1",
|
"@dnd-kit/core": "^6.3.1",
|
||||||
@@ -24,6 +26,7 @@
|
|||||||
"better-auth": "^1.2.0",
|
"better-auth": "^1.2.0",
|
||||||
"class-variance-authority": "^0.7.1",
|
"class-variance-authority": "^0.7.1",
|
||||||
"clsx": "^2.1.1",
|
"clsx": "^2.1.1",
|
||||||
|
"cmdk": "^1.1.1",
|
||||||
"lucide-react": "^1.14.0",
|
"lucide-react": "^1.14.0",
|
||||||
"next": "^16.0.0",
|
"next": "^16.0.0",
|
||||||
"next-themes": "^0.4.6",
|
"next-themes": "^0.4.6",
|
||||||
|
|||||||
@@ -72,7 +72,9 @@ model Project {
|
|||||||
key String @unique
|
key String @unique
|
||||||
name String
|
name String
|
||||||
description String?
|
description String?
|
||||||
active Boolean @default(true)
|
/// planned | wired | active
|
||||||
|
status String @default("planned")
|
||||||
|
active Boolean @default(false)
|
||||||
createdAt DateTime @default(now())
|
createdAt DateTime @default(now())
|
||||||
updatedAt DateTime @updatedAt
|
updatedAt DateTime @updatedAt
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,18 +3,19 @@ import { PrismaClient } from "@prisma/client";
|
|||||||
const prisma = new PrismaClient();
|
const prisma = new PrismaClient();
|
||||||
|
|
||||||
const projects = [
|
const projects = [
|
||||||
{ key: "sase", name: "Sase.tr", description: "VIN/chassis lookup + auto parts catalog", active: true },
|
{ key: "sase", name: "Sase.tr", description: "VIN/chassis lookup + auto parts catalog", status: "active", active: true },
|
||||||
{ key: "catvicer", name: "Catvicer", description: "Multi-tenant catering SaaS", active: false },
|
{ key: "catvicer", name: "Catvicer", description: "Multi-tenant catering SaaS", status: "planned", active: false },
|
||||||
{ key: "kokpit", name: "Kokpit", description: "Internal ops dashboard", active: false },
|
{ key: "kokpit", name: "Kokpit", description: "Internal ops dashboard", status: "planned", active: false },
|
||||||
{ key: "otoyedekparca", name: "otoyedekparca.co", description: "Auto parts marketplace", active: false },
|
{ key: "otoyedekparca", name: "otoyedekparca.co", description: "Auto parts marketplace", status: "planned", active: false },
|
||||||
{ key: "eryaman", name: "Eryaman Evleri TYY", description: "Residence portal", active: false },
|
{ key: "eryaman", name: "Eryaman Evleri TYY", description: "Residence portal", status: "planned", active: false },
|
||||||
|
{ key: "doner312", name: "312 Döner", description: "QSR ordering & ops", status: "planned", active: false },
|
||||||
];
|
];
|
||||||
|
|
||||||
async function main() {
|
async function main() {
|
||||||
for (const p of projects) {
|
for (const p of projects) {
|
||||||
await prisma.project.upsert({
|
await prisma.project.upsert({
|
||||||
where: { key: p.key },
|
where: { key: p.key },
|
||||||
update: { name: p.name, description: p.description },
|
update: { name: p.name, description: p.description, status: p.status, active: p.active },
|
||||||
create: p,
|
create: p,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,42 +1,97 @@
|
|||||||
import { headers } from "next/headers";
|
import Link from "next/link";
|
||||||
import { redirect } from "next/navigation";
|
import { PanelShell } from "@/components/panel-shell";
|
||||||
import { auth } from "@/lib/auth";
|
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
|
||||||
import { AppSidebar } from "@/components/app-sidebar";
|
import { ProjectBadge } from "@/lib/project-badge";
|
||||||
import { ChartAreaInteractive } from "@/components/chart-area-interactive";
|
import { prisma } from "@/lib/db";
|
||||||
import { SectionCards } from "@/components/section-cards";
|
import { saseDb } from "@/lib/db-sase";
|
||||||
import { SiteHeader } from "@/components/site-header";
|
|
||||||
import { SidebarInset, SidebarProvider } from "@/components/ui/sidebar";
|
export const dynamic = "force-dynamic";
|
||||||
|
|
||||||
|
async function fetchProjectMetric(key: string): Promise<string | null> {
|
||||||
|
try {
|
||||||
|
if (key === "sase") {
|
||||||
|
const users = await saseDb.user.count();
|
||||||
|
return `${users} users`;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
} catch {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function fetchPanelStats() {
|
||||||
|
const [audit24h, audit7d] = await Promise.all([
|
||||||
|
prisma.auditLog.count({
|
||||||
|
where: { createdAt: { gte: new Date(Date.now() - 24 * 60 * 60 * 1000) } },
|
||||||
|
}),
|
||||||
|
prisma.auditLog.count({
|
||||||
|
where: { createdAt: { gte: new Date(Date.now() - 7 * 24 * 60 * 60 * 1000) } },
|
||||||
|
}),
|
||||||
|
]);
|
||||||
|
return { audit24h, audit7d };
|
||||||
|
}
|
||||||
|
|
||||||
export default async function Home() {
|
export default async function Home() {
|
||||||
const session = await auth.api.getSession({ headers: await headers() });
|
const [projects, stats] = await Promise.all([
|
||||||
if (!session) redirect("/login");
|
prisma.project.findMany({ orderBy: [{ status: "desc" }, { name: "asc" }] }),
|
||||||
|
fetchPanelStats(),
|
||||||
|
]);
|
||||||
|
|
||||||
|
const projectsWithMetric = await Promise.all(
|
||||||
|
projects.map(async (p) => ({ ...p, metric: await fetchProjectMetric(p.key) })),
|
||||||
|
);
|
||||||
|
|
||||||
|
const active = projectsWithMetric.filter((p) => p.status === "active").length;
|
||||||
|
const planned = projectsWithMetric.filter((p) => p.status === "planned").length;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<SidebarProvider
|
<PanelShell title="Overview">
|
||||||
style={
|
<div className="grid grid-cols-1 gap-4 md:grid-cols-4">
|
||||||
{
|
<KpiCard label="Active spokes" value={active.toString()} hint={`of ${projects.length} total`} />
|
||||||
"--sidebar-width": "calc(var(--spacing) * 72)",
|
<KpiCard label="Planned" value={planned.toString()} hint="awaiting wiring" />
|
||||||
"--header-height": "calc(var(--spacing) * 12)",
|
<KpiCard label="Audit (24h)" value={stats.audit24h.toString()} hint="all actions logged" />
|
||||||
} as React.CSSProperties
|
<KpiCard label="Audit (7d)" value={stats.audit7d.toString()} hint="rolling window" />
|
||||||
}
|
</div>
|
||||||
>
|
|
||||||
<AppSidebar
|
<div>
|
||||||
variant="inset"
|
<h2 className="mb-3 text-sm font-medium text-muted-foreground">Projects</h2>
|
||||||
user={{ name: session.user.name, email: session.user.email }}
|
<div className="grid grid-cols-1 gap-4 md:grid-cols-2 lg:grid-cols-3">
|
||||||
/>
|
{projectsWithMetric.map((p) => (
|
||||||
<SidebarInset>
|
<Link key={p.id} href={`/projects/${p.key}`} className="block">
|
||||||
<SiteHeader />
|
<Card className="h-full transition-colors hover:border-foreground/40">
|
||||||
<div className="flex flex-1 flex-col">
|
<CardHeader>
|
||||||
<div className="@container/main flex flex-1 flex-col gap-2">
|
<div className="flex items-center justify-between">
|
||||||
<div className="flex flex-col gap-4 py-4 md:gap-6 md:py-6">
|
<CardTitle className="text-base">{p.name}</CardTitle>
|
||||||
<SectionCards />
|
<ProjectBadge status={p.status} />
|
||||||
<div className="px-4 lg:px-6">
|
</div>
|
||||||
<ChartAreaInteractive />
|
<CardDescription>{p.description ?? "—"}</CardDescription>
|
||||||
</div>
|
</CardHeader>
|
||||||
</div>
|
<CardContent>
|
||||||
</div>
|
<div className="text-sm tabular-nums">
|
||||||
|
{p.metric ?? <span className="text-muted-foreground">—</span>}
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
</Link>
|
||||||
|
))}
|
||||||
</div>
|
</div>
|
||||||
</SidebarInset>
|
</div>
|
||||||
</SidebarProvider>
|
|
||||||
|
<p className="mt-4 text-xs text-muted-foreground">
|
||||||
|
Tip: <kbd className="rounded border px-1.5 py-0.5">⌘K</kbd> for command palette
|
||||||
|
</p>
|
||||||
|
</PanelShell>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function KpiCard({ label, value, hint }: { label: string; value: string; hint: string }) {
|
||||||
|
return (
|
||||||
|
<Card>
|
||||||
|
<CardHeader>
|
||||||
|
<CardDescription>{label}</CardDescription>
|
||||||
|
<CardTitle className="text-3xl font-semibold tabular-nums">{value}</CardTitle>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent className="text-xs text-muted-foreground">{hint}</CardContent>
|
||||||
|
</Card>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import { notFound } from "next/navigation";
|
|||||||
import { prisma } from "@/lib/db";
|
import { prisma } from "@/lib/db";
|
||||||
import { PanelShell } from "@/components/panel-shell";
|
import { PanelShell } from "@/components/panel-shell";
|
||||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
|
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
|
||||||
import { Badge } from "@/components/ui/badge";
|
import { ProjectBadge } from "@/lib/project-badge";
|
||||||
import { SaseHealth } from "./_sase";
|
import { SaseHealth } from "./_sase";
|
||||||
|
|
||||||
export const dynamic = "force-dynamic";
|
export const dynamic = "force-dynamic";
|
||||||
@@ -16,9 +16,7 @@ export default async function ProjectDetail({ params }: { params: Promise<{ key:
|
|||||||
<PanelShell title={project.name}>
|
<PanelShell title={project.name}>
|
||||||
<div className="flex items-center gap-3">
|
<div className="flex items-center gap-3">
|
||||||
<h2 className="text-xl font-semibold">{project.name}</h2>
|
<h2 className="text-xl font-semibold">{project.name}</h2>
|
||||||
<Badge variant={project.active ? "default" : "outline"}>
|
<ProjectBadge status={project.status} />
|
||||||
{project.active ? "wired" : "not wired"}
|
|
||||||
</Badge>
|
|
||||||
</div>
|
</div>
|
||||||
<p className="text-sm text-muted-foreground">{project.description}</p>
|
<p className="text-sm text-muted-foreground">{project.description}</p>
|
||||||
|
|
||||||
|
|||||||
@@ -1,13 +1,15 @@
|
|||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { prisma } from "@/lib/db";
|
import { prisma } from "@/lib/db";
|
||||||
|
|
||||||
export const dynamic = "force-dynamic";
|
|
||||||
import { PanelShell } from "@/components/panel-shell";
|
import { PanelShell } from "@/components/panel-shell";
|
||||||
import { Card, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
|
import { Card, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
|
||||||
import { Badge } from "@/components/ui/badge";
|
import { ProjectBadge } from "@/lib/project-badge";
|
||||||
|
|
||||||
|
export const dynamic = "force-dynamic";
|
||||||
|
|
||||||
export default async function ProjectsPage() {
|
export default async function ProjectsPage() {
|
||||||
const projects = await prisma.project.findMany({ orderBy: { name: "asc" } });
|
const projects = await prisma.project.findMany({
|
||||||
|
orderBy: [{ status: "desc" }, { name: "asc" }],
|
||||||
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<PanelShell title="Projects">
|
<PanelShell title="Projects">
|
||||||
@@ -18,18 +20,13 @@ export default async function ProjectsPage() {
|
|||||||
<CardHeader>
|
<CardHeader>
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
<CardTitle className="text-base">{p.name}</CardTitle>
|
<CardTitle className="text-base">{p.name}</CardTitle>
|
||||||
<Badge variant={p.active ? "default" : "outline"}>
|
<ProjectBadge status={p.status} />
|
||||||
{p.active ? "wired" : "not wired"}
|
|
||||||
</Badge>
|
|
||||||
</div>
|
</div>
|
||||||
<CardDescription>{p.description ?? "—"}</CardDescription>
|
<CardDescription>{p.description ?? "—"}</CardDescription>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
</Card>
|
</Card>
|
||||||
</Link>
|
</Link>
|
||||||
))}
|
))}
|
||||||
{projects.length === 0 && (
|
|
||||||
<p className="text-sm text-muted-foreground">No projects yet — run prisma seed.</p>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
</PanelShell>
|
</PanelShell>
|
||||||
);
|
);
|
||||||
|
|||||||
94
apps/web/src/components/command-palette.tsx
Normal file
94
apps/web/src/components/command-palette.tsx
Normal file
@@ -0,0 +1,94 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { useEffect, useState } from "react";
|
||||||
|
import { useRouter } from "next/navigation";
|
||||||
|
import {
|
||||||
|
CommandDialog,
|
||||||
|
CommandEmpty,
|
||||||
|
CommandGroup,
|
||||||
|
CommandInput,
|
||||||
|
CommandItem,
|
||||||
|
CommandList,
|
||||||
|
CommandSeparator,
|
||||||
|
} from "@/components/ui/command";
|
||||||
|
import {
|
||||||
|
ActivityIcon,
|
||||||
|
FolderIcon,
|
||||||
|
LayoutDashboardIcon,
|
||||||
|
LogOutIcon,
|
||||||
|
ScrollTextIcon,
|
||||||
|
Settings2Icon,
|
||||||
|
TerminalIcon,
|
||||||
|
} from "lucide-react";
|
||||||
|
import { signOut } from "@/lib/auth-client";
|
||||||
|
|
||||||
|
type ProjectLite = { key: string; name: string; status: string };
|
||||||
|
|
||||||
|
export function CommandPalette({ projects }: { projects: ProjectLite[] }) {
|
||||||
|
const router = useRouter();
|
||||||
|
const [open, setOpen] = useState(false);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const onKey = (e: KeyboardEvent) => {
|
||||||
|
if ((e.metaKey || e.ctrlKey) && e.key === "k") {
|
||||||
|
e.preventDefault();
|
||||||
|
setOpen((v) => !v);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
window.addEventListener("keydown", onKey);
|
||||||
|
return () => window.removeEventListener("keydown", onKey);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const go = (path: string) => {
|
||||||
|
setOpen(false);
|
||||||
|
router.push(path);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<CommandDialog open={open} onOpenChange={setOpen} title="Command palette" description="Quick navigation">
|
||||||
|
<CommandInput placeholder="Jump to project, page, or action…" />
|
||||||
|
<CommandList>
|
||||||
|
<CommandEmpty>No match.</CommandEmpty>
|
||||||
|
|
||||||
|
<CommandGroup heading="Navigation">
|
||||||
|
<CommandItem onSelect={() => go("/")}><LayoutDashboardIcon /> Overview</CommandItem>
|
||||||
|
<CommandItem onSelect={() => go("/projects")}><FolderIcon /> Projects</CommandItem>
|
||||||
|
<CommandItem onSelect={() => go("/operations")}><TerminalIcon /> Operations</CommandItem>
|
||||||
|
<CommandItem onSelect={() => go("/events")}><ActivityIcon /> Events</CommandItem>
|
||||||
|
<CommandItem onSelect={() => go("/audit")}><ScrollTextIcon /> Audit</CommandItem>
|
||||||
|
<CommandItem onSelect={() => go("/settings")}><Settings2Icon /> Settings</CommandItem>
|
||||||
|
</CommandGroup>
|
||||||
|
|
||||||
|
<CommandSeparator />
|
||||||
|
|
||||||
|
<CommandGroup heading="Projects">
|
||||||
|
{projects.map((p) => (
|
||||||
|
<CommandItem
|
||||||
|
key={p.key}
|
||||||
|
keywords={[p.key, p.name, p.status]}
|
||||||
|
onSelect={() => go(`/projects/${p.key}`)}
|
||||||
|
>
|
||||||
|
<FolderIcon />
|
||||||
|
<span>{p.name}</span>
|
||||||
|
<span className="ml-auto text-xs text-muted-foreground">{p.status}</span>
|
||||||
|
</CommandItem>
|
||||||
|
))}
|
||||||
|
</CommandGroup>
|
||||||
|
|
||||||
|
<CommandSeparator />
|
||||||
|
|
||||||
|
<CommandGroup heading="Account">
|
||||||
|
<CommandItem
|
||||||
|
onSelect={async () => {
|
||||||
|
setOpen(false);
|
||||||
|
await signOut();
|
||||||
|
router.replace("/login");
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<LogOutIcon /> Sign out
|
||||||
|
</CommandItem>
|
||||||
|
</CommandGroup>
|
||||||
|
</CommandList>
|
||||||
|
</CommandDialog>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1,8 +1,10 @@
|
|||||||
import { headers } from "next/headers";
|
import { headers } from "next/headers";
|
||||||
import { redirect } from "next/navigation";
|
import { redirect } from "next/navigation";
|
||||||
import { auth } from "@/lib/auth";
|
import { auth } from "@/lib/auth";
|
||||||
|
import { prisma } from "@/lib/db";
|
||||||
import { AppSidebar } from "@/components/app-sidebar";
|
import { AppSidebar } from "@/components/app-sidebar";
|
||||||
import { SiteHeader } from "@/components/site-header";
|
import { SiteHeader } from "@/components/site-header";
|
||||||
|
import { CommandPalette } from "@/components/command-palette";
|
||||||
import { SidebarInset, SidebarProvider } from "@/components/ui/sidebar";
|
import { SidebarInset, SidebarProvider } from "@/components/ui/sidebar";
|
||||||
|
|
||||||
export async function PanelShell({
|
export async function PanelShell({
|
||||||
@@ -14,6 +16,12 @@ export async function PanelShell({
|
|||||||
}) {
|
}) {
|
||||||
const session = await auth.api.getSession({ headers: await headers() });
|
const session = await auth.api.getSession({ headers: await headers() });
|
||||||
if (!session) redirect("/login");
|
if (!session) redirect("/login");
|
||||||
|
|
||||||
|
const projects = await prisma.project.findMany({
|
||||||
|
select: { key: true, name: true, status: true },
|
||||||
|
orderBy: [{ status: "desc" }, { name: "asc" }],
|
||||||
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<SidebarProvider
|
<SidebarProvider
|
||||||
style={
|
style={
|
||||||
@@ -28,6 +36,7 @@ export async function PanelShell({
|
|||||||
<SiteHeader title={title} />
|
<SiteHeader title={title} />
|
||||||
<div className="flex flex-1 flex-col gap-4 p-4 md:p-6">{children}</div>
|
<div className="flex flex-1 flex-col gap-4 p-4 md:p-6">{children}</div>
|
||||||
</SidebarInset>
|
</SidebarInset>
|
||||||
|
<CommandPalette projects={projects} />
|
||||||
</SidebarProvider>
|
</SidebarProvider>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
196
apps/web/src/components/ui/command.tsx
Normal file
196
apps/web/src/components/ui/command.tsx
Normal file
@@ -0,0 +1,196 @@
|
|||||||
|
"use client"
|
||||||
|
|
||||||
|
import * as React from "react"
|
||||||
|
import { Command as CommandPrimitive } from "cmdk"
|
||||||
|
|
||||||
|
import { cn } from "@/lib/utils"
|
||||||
|
import {
|
||||||
|
Dialog,
|
||||||
|
DialogContent,
|
||||||
|
DialogDescription,
|
||||||
|
DialogHeader,
|
||||||
|
DialogTitle,
|
||||||
|
} from "@/components/ui/dialog"
|
||||||
|
import {
|
||||||
|
InputGroup,
|
||||||
|
InputGroupAddon,
|
||||||
|
} from "@/components/ui/input-group"
|
||||||
|
import { SearchIcon, CheckIcon } from "lucide-react"
|
||||||
|
|
||||||
|
function Command({
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof CommandPrimitive>) {
|
||||||
|
return (
|
||||||
|
<CommandPrimitive
|
||||||
|
data-slot="command"
|
||||||
|
className={cn(
|
||||||
|
"flex size-full flex-col overflow-hidden rounded-xl! bg-popover p-1 text-popover-foreground",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function CommandDialog({
|
||||||
|
title = "Command Palette",
|
||||||
|
description = "Search for a command to run...",
|
||||||
|
children,
|
||||||
|
className,
|
||||||
|
showCloseButton = false,
|
||||||
|
...props
|
||||||
|
}: Omit<React.ComponentProps<typeof Dialog>, "children"> & {
|
||||||
|
title?: string
|
||||||
|
description?: string
|
||||||
|
className?: string
|
||||||
|
showCloseButton?: boolean
|
||||||
|
children: React.ReactNode
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<Dialog {...props}>
|
||||||
|
<DialogHeader className="sr-only">
|
||||||
|
<DialogTitle>{title}</DialogTitle>
|
||||||
|
<DialogDescription>{description}</DialogDescription>
|
||||||
|
</DialogHeader>
|
||||||
|
<DialogContent
|
||||||
|
className={cn(
|
||||||
|
"top-1/3 translate-y-0 overflow-hidden rounded-xl! p-0",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
showCloseButton={showCloseButton}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function CommandInput({
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof CommandPrimitive.Input>) {
|
||||||
|
return (
|
||||||
|
<div data-slot="command-input-wrapper" className="p-1 pb-0">
|
||||||
|
<InputGroup className="h-8! rounded-lg! border-input/30 bg-input/30 shadow-none! *:data-[slot=input-group-addon]:pl-2!">
|
||||||
|
<CommandPrimitive.Input
|
||||||
|
data-slot="command-input"
|
||||||
|
className={cn(
|
||||||
|
"w-full text-sm outline-hidden disabled:cursor-not-allowed disabled:opacity-50",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
<InputGroupAddon>
|
||||||
|
<SearchIcon className="size-4 shrink-0 opacity-50" />
|
||||||
|
</InputGroupAddon>
|
||||||
|
</InputGroup>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function CommandList({
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof CommandPrimitive.List>) {
|
||||||
|
return (
|
||||||
|
<CommandPrimitive.List
|
||||||
|
data-slot="command-list"
|
||||||
|
className={cn(
|
||||||
|
"no-scrollbar max-h-72 scroll-py-1 overflow-x-hidden overflow-y-auto outline-none",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function CommandEmpty({
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof CommandPrimitive.Empty>) {
|
||||||
|
return (
|
||||||
|
<CommandPrimitive.Empty
|
||||||
|
data-slot="command-empty"
|
||||||
|
className={cn("py-6 text-center text-sm", className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function CommandGroup({
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof CommandPrimitive.Group>) {
|
||||||
|
return (
|
||||||
|
<CommandPrimitive.Group
|
||||||
|
data-slot="command-group"
|
||||||
|
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}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function CommandSeparator({
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof CommandPrimitive.Separator>) {
|
||||||
|
return (
|
||||||
|
<CommandPrimitive.Separator
|
||||||
|
data-slot="command-separator"
|
||||||
|
className={cn("-mx-1 h-px bg-border", className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function CommandItem({
|
||||||
|
className,
|
||||||
|
children,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof CommandPrimitive.Item>) {
|
||||||
|
return (
|
||||||
|
<CommandPrimitive.Item
|
||||||
|
data-slot="command-item"
|
||||||
|
className={cn(
|
||||||
|
"group/command-item relative flex cursor-default items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-hidden select-none in-data-[slot=dialog-content]:rounded-lg! data-[disabled=true]:pointer-events-none data-[disabled=true]:opacity-50 data-selected:bg-muted data-selected:text-foreground [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4 data-selected:*:[svg]:text-foreground",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
<CheckIcon className="ml-auto opacity-0 group-has-data-[slot=command-shortcut]/command-item:hidden group-data-[checked=true]/command-item:opacity-100" />
|
||||||
|
</CommandPrimitive.Item>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function CommandShortcut({
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<"span">) {
|
||||||
|
return (
|
||||||
|
<span
|
||||||
|
data-slot="command-shortcut"
|
||||||
|
className={cn(
|
||||||
|
"ml-auto text-xs tracking-widest text-muted-foreground group-data-selected/command-item:text-foreground",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export {
|
||||||
|
Command,
|
||||||
|
CommandDialog,
|
||||||
|
CommandInput,
|
||||||
|
CommandList,
|
||||||
|
CommandEmpty,
|
||||||
|
CommandGroup,
|
||||||
|
CommandItem,
|
||||||
|
CommandShortcut,
|
||||||
|
CommandSeparator,
|
||||||
|
}
|
||||||
160
apps/web/src/components/ui/dialog.tsx
Normal file
160
apps/web/src/components/ui/dialog.tsx
Normal file
@@ -0,0 +1,160 @@
|
|||||||
|
"use client"
|
||||||
|
|
||||||
|
import * as React from "react"
|
||||||
|
import { Dialog as DialogPrimitive } from "@base-ui/react/dialog"
|
||||||
|
|
||||||
|
import { cn } from "@/lib/utils"
|
||||||
|
import { Button } from "@/components/ui/button"
|
||||||
|
import { XIcon } from "lucide-react"
|
||||||
|
|
||||||
|
function Dialog({ ...props }: DialogPrimitive.Root.Props) {
|
||||||
|
return <DialogPrimitive.Root data-slot="dialog" {...props} />
|
||||||
|
}
|
||||||
|
|
||||||
|
function DialogTrigger({ ...props }: DialogPrimitive.Trigger.Props) {
|
||||||
|
return <DialogPrimitive.Trigger data-slot="dialog-trigger" {...props} />
|
||||||
|
}
|
||||||
|
|
||||||
|
function DialogPortal({ ...props }: DialogPrimitive.Portal.Props) {
|
||||||
|
return <DialogPrimitive.Portal data-slot="dialog-portal" {...props} />
|
||||||
|
}
|
||||||
|
|
||||||
|
function DialogClose({ ...props }: DialogPrimitive.Close.Props) {
|
||||||
|
return <DialogPrimitive.Close data-slot="dialog-close" {...props} />
|
||||||
|
}
|
||||||
|
|
||||||
|
function DialogOverlay({
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: DialogPrimitive.Backdrop.Props) {
|
||||||
|
return (
|
||||||
|
<DialogPrimitive.Backdrop
|
||||||
|
data-slot="dialog-overlay"
|
||||||
|
className={cn(
|
||||||
|
"fixed inset-0 isolate z-50 bg-black/10 duration-100 supports-backdrop-filter:backdrop-blur-xs data-open:animate-in data-open:fade-in-0 data-closed:animate-out data-closed:fade-out-0",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function DialogContent({
|
||||||
|
className,
|
||||||
|
children,
|
||||||
|
showCloseButton = true,
|
||||||
|
...props
|
||||||
|
}: DialogPrimitive.Popup.Props & {
|
||||||
|
showCloseButton?: boolean
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<DialogPortal>
|
||||||
|
<DialogOverlay />
|
||||||
|
<DialogPrimitive.Popup
|
||||||
|
data-slot="dialog-content"
|
||||||
|
className={cn(
|
||||||
|
"fixed top-1/2 left-1/2 z-50 grid w-full max-w-[calc(100%-2rem)] -translate-x-1/2 -translate-y-1/2 gap-4 rounded-xl bg-popover p-4 text-sm text-popover-foreground ring-1 ring-foreground/10 duration-100 outline-none sm:max-w-sm data-open:animate-in data-open:fade-in-0 data-open:zoom-in-95 data-closed:animate-out data-closed:fade-out-0 data-closed:zoom-out-95",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
{showCloseButton && (
|
||||||
|
<DialogPrimitive.Close
|
||||||
|
data-slot="dialog-close"
|
||||||
|
render={
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
className="absolute top-2 right-2"
|
||||||
|
size="icon-sm"
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<XIcon
|
||||||
|
/>
|
||||||
|
<span className="sr-only">Close</span>
|
||||||
|
</DialogPrimitive.Close>
|
||||||
|
)}
|
||||||
|
</DialogPrimitive.Popup>
|
||||||
|
</DialogPortal>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function DialogHeader({ className, ...props }: React.ComponentProps<"div">) {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
data-slot="dialog-header"
|
||||||
|
className={cn("flex flex-col gap-2", className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function DialogFooter({
|
||||||
|
className,
|
||||||
|
showCloseButton = false,
|
||||||
|
children,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<"div"> & {
|
||||||
|
showCloseButton?: boolean
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
data-slot="dialog-footer"
|
||||||
|
className={cn(
|
||||||
|
"-mx-4 -mb-4 flex flex-col-reverse gap-2 rounded-b-xl border-t bg-muted/50 p-4 sm:flex-row sm:justify-end",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
{showCloseButton && (
|
||||||
|
<DialogPrimitive.Close render={<Button variant="outline" />}>
|
||||||
|
Close
|
||||||
|
</DialogPrimitive.Close>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function DialogTitle({ className, ...props }: DialogPrimitive.Title.Props) {
|
||||||
|
return (
|
||||||
|
<DialogPrimitive.Title
|
||||||
|
data-slot="dialog-title"
|
||||||
|
className={cn(
|
||||||
|
"font-heading text-base leading-none font-medium",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function DialogDescription({
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: DialogPrimitive.Description.Props) {
|
||||||
|
return (
|
||||||
|
<DialogPrimitive.Description
|
||||||
|
data-slot="dialog-description"
|
||||||
|
className={cn(
|
||||||
|
"text-sm text-muted-foreground *:[a]:underline *:[a]:underline-offset-3 *:[a]:hover:text-foreground",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export {
|
||||||
|
Dialog,
|
||||||
|
DialogClose,
|
||||||
|
DialogContent,
|
||||||
|
DialogDescription,
|
||||||
|
DialogFooter,
|
||||||
|
DialogHeader,
|
||||||
|
DialogOverlay,
|
||||||
|
DialogPortal,
|
||||||
|
DialogTitle,
|
||||||
|
DialogTrigger,
|
||||||
|
}
|
||||||
158
apps/web/src/components/ui/input-group.tsx
Normal file
158
apps/web/src/components/ui/input-group.tsx
Normal file
@@ -0,0 +1,158 @@
|
|||||||
|
"use client"
|
||||||
|
|
||||||
|
import * as React from "react"
|
||||||
|
import { cva, type VariantProps } from "class-variance-authority"
|
||||||
|
|
||||||
|
import { cn } from "@/lib/utils"
|
||||||
|
import { Button } from "@/components/ui/button"
|
||||||
|
import { Input } from "@/components/ui/input"
|
||||||
|
import { Textarea } from "@/components/ui/textarea"
|
||||||
|
|
||||||
|
function InputGroup({ className, ...props }: React.ComponentProps<"div">) {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
data-slot="input-group"
|
||||||
|
role="group"
|
||||||
|
className={cn(
|
||||||
|
"group/input-group relative flex h-8 w-full min-w-0 items-center rounded-lg border border-input transition-colors outline-none in-data-[slot=combobox-content]:focus-within:border-inherit in-data-[slot=combobox-content]:focus-within:ring-0 has-disabled:bg-input/50 has-disabled:opacity-50 has-[[data-slot=input-group-control]:focus-visible]:border-ring has-[[data-slot=input-group-control]:focus-visible]:ring-3 has-[[data-slot=input-group-control]:focus-visible]:ring-ring/50 has-[[data-slot][aria-invalid=true]]:border-destructive has-[[data-slot][aria-invalid=true]]:ring-3 has-[[data-slot][aria-invalid=true]]:ring-destructive/20 has-[>[data-align=block-end]]:h-auto has-[>[data-align=block-end]]:flex-col has-[>[data-align=block-start]]:h-auto has-[>[data-align=block-start]]:flex-col has-[>textarea]:h-auto dark:bg-input/30 dark:has-disabled:bg-input/80 dark:has-[[data-slot][aria-invalid=true]]:ring-destructive/40 has-[>[data-align=block-end]]:[&>input]:pt-3 has-[>[data-align=block-start]]:[&>input]:pb-3 has-[>[data-align=inline-end]]:[&>input]:pr-1.5 has-[>[data-align=inline-start]]:[&>input]:pl-1.5",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const inputGroupAddonVariants = cva(
|
||||||
|
"flex h-auto cursor-text items-center justify-center gap-2 py-1.5 text-sm font-medium text-muted-foreground select-none group-data-[disabled=true]/input-group:opacity-50 [&>kbd]:rounded-[calc(var(--radius)-5px)] [&>svg:not([class*='size-'])]:size-4",
|
||||||
|
{
|
||||||
|
variants: {
|
||||||
|
align: {
|
||||||
|
"inline-start":
|
||||||
|
"order-first pl-2 has-[>button]:ml-[-0.3rem] has-[>kbd]:ml-[-0.15rem]",
|
||||||
|
"inline-end":
|
||||||
|
"order-last pr-2 has-[>button]:mr-[-0.3rem] has-[>kbd]:mr-[-0.15rem]",
|
||||||
|
"block-start":
|
||||||
|
"order-first w-full justify-start px-2.5 pt-2 group-has-[>input]/input-group:pt-2 [.border-b]:pb-2",
|
||||||
|
"block-end":
|
||||||
|
"order-last w-full justify-start px-2.5 pb-2 group-has-[>input]/input-group:pb-2 [.border-t]:pt-2",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
defaultVariants: {
|
||||||
|
align: "inline-start",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
function InputGroupAddon({
|
||||||
|
className,
|
||||||
|
align = "inline-start",
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<"div"> & VariantProps<typeof inputGroupAddonVariants>) {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
role="group"
|
||||||
|
data-slot="input-group-addon"
|
||||||
|
data-align={align}
|
||||||
|
className={cn(inputGroupAddonVariants({ align }), className)}
|
||||||
|
onClick={(e) => {
|
||||||
|
if ((e.target as HTMLElement).closest("button")) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
e.currentTarget.parentElement?.querySelector("input")?.focus()
|
||||||
|
}}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const inputGroupButtonVariants = cva(
|
||||||
|
"flex items-center gap-2 text-sm shadow-none",
|
||||||
|
{
|
||||||
|
variants: {
|
||||||
|
size: {
|
||||||
|
xs: "h-6 gap-1 rounded-[calc(var(--radius)-3px)] px-1.5 [&>svg:not([class*='size-'])]:size-3.5",
|
||||||
|
sm: "",
|
||||||
|
"icon-xs":
|
||||||
|
"size-6 rounded-[calc(var(--radius)-3px)] p-0 has-[>svg]:p-0",
|
||||||
|
"icon-sm": "size-8 p-0 has-[>svg]:p-0",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
defaultVariants: {
|
||||||
|
size: "xs",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
function InputGroupButton({
|
||||||
|
className,
|
||||||
|
type = "button",
|
||||||
|
variant = "ghost",
|
||||||
|
size = "xs",
|
||||||
|
...props
|
||||||
|
}: Omit<React.ComponentProps<typeof Button>, "size" | "type"> &
|
||||||
|
VariantProps<typeof inputGroupButtonVariants> & {
|
||||||
|
type?: "button" | "submit" | "reset"
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<Button
|
||||||
|
type={type}
|
||||||
|
data-size={size}
|
||||||
|
variant={variant}
|
||||||
|
className={cn(inputGroupButtonVariants({ size }), className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function InputGroupText({ className, ...props }: React.ComponentProps<"span">) {
|
||||||
|
return (
|
||||||
|
<span
|
||||||
|
className={cn(
|
||||||
|
"flex items-center gap-2 text-sm text-muted-foreground [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function InputGroupInput({
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<"input">) {
|
||||||
|
return (
|
||||||
|
<Input
|
||||||
|
data-slot="input-group-control"
|
||||||
|
className={cn(
|
||||||
|
"flex-1 rounded-none border-0 bg-transparent shadow-none ring-0 focus-visible:ring-0 disabled:bg-transparent aria-invalid:ring-0 dark:bg-transparent dark:disabled:bg-transparent",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function InputGroupTextarea({
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<"textarea">) {
|
||||||
|
return (
|
||||||
|
<Textarea
|
||||||
|
data-slot="input-group-control"
|
||||||
|
className={cn(
|
||||||
|
"flex-1 resize-none rounded-none border-0 bg-transparent py-2 shadow-none ring-0 focus-visible:ring-0 disabled:bg-transparent aria-invalid:ring-0 dark:bg-transparent dark:disabled:bg-transparent",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export {
|
||||||
|
InputGroup,
|
||||||
|
InputGroupAddon,
|
||||||
|
InputGroupButton,
|
||||||
|
InputGroupText,
|
||||||
|
InputGroupInput,
|
||||||
|
InputGroupTextarea,
|
||||||
|
}
|
||||||
18
apps/web/src/components/ui/textarea.tsx
Normal file
18
apps/web/src/components/ui/textarea.tsx
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
import * as React from "react"
|
||||||
|
|
||||||
|
import { cn } from "@/lib/utils"
|
||||||
|
|
||||||
|
function Textarea({ className, ...props }: React.ComponentProps<"textarea">) {
|
||||||
|
return (
|
||||||
|
<textarea
|
||||||
|
data-slot="textarea"
|
||||||
|
className={cn(
|
||||||
|
"flex field-sizing-content min-h-16 w-full rounded-lg border border-input bg-transparent px-2.5 py-2 text-base transition-colors outline-none placeholder:text-muted-foreground focus-visible:border-ring focus-visible:ring-3 focus-visible:ring-ring/50 disabled:cursor-not-allowed disabled:bg-input/50 disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-3 aria-invalid:ring-destructive/20 md:text-sm dark:bg-input/30 dark:disabled:bg-input/80 dark:aria-invalid:border-destructive/50 dark:aria-invalid:ring-destructive/40",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export { Textarea }
|
||||||
13
apps/web/src/lib/project-badge.tsx
Normal file
13
apps/web/src/lib/project-badge.tsx
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
import { Badge } from "@/components/ui/badge";
|
||||||
|
|
||||||
|
export function ProjectBadge({ status }: { status: string }) {
|
||||||
|
switch (status) {
|
||||||
|
case "active":
|
||||||
|
return <Badge className="bg-emerald-500/15 text-emerald-300 border-emerald-500/30">active</Badge>;
|
||||||
|
case "wired":
|
||||||
|
return <Badge variant="secondary">wired</Badge>;
|
||||||
|
case "planned":
|
||||||
|
default:
|
||||||
|
return <Badge variant="outline">planned</Badge>;
|
||||||
|
}
|
||||||
|
}
|
||||||
21
pnpm-lock.yaml
generated
21
pnpm-lock.yaml
generated
@@ -47,6 +47,9 @@ importers:
|
|||||||
clsx:
|
clsx:
|
||||||
specifier: ^2.1.1
|
specifier: ^2.1.1
|
||||||
version: 2.1.1
|
version: 2.1.1
|
||||||
|
cmdk:
|
||||||
|
specifier: ^1.1.1
|
||||||
|
version: 1.1.1(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.6(react@19.2.6))(react@19.2.6)
|
||||||
lucide-react:
|
lucide-react:
|
||||||
specifier: ^1.14.0
|
specifier: ^1.14.0
|
||||||
version: 1.14.0(react@19.2.6)
|
version: 1.14.0(react@19.2.6)
|
||||||
@@ -1471,6 +1474,12 @@ packages:
|
|||||||
resolution: {integrity: sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==}
|
resolution: {integrity: sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==}
|
||||||
engines: {node: '>=6'}
|
engines: {node: '>=6'}
|
||||||
|
|
||||||
|
cmdk@1.1.1:
|
||||||
|
resolution: {integrity: sha512-Vsv7kFaXm+ptHDMZ7izaRsP70GgrW9NBNGswt9OZaVBLlE0SNpDq8eu/VGXyF9r7M0azK3Wy7OlYXsuyYLFzHg==}
|
||||||
|
peerDependencies:
|
||||||
|
react: ^18 || ^19 || ^19.0.0-rc
|
||||||
|
react-dom: ^18 || ^19 || ^19.0.0-rc
|
||||||
|
|
||||||
code-block-writer@13.0.3:
|
code-block-writer@13.0.3:
|
||||||
resolution: {integrity: sha512-Oofo0pq3IKnsFtuHqSF7TqBfr71aeyZDVJ0HpmqB7FBM2qEigL0iPONSCZSO9pE9dZTAxANe5XHG9Uy0YMv8cg==}
|
resolution: {integrity: sha512-Oofo0pq3IKnsFtuHqSF7TqBfr71aeyZDVJ0HpmqB7FBM2qEigL0iPONSCZSO9pE9dZTAxANe5XHG9Uy0YMv8cg==}
|
||||||
|
|
||||||
@@ -4017,6 +4026,18 @@ snapshots:
|
|||||||
|
|
||||||
clsx@2.1.1: {}
|
clsx@2.1.1: {}
|
||||||
|
|
||||||
|
cmdk@1.1.1(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.6(react@19.2.6))(react@19.2.6):
|
||||||
|
dependencies:
|
||||||
|
'@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.14)(react@19.2.6)
|
||||||
|
'@radix-ui/react-dialog': 1.1.15(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.6(react@19.2.6))(react@19.2.6)
|
||||||
|
'@radix-ui/react-id': 1.1.1(@types/react@19.2.14)(react@19.2.6)
|
||||||
|
'@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.6(react@19.2.6))(react@19.2.6)
|
||||||
|
react: 19.2.6
|
||||||
|
react-dom: 19.2.6(react@19.2.6)
|
||||||
|
transitivePeerDependencies:
|
||||||
|
- '@types/react'
|
||||||
|
- '@types/react-dom'
|
||||||
|
|
||||||
code-block-writer@13.0.3: {}
|
code-block-writer@13.0.3: {}
|
||||||
|
|
||||||
color-convert@2.0.1:
|
color-convert@2.0.1:
|
||||||
|
|||||||
Reference in New Issue
Block a user