Revert "feat: migrate frontend to Astro SSG with design system"
This reverts commit 8ad888da28.
This commit is contained in:
@@ -1,34 +0,0 @@
|
||||
---
|
||||
// ─── Header.astro — App header bar ──────────────────────────────────────────
|
||||
// Features: sticky top, backdrop blur, border-bottom, actions slot
|
||||
// ──────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
export interface Props {
|
||||
title: string;
|
||||
}
|
||||
|
||||
const { title } = Astro.props;
|
||||
---
|
||||
|
||||
<header
|
||||
class="header sticky top-0 z-[var(--z-header)] flex items-center justify-between border-b border-border px-6 py-3"
|
||||
>
|
||||
<!-- Left: title -->
|
||||
<div class="flex items-center gap-3">
|
||||
<h1 class="text-lg font-semibold text-foreground">{title}</h1>
|
||||
</div>
|
||||
|
||||
<!-- Right: actions slot -->
|
||||
<div class="flex items-center gap-2">
|
||||
<slot name="actions" />
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<style>
|
||||
.header {
|
||||
backdrop-filter: blur(12px);
|
||||
-webkit-backdrop-filter: blur(12px);
|
||||
background-color: oklch(var(--glass-bg));
|
||||
min-height: 3.5rem;
|
||||
}
|
||||
</style>
|
||||
@@ -1,60 +0,0 @@
|
||||
---
|
||||
// ─── NavItem.astro — Sidebar navigation item ────────────────────────────────
|
||||
// Handles: active state highlighting, collapsed icon-only mode,
|
||||
// notification badge / dot indicators.
|
||||
// ────────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
export interface Props {
|
||||
href: string;
|
||||
icon: string;
|
||||
label: string;
|
||||
active?: boolean;
|
||||
collapsed?: boolean;
|
||||
notificationCount?: number;
|
||||
}
|
||||
|
||||
const {
|
||||
href,
|
||||
icon,
|
||||
label,
|
||||
active = false,
|
||||
collapsed = false,
|
||||
notificationCount = 0,
|
||||
} = Astro.props;
|
||||
|
||||
const navClass = active
|
||||
? "bg-[var(--clr-interactive-selected,oklch(var(--primary-soft)))] text-primary-foreground"
|
||||
: "text-muted-foreground hover:bg-accent hover:text-accent-foreground";
|
||||
---
|
||||
|
||||
<a
|
||||
href={href}
|
||||
class:list={[
|
||||
"nav-item group relative flex items-center gap-3 rounded-lg px-3 py-2.5 text-sm font-medium no-underline transition-all duration-150",
|
||||
collapsed ? "justify-center px-2.5" : "",
|
||||
navClass,
|
||||
]}
|
||||
aria-current={active ? "page" : undefined}
|
||||
aria-label={collapsed ? label : undefined}
|
||||
>
|
||||
<span class="size-5 shrink-0" aria-hidden="true" set:html={icon} />
|
||||
|
||||
{!collapsed && <span class="truncate">{label}</span>}
|
||||
|
||||
{!collapsed && notificationCount > 0 && (
|
||||
<span
|
||||
class="ml-auto inline-flex min-w-5 items-center justify-center rounded-full px-1.5 py-0.5 text-[10px] font-semibold leading-none text-white"
|
||||
style="background-color: var(--clr-severity-high, oklch(var(--destructive)))"
|
||||
>
|
||||
{notificationCount > 99 ? "99+" : notificationCount}
|
||||
</span>
|
||||
)}
|
||||
|
||||
{collapsed && notificationCount > 0 && (
|
||||
<span
|
||||
class="absolute right-1.5 top-1.5 size-2 rounded-full"
|
||||
style="background-color: var(--clr-severity-high, oklch(var(--destructive)))"
|
||||
aria-label={`${notificationCount} notifications`}
|
||||
/>
|
||||
)}
|
||||
</a>
|
||||
@@ -1,93 +0,0 @@
|
||||
---
|
||||
// ─── Sidebar.astro — Main app sidebar navigation ──────────────────────────
|
||||
// Features: collapsed / expanded (64px / 256px), logo, nav items, version
|
||||
// ──────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
import NavItem from "./NavItem.astro";
|
||||
|
||||
export interface Props {
|
||||
collapsed?: boolean;
|
||||
version?: string;
|
||||
currentPath?: string;
|
||||
}
|
||||
|
||||
const { collapsed = false, version = "1.0.0", currentPath = "/live" } =
|
||||
Astro.props;
|
||||
|
||||
const ICONS = {
|
||||
live: `<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"/><circle cx="12" cy="12" r="6"/><circle cx="12" cy="12" r="2"/></svg>`,
|
||||
messages: `<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z"/></svg>`,
|
||||
recordings: `<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 2a3 3 0 0 0-3 3v7a3 3 0 0 0 6 0V5a3 3 0 0 0-3-3Z"/><path d="M19 10v2a7 7 0 0 1-14 0v-2"/><line x1="12" y1="19" x2="12" y2="22"/></svg>`,
|
||||
settings: `<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="3"/><path d="M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 0 1-2.83 2.83l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 0 1-4 0v-.09A1.65 1.65 0 0 0 9 19.4a1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 0 1-2.83-2.83l.06-.06A1.65 1.65 0 0 0 4.68 15a1.65 1.65 0 0 0-1.51-1H3a2 2 0 0 1 0-4h.09A1.65 1.65 0 0 0 4.6 9a1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 0 1 2.83-2.83l.06.06A1.65 1.65 0 0 0 9 4.68a1.65 1.65 0 0 0 1-1.51V3a2 2 0 0 1 4 0v.09a1.65 1.65 0 0 0 1 1.51 1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 0 1 2.83 2.83l-.06.06A1.65 1.65 0 0 0 19.4 9a1.65 1.65 0 0 0 1.51 1H21a2 2 0 0 1 0 4h-.09a1.65 1.65 0 0 0-1.51 1z"/></svg>`,
|
||||
} as const;
|
||||
---
|
||||
|
||||
<aside
|
||||
class="sidebar fixed left-0 top-0 z-30 flex h-screen flex-col border-r border-border bg-background transition-all duration-300 ease-out"
|
||||
class:list={[collapsed ? "w-16" : "w-64"]}
|
||||
aria-label="Main navigation"
|
||||
>
|
||||
<!-- ── Header / Logo ──────────────────────────────────────────── -->
|
||||
<div class="flex items-center border-b border-border px-4 py-4">
|
||||
<a
|
||||
href="/"
|
||||
class="flex items-center gap-3 no-underline"
|
||||
aria-label="Dashboard home"
|
||||
>
|
||||
<span
|
||||
class="flex size-8 items-center justify-center rounded-lg bg-primary text-sm font-bold text-primary-foreground"
|
||||
>
|
||||
B
|
||||
</span>
|
||||
{!collapsed && (
|
||||
<span class="text-lg font-extrabold gradient-text">BETE</span>
|
||||
)}
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<!-- ── Navigation ─────────────────────────────────────────────── -->
|
||||
<nav class="flex flex-1 flex-col gap-1 overflow-y-auto px-3 py-4">
|
||||
<NavItem
|
||||
href="/live"
|
||||
icon={ICONS.live}
|
||||
label="Live"
|
||||
active={currentPath.startsWith("/live")}
|
||||
collapsed={collapsed}
|
||||
/>
|
||||
<NavItem
|
||||
href="/messages"
|
||||
icon={ICONS.messages}
|
||||
label="Messages"
|
||||
active={currentPath.startsWith("/messages")}
|
||||
collapsed={collapsed}
|
||||
notificationCount={12}
|
||||
/>
|
||||
<NavItem
|
||||
href="/recordings"
|
||||
icon={ICONS.recordings}
|
||||
label="Recordings"
|
||||
active={currentPath.startsWith("/recordings")}
|
||||
collapsed={collapsed}
|
||||
/>
|
||||
<NavItem
|
||||
href="/settings"
|
||||
icon={ICONS.settings}
|
||||
label="Settings"
|
||||
active={currentPath.startsWith("/settings")}
|
||||
collapsed={collapsed}
|
||||
/>
|
||||
</nav>
|
||||
|
||||
<!-- ── Footer / Version ───────────────────────────────────────── -->
|
||||
<footer class="border-t border-border px-4 py-3">
|
||||
{!collapsed && (
|
||||
<span class="text-xs text-muted-foreground">v{version}</span>
|
||||
)}
|
||||
</footer>
|
||||
</aside>
|
||||
|
||||
<style>
|
||||
.sidebar {
|
||||
will-change: width;
|
||||
}
|
||||
</style>
|
||||
@@ -1,35 +0,0 @@
|
||||
---
|
||||
// ─── EmptyState.astro — Empty / no-data state ───────────────────────────────
|
||||
// Features: centered layout, icon, title, description, actions slot
|
||||
// ──────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
export interface Props {
|
||||
icon: string;
|
||||
title: string;
|
||||
description?: string;
|
||||
}
|
||||
|
||||
const { icon, title, description } = Astro.props;
|
||||
---
|
||||
|
||||
<div
|
||||
class="flex min-h-64 flex-col items-center justify-center gap-4 px-6 py-12 text-center"
|
||||
>
|
||||
<!-- Icon -->
|
||||
<div class="flex size-16 items-center justify-center" aria-hidden="true" set:html={icon} />
|
||||
|
||||
<!-- Title -->
|
||||
<h2 class="text-xl font-semibold text-foreground">{title}</h2>
|
||||
|
||||
<!-- Description -->
|
||||
{description && (
|
||||
<p class="max-w-sm text-sm text-muted-foreground">{description}</p>
|
||||
)}
|
||||
|
||||
<!-- Actions slot (buttons, links, etc.) -->
|
||||
{Astro.slots.has("actions") && (
|
||||
<div class="mt-2">
|
||||
<slot name="actions" />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
@@ -1,51 +0,0 @@
|
||||
---
|
||||
// ─── ErrorState.astro — Error / failure state ───────────────────────────────
|
||||
// Features: role="alert", severity-critical bg tint, error icon, retry slot
|
||||
// ──────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
export interface Props {
|
||||
message: string;
|
||||
}
|
||||
|
||||
const { message } = Astro.props;
|
||||
---
|
||||
|
||||
<div
|
||||
role="alert"
|
||||
class="flex min-h-48 flex-col items-center justify-center gap-4 rounded-xl px-6 py-10 text-center"
|
||||
style="background-color: oklch(var(--destructive) / 0.08)"
|
||||
>
|
||||
<!-- Error icon circle -->
|
||||
<div
|
||||
class="flex size-12 items-center justify-center rounded-full"
|
||||
style="background-color: oklch(var(--destructive) / 0.15)"
|
||||
aria-hidden="true"
|
||||
>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
style="color: oklch(var(--destructive))"
|
||||
>
|
||||
<circle cx="12" cy="12" r="10" />
|
||||
<line x1="12" y1="8" x2="12" y2="12" />
|
||||
<line x1="12" y1="16" x2="12.01" y2="16" />
|
||||
</svg>
|
||||
</div>
|
||||
|
||||
<!-- Error message -->
|
||||
<p class="max-w-sm text-sm font-medium text-foreground">{message}</p>
|
||||
|
||||
<!-- Retry slot -->
|
||||
{Astro.slots.has("retry") && (
|
||||
<div class="mt-2">
|
||||
<slot name="retry" />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
@@ -1,60 +0,0 @@
|
||||
---
|
||||
// ─── LoadingSkeleton.astro — Placeholder loading states ─────────────────────
|
||||
// Uses the shared Skeleton React component in 3 layout variants.
|
||||
// ──────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
import { Skeleton } from "../../shared/components/skeleton";
|
||||
|
||||
export type Variant = "card" | "list" | "detail";
|
||||
|
||||
export interface Props {
|
||||
/** Visual layout variant */
|
||||
variant?: Variant;
|
||||
/** Number of skeleton items to render (default 1) */
|
||||
count?: number;
|
||||
}
|
||||
|
||||
const { variant = "card", count = 1 } = Astro.props;
|
||||
|
||||
const items = Array.from({ length: count });
|
||||
---
|
||||
|
||||
<div role="presentation" aria-hidden="true" class="flex flex-col gap-4">
|
||||
{
|
||||
items.map((_, i) => (
|
||||
<div class="skeleton-group" data-variant={variant}>
|
||||
{variant === "card" && (
|
||||
<div class="overflow-hidden rounded-xl border border-border">
|
||||
<Skeleton class="h-40 w-full rounded-none" />
|
||||
<div class="space-y-3 p-4">
|
||||
<Skeleton class="h-4 w-3/4" />
|
||||
<Skeleton class="h-3 w-1/2" />
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{variant === "list" && (
|
||||
<div class="flex items-center gap-3 rounded-lg p-3">
|
||||
<Skeleton class="size-10 shrink-0 rounded-full" />
|
||||
<div class="flex-1 space-y-2">
|
||||
<Skeleton class="h-3.5 w-1/3" />
|
||||
<Skeleton class="h-3 w-2/3" />
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{variant === "detail" && (
|
||||
<div class="space-y-4 rounded-xl p-4">
|
||||
<Skeleton class="h-6 w-1/2" />
|
||||
<div class="space-y-2">
|
||||
<Skeleton class="h-4 w-full" />
|
||||
<Skeleton class="h-4 w-full" />
|
||||
<Skeleton class="h-4 w-3/4" />
|
||||
</div>
|
||||
<Skeleton class="h-10 w-32" />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
))
|
||||
}
|
||||
</div>
|
||||
@@ -1,106 +0,0 @@
|
||||
---
|
||||
export interface Props {
|
||||
variant?: "default" | "secondary" | "success" | "warning" | "destructive" | "outline";
|
||||
size?: "sm" | "default";
|
||||
dot?: boolean;
|
||||
class?: string;
|
||||
}
|
||||
|
||||
const {
|
||||
variant = "default",
|
||||
size = "default",
|
||||
dot = false,
|
||||
class: className = "",
|
||||
} = Astro.props;
|
||||
---
|
||||
|
||||
<span class:list={[`badge badge--${variant} badge--${size}`, className]}>
|
||||
{dot && <span class="badge__dot" aria-hidden="true" />}
|
||||
<slot />
|
||||
</span>
|
||||
|
||||
<style is:global>
|
||||
.badge {
|
||||
--fs-xs: 0.75rem;
|
||||
--fs-xxs: 0.625rem;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 0.375rem;
|
||||
border-radius: 999px;
|
||||
border: 1px solid transparent;
|
||||
font-weight: var(--fw-medium, 500);
|
||||
transition:
|
||||
background-color var(--duration-fast, 150ms) var(--ease-out, ease-out),
|
||||
color var(--duration-fast, 150ms) var(--ease-out, ease-out);
|
||||
white-space: nowrap;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.badge--default {
|
||||
padding-inline: 0.625rem;
|
||||
padding-block: 0.125rem;
|
||||
font-size: var(--fs-xs);
|
||||
background-color: oklch(var(--primary));
|
||||
color: oklch(var(--primary-foreground));
|
||||
}
|
||||
|
||||
.badge--secondary {
|
||||
padding-inline: 0.625rem;
|
||||
padding-block: 0.125rem;
|
||||
font-size: var(--fs-xs);
|
||||
background-color: oklch(var(--muted));
|
||||
color: oklch(var(--muted-foreground));
|
||||
}
|
||||
|
||||
.badge--success {
|
||||
padding-inline: 0.625rem;
|
||||
padding-block: 0.125rem;
|
||||
font-size: var(--fs-xs);
|
||||
background-color: oklch(var(--success) / 0.15);
|
||||
color: oklch(var(--success));
|
||||
}
|
||||
|
||||
.badge--warning {
|
||||
padding-inline: 0.625rem;
|
||||
padding-block: 0.125rem;
|
||||
font-size: var(--fs-xs);
|
||||
background-color: oklch(var(--warning) / 0.15);
|
||||
color: oklch(var(--warning));
|
||||
}
|
||||
|
||||
.badge--destructive {
|
||||
padding-inline: 0.625rem;
|
||||
padding-block: 0.125rem;
|
||||
font-size: var(--fs-xs);
|
||||
background-color: oklch(var(--destructive) / 0.15);
|
||||
color: oklch(var(--destructive));
|
||||
}
|
||||
|
||||
.badge--outline {
|
||||
padding-inline: 0.625rem;
|
||||
padding-block: 0.125rem;
|
||||
font-size: var(--fs-xs);
|
||||
border-color: oklch(var(--border));
|
||||
background-color: transparent;
|
||||
color: oklch(var(--foreground));
|
||||
}
|
||||
|
||||
/* ── Size sm ───────────────────────────────────── */
|
||||
|
||||
.badge--sm {
|
||||
padding-inline: 0.375rem;
|
||||
padding-block: 0.0625rem;
|
||||
font-size: var(--fs-xxs);
|
||||
}
|
||||
|
||||
/* ── Dot indicator ─────────────────────────────── */
|
||||
|
||||
.badge__dot {
|
||||
display: inline-block;
|
||||
width: 0.375rem;
|
||||
height: 0.375rem;
|
||||
border-radius: 50%;
|
||||
background-color: currentColor;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
</style>
|
||||
@@ -1,146 +0,0 @@
|
||||
---
|
||||
export interface Props {
|
||||
variant?: "primary" | "secondary" | "destructive" | "outline" | "ghost";
|
||||
size?: "sm" | "default" | "lg" | "icon";
|
||||
disabled?: boolean;
|
||||
href?: string;
|
||||
class?: string;
|
||||
}
|
||||
|
||||
const {
|
||||
variant = "primary",
|
||||
size = "default",
|
||||
disabled = false,
|
||||
href,
|
||||
class: className = "",
|
||||
} = Astro.props;
|
||||
|
||||
const Tag = href ? "a" : "button";
|
||||
|
||||
const attrs: Record<string, string | number | boolean> = {
|
||||
class: `btn btn--${variant} btn--${size} ${className}`,
|
||||
};
|
||||
if (Tag === "button") {
|
||||
attrs.disabled = disabled;
|
||||
} else if (disabled) {
|
||||
attrs["aria-disabled"] = true;
|
||||
attrs.role = "link";
|
||||
}
|
||||
---
|
||||
|
||||
<Tag {...attrs}>
|
||||
<slot />
|
||||
</Tag>
|
||||
|
||||
<style is:global>
|
||||
.btn {
|
||||
--rd-md: calc(var(--radius, 1rem) - 2px);
|
||||
--fs-sm: 0.875rem;
|
||||
--fw-medium: 500;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 0.5rem;
|
||||
white-space: nowrap;
|
||||
border-radius: var(--rd-md);
|
||||
font-size: var(--fs-sm);
|
||||
font-weight: var(--fw-medium);
|
||||
line-height: 1.25rem;
|
||||
transition:
|
||||
background-color var(--duration-fast, 150ms) var(--ease-out, ease-out),
|
||||
color var(--duration-fast, 150ms) var(--ease-out, ease-out),
|
||||
border-color var(--duration-fast, 150ms) var(--ease-out, ease-out),
|
||||
box-shadow var(--duration-fast, 150ms) var(--ease-out, ease-out),
|
||||
transform var(--duration-fast, 150ms) var(--ease-out, ease-out);
|
||||
cursor: pointer;
|
||||
border: 1px solid transparent;
|
||||
text-decoration: none;
|
||||
outline: none;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.btn:focus-visible {
|
||||
outline: 2px solid oklch(var(--ring));
|
||||
outline-offset: 2px;
|
||||
}
|
||||
|
||||
.btn:active {
|
||||
transform: scale(0.97);
|
||||
}
|
||||
|
||||
.btn:disabled,
|
||||
.btn[aria-disabled="true"] {
|
||||
pointer-events: none;
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
/* ── Variants ──────────────────────────────────────────── */
|
||||
|
||||
.btn--primary {
|
||||
background-color: oklch(var(--primary));
|
||||
color: oklch(var(--primary-foreground));
|
||||
box-shadow: 0 1px 2px oklch(0 0 0 / 0.05);
|
||||
}
|
||||
.btn--primary:hover {
|
||||
background-color: oklch(var(--primary) / 0.9);
|
||||
}
|
||||
|
||||
.btn--secondary {
|
||||
background-color: oklch(var(--secondary));
|
||||
color: oklch(var(--secondary-foreground));
|
||||
}
|
||||
.btn--secondary:hover {
|
||||
background-color: oklch(var(--secondary) / 0.8);
|
||||
}
|
||||
|
||||
.btn--destructive {
|
||||
background-color: oklch(var(--destructive));
|
||||
color: oklch(var(--destructive-foreground));
|
||||
box-shadow: 0 1px 2px oklch(0 0 0 / 0.05);
|
||||
}
|
||||
.btn--destructive:hover {
|
||||
background-color: oklch(var(--destructive) / 0.9);
|
||||
}
|
||||
|
||||
.btn--outline {
|
||||
border-color: oklch(var(--border));
|
||||
background-color: oklch(var(--background));
|
||||
color: oklch(var(--foreground));
|
||||
}
|
||||
.btn--outline:hover {
|
||||
background-color: oklch(var(--accent));
|
||||
color: oklch(var(--accent-foreground));
|
||||
}
|
||||
|
||||
.btn--ghost {
|
||||
background-color: transparent;
|
||||
color: oklch(var(--foreground));
|
||||
}
|
||||
.btn--ghost:hover {
|
||||
background-color: oklch(var(--accent));
|
||||
color: oklch(var(--accent-foreground));
|
||||
}
|
||||
|
||||
/* ── Sizes ─────────────────────────────────────────────── */
|
||||
|
||||
.btn--sm {
|
||||
height: 2.25rem;
|
||||
padding-inline: 0.75rem;
|
||||
}
|
||||
|
||||
.btn--default {
|
||||
height: 2.5rem;
|
||||
padding-inline: 1rem;
|
||||
}
|
||||
|
||||
.btn--lg {
|
||||
height: 2.75rem;
|
||||
padding-inline: 2rem;
|
||||
}
|
||||
|
||||
.btn--icon {
|
||||
height: 2.5rem;
|
||||
width: 2.5rem;
|
||||
padding: 0;
|
||||
}
|
||||
</style>
|
||||
@@ -1,101 +0,0 @@
|
||||
---
|
||||
export interface Props {
|
||||
variant?: "default" | "elevated" | "glass" | "interactive";
|
||||
padding?: "none" | "sm" | "md" | "lg";
|
||||
class?: string;
|
||||
}
|
||||
|
||||
const {
|
||||
variant = "default",
|
||||
padding = "md",
|
||||
class: className = "",
|
||||
} = Astro.props;
|
||||
---
|
||||
|
||||
<div class:list={[`card card--${variant} card--pad-${padding}`, className]}>
|
||||
<slot />
|
||||
</div>
|
||||
|
||||
<style is:global>
|
||||
.card {
|
||||
--rd-lg: var(--radius, 1rem);
|
||||
border-radius: var(--rd-lg);
|
||||
transition:
|
||||
background-color var(--duration-normal, 300ms) var(--ease-out, ease-out),
|
||||
color var(--duration-normal, 300ms) var(--ease-out, ease-out),
|
||||
border-color var(--duration-normal, 300ms) var(--ease-out, ease-out),
|
||||
box-shadow var(--duration-normal, 300ms) var(--ease-out, ease-out),
|
||||
transform var(--duration-normal, 300ms) var(--ease-out, ease-out);
|
||||
}
|
||||
|
||||
/* ── Padding variants ──────────────────────────── */
|
||||
|
||||
.card--pad-none {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.card--pad-sm {
|
||||
padding: 0.75rem;
|
||||
}
|
||||
|
||||
.card--pad-md {
|
||||
padding: 1.5rem;
|
||||
}
|
||||
|
||||
.card--pad-lg {
|
||||
padding: 2rem;
|
||||
}
|
||||
|
||||
/* ── Variant: default ──────────────────────────── */
|
||||
|
||||
.card--default {
|
||||
background-color: oklch(var(--card));
|
||||
color: oklch(var(--card-foreground));
|
||||
border: 1px solid oklch(var(--border));
|
||||
box-shadow: 0 1px 2px oklch(0 0 0 / 0.05);
|
||||
}
|
||||
|
||||
/* ── Variant: elevated ─────────────────────────── */
|
||||
|
||||
.card--elevated {
|
||||
background-color: oklch(var(--card));
|
||||
color: oklch(var(--card-foreground));
|
||||
border: 1px solid oklch(var(--border));
|
||||
box-shadow:
|
||||
0 4px 6px -1px oklch(0 0 0 / 0.1),
|
||||
0 2px 4px -2px oklch(0 0 0 / 0.1);
|
||||
}
|
||||
|
||||
/* ── Variant: glass ────────────────────────────── */
|
||||
|
||||
.card--glass {
|
||||
background-color: oklch(var(--glass-bg));
|
||||
border: 1px solid oklch(var(--glass-border));
|
||||
backdrop-filter: blur(12px);
|
||||
-webkit-backdrop-filter: blur(12px);
|
||||
box-shadow: 0 4px 24px oklch(var(--glass-shadow));
|
||||
}
|
||||
|
||||
/* ── Variant: interactive ──────────────────────── */
|
||||
|
||||
.card--interactive {
|
||||
background-color: oklch(var(--card));
|
||||
color: oklch(var(--card-foreground));
|
||||
border: 1px solid oklch(var(--border));
|
||||
box-shadow: 0 1px 2px oklch(0 0 0 / 0.05);
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.card--interactive:hover {
|
||||
transform: translateY(-4px);
|
||||
box-shadow:
|
||||
0 10px 15px -3px oklch(0 0 0 / 0.1),
|
||||
0 4px 6px -4px oklch(0 0 0 / 0.1);
|
||||
border-color: oklch(var(--primary) / 0.3);
|
||||
}
|
||||
|
||||
.card--interactive:focus-visible {
|
||||
outline: 2px solid oklch(var(--ring));
|
||||
outline-offset: 2px;
|
||||
}
|
||||
</style>
|
||||
@@ -1,97 +0,0 @@
|
||||
---
|
||||
export interface Props {
|
||||
severity: "safe" | "low" | "medium" | "high" | "critical";
|
||||
class?: string;
|
||||
}
|
||||
|
||||
const { severity, class: className = "" } = Astro.props;
|
||||
|
||||
const ICONS: Record<string, string> = {
|
||||
safe: "✔",
|
||||
low: "↗",
|
||||
medium: "⚠",
|
||||
high: "⚡",
|
||||
critical: "✖",
|
||||
};
|
||||
|
||||
const icon = ICONS[severity] ?? "";
|
||||
---
|
||||
|
||||
<span
|
||||
class:list={[`severity-badge severity-badge--${severity}`, className]}
|
||||
data-severity={severity}
|
||||
role="status"
|
||||
>
|
||||
<span class="severity-badge__icon" aria-hidden="true" set:html={icon} />
|
||||
<span class="severity-badge__label"><slot /></span>
|
||||
</span>
|
||||
|
||||
<style is:global>
|
||||
.severity-badge {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 0.375rem;
|
||||
padding-inline: 0.5rem;
|
||||
padding-block: 0.2rem;
|
||||
border-radius: 999px;
|
||||
font-size: 0.75rem;
|
||||
font-weight: 500;
|
||||
border: 1px solid transparent;
|
||||
line-height: 1;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.severity-badge__icon {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 0.75rem;
|
||||
line-height: 1;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.severity-badge__label {
|
||||
text-transform: capitalize;
|
||||
}
|
||||
|
||||
/* ── Severity: safe → success ──────────────────── */
|
||||
|
||||
.severity-badge--safe {
|
||||
background-color: oklch(var(--success) / 0.15);
|
||||
color: oklch(var(--success));
|
||||
border-color: oklch(var(--success) / 0.25);
|
||||
}
|
||||
|
||||
/* ── Severity: low → info ──────────────────────── */
|
||||
|
||||
.severity-badge--low {
|
||||
background-color: oklch(var(--info) / 0.15);
|
||||
color: oklch(var(--info));
|
||||
border-color: oklch(var(--info) / 0.25);
|
||||
}
|
||||
|
||||
/* ── Severity: medium → warning ────────────────── */
|
||||
|
||||
.severity-badge--medium {
|
||||
background-color: oklch(var(--warning) / 0.15);
|
||||
color: oklch(var(--warning));
|
||||
border-color: oklch(var(--warning) / 0.25);
|
||||
}
|
||||
|
||||
/* ── Severity: high → destructive ──────────────── */
|
||||
|
||||
.severity-badge--high {
|
||||
background-color: oklch(var(--destructive) / 0.12);
|
||||
color: oklch(var(--destructive));
|
||||
border-color: oklch(var(--destructive) / 0.2);
|
||||
}
|
||||
|
||||
/* ── Severity: critical ────────────────────────── */
|
||||
|
||||
.severity-badge--critical {
|
||||
background-color: oklch(var(--destructive) / 0.25);
|
||||
color: oklch(var(--destructive));
|
||||
border-color: oklch(var(--destructive) / 0.4);
|
||||
font-weight: 600;
|
||||
}
|
||||
</style>
|
||||
@@ -1,74 +0,0 @@
|
||||
---
|
||||
export interface Props {
|
||||
variant?: "text" | "card" | "circle" | "rect";
|
||||
width?: string;
|
||||
height?: string;
|
||||
class?: string;
|
||||
}
|
||||
|
||||
const {
|
||||
variant = "text",
|
||||
width,
|
||||
height,
|
||||
class: className = "",
|
||||
} = Astro.props;
|
||||
|
||||
const style: Record<string, string> = {};
|
||||
if (width) style.width = width;
|
||||
if (height) style.height = height;
|
||||
---
|
||||
|
||||
<div
|
||||
aria-hidden="true"
|
||||
role="presentation"
|
||||
class:list={[`skeleton skeleton--${variant}`, className]}
|
||||
style={Object.keys(style).length > 0 ? style : undefined}
|
||||
>
|
||||
<slot />
|
||||
</div>
|
||||
|
||||
<style is:global>
|
||||
.skeleton {
|
||||
background: linear-gradient(
|
||||
90deg,
|
||||
oklch(var(--muted)) 25%,
|
||||
oklch(var(--muted-foreground) / 0.15) 50%,
|
||||
oklch(var(--muted)) 75%
|
||||
);
|
||||
background-size: 200% 100%;
|
||||
animation: shimmer 1.5s ease-in-out infinite;
|
||||
border-radius: 0.5rem;
|
||||
}
|
||||
|
||||
/* ── Variant: text (default) ───────────────────── */
|
||||
|
||||
.skeleton--text {
|
||||
width: 100%;
|
||||
height: 1rem;
|
||||
border-radius: calc(var(--radius, 1rem) - 4px);
|
||||
}
|
||||
|
||||
/* ── Variant: card ─────────────────────────────── */
|
||||
|
||||
.skeleton--card {
|
||||
width: 100%;
|
||||
height: 8rem;
|
||||
border-radius: var(--radius, 1rem);
|
||||
}
|
||||
|
||||
/* ── Variant: circle ───────────────────────────── */
|
||||
|
||||
.skeleton--circle {
|
||||
width: 2.5rem;
|
||||
height: 2.5rem;
|
||||
border-radius: 50%;
|
||||
}
|
||||
|
||||
/* ── Variant: rect ─────────────────────────────── */
|
||||
|
||||
.skeleton--rect {
|
||||
width: 100%;
|
||||
height: 3rem;
|
||||
border-radius: calc(var(--radius, 1rem) - 2px);
|
||||
}
|
||||
</style>
|
||||
@@ -1,60 +0,0 @@
|
||||
---
|
||||
export interface Props {
|
||||
size?: "sm" | "default" | "lg";
|
||||
class?: string;
|
||||
}
|
||||
|
||||
const { size = "default", class: className = "" } = Astro.props;
|
||||
---
|
||||
|
||||
<span
|
||||
aria-label="Loading"
|
||||
role="status"
|
||||
class:list={[`spinner spinner--${size}`, className]}
|
||||
>
|
||||
<span class="spinner__ring" aria-hidden="true" />
|
||||
</span>
|
||||
|
||||
<style is:global>
|
||||
@keyframes spin {
|
||||
to {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
|
||||
.spinner {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
.spinner__ring {
|
||||
display: block;
|
||||
border-radius: 50%;
|
||||
border-style: solid;
|
||||
border-color: oklch(var(--muted));
|
||||
border-top-color: oklch(var(--primary));
|
||||
animation: spin 0.6s linear infinite;
|
||||
}
|
||||
|
||||
/* ── Sizes ─────────────────────────────────────── */
|
||||
|
||||
.spinner--sm .spinner__ring {
|
||||
width: 1rem;
|
||||
height: 1rem;
|
||||
border-width: 2px;
|
||||
}
|
||||
|
||||
.spinner--default .spinner__ring {
|
||||
width: 1.5rem;
|
||||
height: 1.5rem;
|
||||
border-width: 2.5px;
|
||||
}
|
||||
|
||||
.spinner--lg .spinner__ring {
|
||||
width: 2.5rem;
|
||||
height: 2.5rem;
|
||||
border-width: 3px;
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user