Revert "feat: migrate frontend to Astro SSG with design system"

This reverts commit 8ad888da28.
This commit is contained in:
asepharyana
2026-07-02 03:54:44 +07:00
parent 880c68fbab
commit bf86fcda8e
110 changed files with 4980 additions and 3396 deletions
@@ -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>
File diff suppressed because one or more lines are too long
@@ -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: "&#x2714;",
low: "&#x2197;",
medium: "&#x26A0;",
high: "&#x26A1;",
critical: "&#x2716;",
};
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>