Files
J621/frontend/src/components/ui.tsx
T
JakeBreath 555c25d77b M0: scaffold SPA + API monorepo
Backend (Django 6.1 + DRF):
- Token auth with a custom User model (register/login/logout/me)
- Library models (MediaItem, MediaLocation) and REST endpoints
- File list/detail with search, rating filter, sorting, pagination
- Multipart upload with optional rating/tags/notes
- Range-aware media serving (video seeking) and ffmpeg thumbnails
- scan_files management command for the watched folder

Frontend (React 19 + Vite + TypeScript):
- Catppuccin Mocha design tokens from the design docs
- App shell, token persistence, protected routes
- Library grid with filters, file detail with custom data editor
- Upload page with per-file progress via XHR
- Dev proxy to the Django API
2026-09-17 07:56:47 -05:00

91 lines
2.5 KiB
TypeScript

import type { ButtonHTMLAttributes, ReactNode } from "react";
import { cn } from "@/lib/cn";
type ButtonVariant = "primary" | "secondary" | "ghost" | "danger";
const buttonVariants: Record<ButtonVariant, string> = {
primary: "bg-ctp-mauve text-ctp-crust hover:brightness-110",
secondary:
"border border-ctp-surface1 bg-ctp-surface0 text-ctp-text hover:bg-ctp-surface1",
ghost: "text-ctp-subtext0 hover:bg-ctp-surface0 hover:text-ctp-text",
danger: "border border-ctp-red/40 text-ctp-red hover:bg-ctp-red/15",
};
interface ButtonProps extends ButtonHTMLAttributes<HTMLButtonElement> {
variant?: ButtonVariant;
}
export function Button({ variant = "primary", className, ...props }: ButtonProps) {
return (
<button
className={cn(
"inline-flex items-center justify-center gap-2 rounded-md px-3 py-1.5 text-sm font-medium transition",
"focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-ctp-mauve",
"active:scale-[.98] disabled:pointer-events-none disabled:opacity-50",
buttonVariants[variant],
className,
)}
{...props}
/>
);
}
export function Spinner({ className }: { className?: string }) {
return (
<div
className={cn(
"h-4 w-4 animate-spin rounded-full border-2 border-ctp-surface2 border-t-ctp-mauve",
className,
)}
role="status"
aria-label="Loading"
/>
);
}
export function FullPageSpinner() {
return (
<div className="flex min-h-screen items-center justify-center bg-ctp-crust">
<Spinner className="h-6 w-6" />
</div>
);
}
export function Field({
label,
children,
}: {
label: string;
children: ReactNode;
}) {
return (
<label className="flex flex-col gap-1.5">
<span className="text-xs font-medium uppercase tracking-wide text-ctp-overlay1">
{label}
</span>
{children}
</label>
);
}
export const inputClass =
"w-full rounded-md border border-ctp-surface0 bg-ctp-mantle px-3 py-2 text-sm text-ctp-text placeholder:text-ctp-overlay0 focus:border-ctp-mauve focus:outline-none";
export function EmptyState({
title,
description,
}: {
title: string;
description?: ReactNode;
}) {
return (
<div className="flex flex-col items-center justify-center gap-1 rounded-lg border border-dashed border-ctp-surface1 px-6 py-16 text-center">
<p className="text-sm font-medium text-ctp-subtext1">{title}</p>
{description ? (
<p className="text-sm text-ctp-overlay0">{description}</p>
) : null}
</div>
);
}