1
0
forked from wrenn/wrenn

Polish dashboard frontend: spacing, copy, resilience

- Increase content padding (p-7→p-8) and table cell padding (px-4→px-5,
  py-3→py-4 for data rows) across capsules, keys, and snapshots pages
- Improve animation performance: wrenn-glow uses opacity instead of
  box-shadow (compositor-only, no paint cost)
- Add prefers-reduced-motion media query covering inline style animations
- Fix OAuth error display on login page (read ?error= param on mount)
- Harden clipboard copy with try-catch and toast fallback
- Improve empty state copy, dialog microcopy, and error messages
- Add retry button to error banners on keys page
- Replace "All systems operational" footer bar with a clean 1px divider
- Fix text truncation on long capsule/snapshot names (min-w-0 + truncate)
This commit is contained in:
2026-03-24 12:33:18 +06:00
parent 71564b202e
commit b786a825d4
9 changed files with 304 additions and 256 deletions

View File

@ -48,6 +48,20 @@
--font-mono: 'JetBrains Mono Variable', monospace; --font-mono: 'JetBrains Mono Variable', monospace;
--font-brand: 'Alice', serif; --font-brand: 'Alice', serif;
/* Type scale — rem-based (root = 87.5%, giving 14px at browser default)
Heading tokens carry a default line-height; body/UI tokens inherit the global 1.6. */
--text-display: 2.571rem; /* ~36px — auth/login section headings */
--text-display--line-height: 1.1;
--text-page: 2rem; /* ~28px — page h1 titles */
--text-page--line-height: 1.15;
--text-heading: 1.429rem; /* ~20px — dialog headings, empty-state */
--text-heading--line-height: 1.25;
--text-body: 1rem; /* 14px — primary body, buttons, inputs */
--text-ui: 0.929rem; /* ~13px — nav labels, table cells, secondary */
--text-meta: 0.857rem; /* ~12px — key prefixes, minor info */
--text-label: 0.786rem; /* ~11px — uppercase section labels */
--text-badge: 0.714rem; /* ~10px — live badges, tiny indicators */
/* Radii */ /* Radii */
--radius-card: 8px; --radius-card: 8px;
--radius-input: 5px; --radius-input: 5px;
@ -62,9 +76,13 @@
/* Base styles */ /* Base styles */
html { html {
font-family: var(--font-sans); font-family: var(--font-sans);
font-size: 14px; font-size: 87.5%; /* 14px at browser default; scales with user text-size preferences */
line-height: 1.6;
color: var(--color-text-primary); color: var(--color-text-primary);
background-color: var(--color-bg-0); background-color: var(--color-bg-0);
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
text-rendering: optimizeLegibility;
} }
body { body {
@ -72,6 +90,11 @@ body {
min-height: 100vh; min-height: 100vh;
} }
/* Tabular figures on all mono text — numbers align in tables and metric displays */
.font-mono {
font-variant-numeric: tabular-nums;
}
/* Selection */ /* Selection */
::selection { ::selection {
background: rgba(94, 140, 88, 0.25); background: rgba(94, 140, 88, 0.25);
@ -97,14 +120,14 @@ body {
background: var(--color-bg-5); background: var(--color-bg-5);
} }
/* Live status dot glow animation */ /* Live status dot pulse animation — opacity-only for GPU compositor, zero paint cost */
@keyframes wrenn-glow { @keyframes wrenn-glow {
0%, 0%,
100% { 100% {
box-shadow: 0 0 6px rgba(94, 140, 88, 0.5); opacity: 1;
} }
50% { 50% {
box-shadow: 0 0 14px rgba(94, 140, 88, 0.2); opacity: 0.3;
} }
} }
@ -119,3 +142,15 @@ body {
transform: translateY(0); transform: translateY(0);
} }
} }
/* Respect user motion preferences — covers both CSS class animations and inline style animations */
@media (prefers-reduced-motion: reduce) {
*,
*::before,
*::after {
animation-duration: 0.01ms !important;
animation-iteration-count: 1 !important;
transition-duration: 0.01ms !important;
scroll-behavior: auto !important;
}
}

View File

@ -61,12 +61,12 @@
<!-- Header --> <!-- Header -->
<div class="mb-7"> <div class="mb-7">
<Dialog.Title <Dialog.Title
class="font-serif text-[24px] tracking-[-0.02em] text-[var(--color-text-bright)]" class="font-serif text-page tracking-[-0.02em] text-[var(--color-text-bright)]"
> >
{title} {title}
</Dialog.Title> </Dialog.Title>
<Dialog.Description <Dialog.Description
class="mt-1 text-[13px] text-[var(--color-text-secondary)]" class="mt-1 text-ui text-[var(--color-text-secondary)]"
> >
{subtitle} {subtitle}
</Dialog.Description> </Dialog.Description>
@ -75,7 +75,7 @@
<!-- GitHub OAuth --> <!-- GitHub OAuth -->
<button <button
type="button" type="button"
class="flex w-full items-center justify-center gap-2.5 rounded-[var(--radius-button)] border border-[var(--color-border-mid)] bg-[var(--color-bg-2)] px-4 py-2.5 text-[13px] font-medium text-[var(--color-text-bright)] transition-all duration-150 hover:border-[var(--color-accent)] hover:text-[var(--color-text-bright)]" class="flex w-full items-center justify-center gap-2.5 rounded-[var(--radius-button)] border border-[var(--color-border-mid)] bg-[var(--color-bg-2)] px-4 py-2.5 text-ui font-medium text-[var(--color-text-bright)] transition-all duration-150 hover:border-[var(--color-accent)] hover:text-[var(--color-text-bright)]"
> >
<IconGithub size={16} /> <IconGithub size={16} />
Continue with GitHub Continue with GitHub
@ -85,7 +85,7 @@
<div class="my-5 flex items-center gap-3"> <div class="my-5 flex items-center gap-3">
<div class="h-px flex-1 bg-[var(--color-border)]"></div> <div class="h-px flex-1 bg-[var(--color-border)]"></div>
<span <span
class="font-mono text-[10px] uppercase tracking-[0.1em] text-[var(--color-text-muted)]" class="font-mono text-badge uppercase tracking-[0.1em] text-[var(--color-text-muted)]"
>or</span >or</span
> >
<div class="h-px flex-1 bg-[var(--color-border)]"></div> <div class="h-px flex-1 bg-[var(--color-border)]"></div>
@ -105,7 +105,7 @@
bind:value={name} bind:value={name}
placeholder="Full name" placeholder="Full name"
autocomplete="name" autocomplete="name"
class="w-full rounded-[var(--radius-input)] border border-[var(--color-border)] bg-[var(--color-bg-2)] py-2.5 pl-9 pr-3 text-[13px] text-[var(--color-text-bright)] outline-none transition-all duration-150 placeholder:text-[var(--color-text-muted)] focus:border-[var(--color-accent)]" class="w-full rounded-[var(--radius-input)] border border-[var(--color-border)] bg-[var(--color-bg-2)] py-2.5 pl-9 pr-3 text-ui text-[var(--color-text-bright)] outline-none transition-all duration-150 placeholder:text-[var(--color-text-muted)] focus:border-[var(--color-accent)]"
/> />
</div> </div>
{/if} {/if}
@ -121,7 +121,7 @@
bind:value={email} bind:value={email}
placeholder="Email address" placeholder="Email address"
autocomplete="email" autocomplete="email"
class="w-full rounded-[var(--radius-input)] border border-[var(--color-border)] bg-[var(--color-bg-2)] py-2.5 pl-9 pr-3 text-[13px] text-[var(--color-text-bright)] outline-none transition-all duration-150 placeholder:text-[var(--color-text-muted)] focus:border-[var(--color-accent)]" class="w-full rounded-[var(--radius-input)] border border-[var(--color-border)] bg-[var(--color-bg-2)] py-2.5 pl-9 pr-3 text-ui text-[var(--color-text-bright)] outline-none transition-all duration-150 placeholder:text-[var(--color-text-muted)] focus:border-[var(--color-accent)]"
/> />
</div> </div>
@ -136,7 +136,7 @@
bind:value={password} bind:value={password}
placeholder="Password" placeholder="Password"
autocomplete={mode === 'signin' ? 'current-password' : 'new-password'} autocomplete={mode === 'signin' ? 'current-password' : 'new-password'}
class="w-full rounded-[var(--radius-input)] border border-[var(--color-border)] bg-[var(--color-bg-2)] py-2.5 pl-9 pr-10 text-[13px] text-[var(--color-text-bright)] outline-none transition-all duration-150 placeholder:text-[var(--color-text-muted)] focus:border-[var(--color-accent)]" class="w-full rounded-[var(--radius-input)] border border-[var(--color-border)] bg-[var(--color-bg-2)] py-2.5 pl-9 pr-10 text-ui text-[var(--color-text-bright)] outline-none transition-all duration-150 placeholder:text-[var(--color-text-muted)] focus:border-[var(--color-accent)]"
/> />
<button <button
type="button" type="button"
@ -156,7 +156,7 @@
<div class="flex justify-end"> <div class="flex justify-end">
<button <button
type="button" type="button"
class="text-[12px] text-[var(--color-text-secondary)] transition-colors duration-150 hover:text-[var(--color-accent-mid)]" class="text-meta text-[var(--color-text-secondary)] transition-colors duration-150 hover:text-[var(--color-accent-mid)]"
> >
Forgot password? Forgot password?
</button> </button>
@ -165,14 +165,14 @@
<button <button
type="submit" type="submit"
class="!mt-5 w-full rounded-[var(--radius-button)] bg-[var(--color-accent)] px-4 py-2.5 text-[13px] font-semibold text-white transition-all duration-150 hover:brightness-115 hover:-translate-y-px active:translate-y-0" class="!mt-5 w-full rounded-[var(--radius-button)] bg-[var(--color-accent)] px-4 py-2.5 text-ui font-semibold text-white transition-all duration-150 hover:brightness-115 hover:-translate-y-px active:translate-y-0"
> >
{submitLabel} {submitLabel}
</button> </button>
</form> </form>
<!-- Switch mode --> <!-- Switch mode -->
<p class="mt-5 text-center text-[12px] text-[var(--color-text-secondary)]"> <p class="mt-5 text-center text-meta text-[var(--color-text-secondary)]">
{switchText} {switchText}
<button <button
type="button" type="button"

View File

@ -74,7 +74,7 @@
alt="Wrenn" alt="Wrenn"
class="h-7 w-7 shrink-0 rounded-[var(--radius-logo)]" class="h-7 w-7 shrink-0 rounded-[var(--radius-logo)]"
/> />
<span class="font-brand text-[15px] text-[var(--color-text-bright)]">Wrenn</span> <span class="font-brand text-[1.286rem] text-[var(--color-text-bright)]">Wrenn</span>
</div> </div>
{/if} {/if}
<button <button
@ -95,18 +95,18 @@
: 'gap-2 px-2.5'}" : 'gap-2 px-2.5'}"
> >
<div <div
class="flex h-6 w-6 shrink-0 items-center justify-center rounded-[var(--radius-avatar)] bg-[var(--color-bg-4)] text-[10px] font-bold uppercase text-[var(--color-text-secondary)]" class="flex h-6 w-6 shrink-0 items-center justify-center rounded-[var(--radius-avatar)] bg-[var(--color-bg-4)] text-badge font-bold uppercase text-[var(--color-text-secondary)]"
> >
{currentTeam[0]} {currentTeam[0]}
</div> </div>
{#if !collapsed} {#if !collapsed}
<div class="min-w-0 flex-1 overflow-hidden whitespace-nowrap"> <div class="min-w-0 flex-1 overflow-hidden whitespace-nowrap">
<div <div
class="text-[11px] font-semibold uppercase tracking-[0.06em] text-[var(--color-text-tertiary)]" class="text-label font-semibold uppercase tracking-[0.06em] text-[var(--color-text-tertiary)]"
> >
Team Team
</div> </div>
<div class="truncate text-[13px] text-[var(--color-text-primary)]"> <div class="truncate text-ui text-[var(--color-text-primary)]">
{currentTeam} {currentTeam}
</div> </div>
</div> </div>
@ -126,20 +126,20 @@
style="animation: popoverSlideIn 150ms ease" style="animation: popoverSlideIn 150ms ease"
> >
<div <div
class="mb-1 px-2.5 py-1 text-[11px] font-semibold uppercase tracking-[0.06em] text-[var(--color-text-tertiary)]" class="mb-1 px-2.5 py-1 text-label font-semibold uppercase tracking-[0.06em] text-[var(--color-text-tertiary)]"
> >
Teams Teams
</div> </div>
{#each teams as team} {#each teams as team}
<button <button
class="flex w-full items-center gap-2.5 rounded-[var(--radius-input)] px-2.5 py-2 text-[13px] transition-colors duration-150 hover:bg-[var(--color-bg-3)] {team === class="flex w-full items-center gap-2.5 rounded-[var(--radius-input)] px-2.5 py-2 text-ui transition-colors duration-150 hover:bg-[var(--color-bg-3)] {team ===
currentTeam currentTeam
? 'bg-[var(--color-accent-glow)]' ? 'bg-[var(--color-accent-glow)]'
: ''}" : ''}"
onclick={() => (teamPopoverOpen = false)} onclick={() => (teamPopoverOpen = false)}
> >
<div <div
class="flex h-5 w-5 items-center justify-center rounded-[var(--radius-avatar)] text-[9px] font-bold uppercase text-white {team === class="flex h-5 w-5 items-center justify-center rounded-[var(--radius-avatar)] text-badge font-bold uppercase text-white {team ===
currentTeam currentTeam
? 'bg-[var(--color-accent)]' ? 'bg-[var(--color-accent)]'
: 'bg-[var(--color-bg-5)]'}" : 'bg-[var(--color-bg-5)]'}"
@ -157,7 +157,7 @@
{/each} {/each}
<div class="mt-0.5 border-t border-[var(--color-border)] pt-0.5"> <div class="mt-0.5 border-t border-[var(--color-border)] pt-0.5">
<button <button
class="flex w-full items-center gap-2.5 rounded-[var(--radius-input)] px-2.5 py-2 text-[13px] text-[var(--color-text-secondary)] transition-colors duration-150 hover:bg-[var(--color-bg-3)] hover:text-[var(--color-text-primary)]" class="flex w-full items-center gap-2.5 rounded-[var(--radius-input)] px-2.5 py-2 text-ui text-[var(--color-text-secondary)] transition-colors duration-150 hover:bg-[var(--color-bg-3)] hover:text-[var(--color-text-primary)]"
> >
<IconPlus size={14} /> <IconPlus size={14} />
Create team Create team
@ -186,7 +186,7 @@
title={collapsed ? 'Docs' : undefined} title={collapsed ? 'Docs' : undefined}
> >
<IconDocs size={16} class="shrink-0 opacity-50 transition-opacity duration-150 group-hover:opacity-100" /> <IconDocs size={16} class="shrink-0 opacity-50 transition-opacity duration-150 group-hover:opacity-100" />
{#if !collapsed}<span class="text-[13px]">Docs</span>{/if} {#if !collapsed}<span class="text-ui">Docs</span>{/if}
</a> </a>
<a <a
href="/dashboard/notifications" href="/dashboard/notifications"
@ -194,7 +194,7 @@
title={collapsed ? 'Notifications' : undefined} title={collapsed ? 'Notifications' : undefined}
> >
<IconBell size={16} class="shrink-0 opacity-50 transition-opacity duration-150 group-hover:opacity-100" /> <IconBell size={16} class="shrink-0 opacity-50 transition-opacity duration-150 group-hover:opacity-100" />
{#if !collapsed}<span class="text-[13px]">Notifications</span>{/if} {#if !collapsed}<span class="text-ui">Notifications</span>{/if}
</a> </a>
<a <a
href="/dashboard/settings" href="/dashboard/settings"
@ -202,7 +202,7 @@
title={collapsed ? 'Settings' : undefined} title={collapsed ? 'Settings' : undefined}
> >
<IconSettings size={16} class="shrink-0 opacity-50 transition-opacity duration-150 group-hover:opacity-100" /> <IconSettings size={16} class="shrink-0 opacity-50 transition-opacity duration-150 group-hover:opacity-100" />
{#if !collapsed}<span class="text-[13px]">Settings</span>{/if} {#if !collapsed}<span class="text-ui">Settings</span>{/if}
</a> </a>
</div> </div>
@ -214,11 +214,11 @@
> >
{#if !collapsed} {#if !collapsed}
<div <div
class="flex h-6 w-6 shrink-0 items-center justify-center rounded-full bg-[var(--color-bg-4)] text-[10px] font-bold uppercase text-[var(--color-text-secondary)]" class="flex h-6 w-6 shrink-0 items-center justify-center rounded-full bg-[var(--color-bg-4)] text-badge font-bold uppercase text-[var(--color-text-secondary)]"
> >
{userName[0] ?? ''} {userName[0] ?? ''}
</div> </div>
<span class="flex-1 truncate text-[13px] text-[var(--color-text-secondary)]"> <span class="flex-1 truncate text-ui text-[var(--color-text-secondary)]">
{userName} {userName}
</span> </span>
{/if} {/if}
@ -242,7 +242,7 @@
{/if} {/if}
{:else} {:else}
<div <div
class="mb-1 px-2.5 py-1.5 text-[11px] font-semibold uppercase tracking-[0.06em] text-[var(--color-text-tertiary)]" class="mb-1 px-2.5 py-1.5 text-label font-semibold uppercase tracking-[0.06em] text-[var(--color-text-tertiary)]"
> >
{label} {label}
</div> </div>
@ -263,7 +263,7 @@
{/if} {/if}
<item.icon size={16} class="shrink-0 text-[var(--color-accent-bright)]" /> <item.icon size={16} class="shrink-0 text-[var(--color-accent-bright)]" />
{#if !collapsed} {#if !collapsed}
<span class="text-[13px] font-medium text-[var(--color-accent-bright)]"> <span class="text-ui font-medium text-[var(--color-accent-bright)]">
{item.label} {item.label}
</span> </span>
{/if} {/if}
@ -282,7 +282,7 @@
/> />
{#if !collapsed} {#if !collapsed}
<span <span
class="text-[13px] text-[var(--color-text-primary)] transition-colors duration-150 group-hover:text-[var(--color-text-bright)]" class="text-ui text-[var(--color-text-primary)] transition-colors duration-150 group-hover:text-[var(--color-text-bright)]"
> >
{item.label} {item.label}
</span> </span>

View File

@ -5,7 +5,7 @@
<div class="pointer-events-none fixed bottom-6 right-6 z-[100] flex flex-col-reverse gap-2"> <div class="pointer-events-none fixed bottom-6 right-6 z-[100] flex flex-col-reverse gap-2">
{#each toast.list as t (t.id)} {#each toast.list as t (t.id)}
<div <div
class="pointer-events-auto flex min-w-[280px] max-w-[400px] items-start gap-3 rounded-[var(--radius-card)] border bg-[var(--color-bg-2)] px-4 py-3 text-[13px] {t.type === 'error' class="pointer-events-auto flex min-w-[280px] max-w-[400px] items-start gap-3 rounded-[var(--radius-card)] border bg-[var(--color-bg-2)] px-4 py-3 text-ui {t.type === 'error'
? 'border-[var(--color-red)]/30 text-[var(--color-red)]' ? 'border-[var(--color-red)]/30 text-[var(--color-red)]'
: 'border-[var(--color-accent)]/30 text-[var(--color-accent-bright)]'}" : 'border-[var(--color-accent)]/30 text-[var(--color-accent-bright)]'}"
style="animation: fadeUp 0.2s ease both" style="animation: fadeUp 0.2s ease both"

View File

@ -24,5 +24,5 @@
</script> </script>
<div class="flex min-h-screen items-center justify-center"> <div class="flex min-h-screen items-center justify-center">
<p class="text-[13px] text-[var(--color-text-secondary)]">Signing you in...</p> <p class="text-ui text-[var(--color-text-secondary)]">Signing you in...</p>
</div> </div>

View File

@ -282,14 +282,14 @@
<div class="flex flex-1 flex-col overflow-hidden"> <div class="flex flex-1 flex-col overflow-hidden">
<main class="flex-1 overflow-y-auto bg-[var(--color-bg-0)]"> <main class="flex-1 overflow-y-auto bg-[var(--color-bg-0)]">
<!-- Header area --> <!-- Header area -->
<div class="px-7 pt-6"> <div class="px-7 pt-8">
<!-- Top row: title + status chip --> <!-- Top row: title + status chip -->
<div class="flex items-center justify-between"> <div class="flex items-center justify-between">
<div> <div>
<h1 class="font-serif text-[24px] tracking-[-0.02em] text-[var(--color-text-bright)]"> <h1 class="font-serif text-page tracking-[-0.02em] text-[var(--color-text-bright)]">
Capsules Capsules
</h1> </h1>
<p class="mt-1 text-[13px] text-[var(--color-text-tertiary)]"> <p class="mt-2 text-ui text-[var(--color-text-tertiary)]">
Isolated VMs you can start, pause, and snapshot on demand. Isolated VMs you can start, pause, and snapshot on demand.
</p> </p>
</div> </div>
@ -306,17 +306,17 @@
></span> ></span>
<span class="relative inline-flex h-[7px] w-[7px] rounded-full bg-[var(--color-accent)]"></span> <span class="relative inline-flex h-[7px] w-[7px] rounded-full bg-[var(--color-accent)]"></span>
</span> </span>
<span class="font-mono text-[14px] font-semibold text-[var(--color-accent-bright)]">{runningCount}</span> <span class="font-mono text-body font-semibold text-[var(--color-accent-bright)]">{runningCount}</span>
<span class="text-[13px] text-[var(--color-text-secondary)]">concurrent capsules</span> <span class="text-ui text-[var(--color-text-secondary)]">concurrent capsules</span>
</div> </div>
</div> </div>
</div> </div>
<!-- Tab bar --> <!-- Tab bar -->
<div class="mt-4 flex gap-1 border-b border-[var(--color-border)]"> <div class="mt-5 flex gap-1 border-b border-[var(--color-border)]">
<button <button
onclick={() => (activeTab = 'list')} onclick={() => (activeTab = 'list')}
class="flex items-center gap-2 border-b-2 px-4 py-2.5 text-[13px] font-medium transition-colors duration-150 {activeTab === 'list' class="flex items-center gap-2 border-b-2 px-4 py-2.5 text-ui font-medium transition-colors duration-150 {activeTab === 'list'
? 'border-[var(--color-accent)] text-[var(--color-accent-bright)]' ? 'border-[var(--color-accent)] text-[var(--color-accent-bright)]'
: 'border-transparent text-[var(--color-text-secondary)] hover:text-[var(--color-text-primary)]'}" : 'border-transparent text-[var(--color-text-secondary)] hover:text-[var(--color-text-primary)]'}"
> >
@ -328,7 +328,7 @@
</button> </button>
<button <button
onclick={() => (activeTab = 'stats')} onclick={() => (activeTab = 'stats')}
class="flex items-center gap-2 border-b-2 px-4 py-2.5 text-[13px] font-medium transition-colors duration-150 {activeTab === 'stats' class="flex items-center gap-2 border-b-2 px-4 py-2.5 text-ui font-medium transition-colors duration-150 {activeTab === 'stats'
? 'border-[var(--color-accent)] text-[var(--color-accent-bright)]' ? 'border-[var(--color-accent)] text-[var(--color-accent-bright)]'
: 'border-transparent text-[var(--color-text-secondary)] hover:text-[var(--color-text-primary)]'}" : 'border-transparent text-[var(--color-text-secondary)] hover:text-[var(--color-text-primary)]'}"
> >
@ -342,7 +342,7 @@
<!-- Tab content --> <!-- Tab content -->
{#if activeTab === 'stats'} {#if activeTab === 'stats'}
<div class="p-7 space-y-5" style="animation: fadeUp 0.35s ease both"> <div class="p-8 space-y-5" style="animation: fadeUp 0.35s ease both">
<div class="flex overflow-hidden rounded-[var(--radius-card)] border border-[var(--color-border)]"> <div class="flex overflow-hidden rounded-[var(--radius-card)] border border-[var(--color-border)]">
{@render metricCell('Concurrent Capsules', String(runningCount), '5-sec avg', 'limit: 20', true)} {@render metricCell('Concurrent Capsules', String(runningCount), '5-sec avg', 'limit: 20', true)}
{@render metricCell('Start Rate / Second', '0.000', '5-sec avg', null, true)} {@render metricCell('Start Rate / Second', '0.000', '5-sec avg', null, true)}
@ -353,7 +353,7 @@
{@render chartCard('Start Rate Per Second', '0.000', 'average')} {@render chartCard('Start Rate Per Second', '0.000', 'average')}
</div> </div>
{:else} {:else}
<div class="p-7" style="animation: fadeUp 0.35s ease both"> <div class="p-8" style="animation: fadeUp 0.35s ease both">
<!-- Search bar + controls --> <!-- Search bar + controls -->
<div class="mb-4 flex items-center gap-3"> <div class="mb-4 flex items-center gap-3">
<div class="relative flex-1 max-w-[300px]"> <div class="relative flex-1 max-w-[300px]">
@ -364,10 +364,10 @@
type="text" type="text"
placeholder="Search by ID..." placeholder="Search by ID..."
bind:value={searchQuery} bind:value={searchQuery}
class="w-full rounded-[var(--radius-input)] border border-[var(--color-border)] bg-[var(--color-bg-2)] py-2 pl-9 pr-3 font-mono text-[13px] text-[var(--color-text-bright)] outline-none placeholder:text-[var(--color-text-muted)] transition-colors duration-150 focus:border-[var(--color-accent)]" class="w-full rounded-[var(--radius-input)] border border-[var(--color-border)] bg-[var(--color-bg-2)] py-2 pl-9 pr-3 font-mono text-ui text-[var(--color-text-bright)] outline-none placeholder:text-[var(--color-text-muted)] transition-colors duration-150 focus:border-[var(--color-accent)]"
/> />
</div> </div>
<span class="text-[13px] text-[var(--color-text-secondary)]">{filteredCapsules.length} total</span> <span class="text-ui text-[var(--color-text-secondary)]">{filteredCapsules.length} total</span>
<div class="flex-1"></div> <div class="flex-1"></div>
@ -390,7 +390,7 @@
<!-- Auto-refresh countdown toggle --> <!-- Auto-refresh countdown toggle -->
<button <button
onclick={toggleAutoRefresh} onclick={toggleAutoRefresh}
class="flex items-center gap-1.5 rounded-[var(--radius-button)] border px-2.5 py-1.5 font-mono text-[11px] transition-colors duration-150 class="flex items-center gap-1.5 rounded-[var(--radius-button)] border px-2.5 py-1.5 font-mono text-label transition-colors duration-150
{autoRefresh {autoRefresh
? 'border-[var(--color-accent)]/30 text-[var(--color-accent-mid)] hover:border-[var(--color-accent)]/50' ? 'border-[var(--color-accent)]/30 text-[var(--color-accent-mid)] hover:border-[var(--color-accent)]/50'
: 'border-[var(--color-border)] text-[var(--color-text-muted)] hover:border-[var(--color-border-mid)] hover:text-[var(--color-text-secondary)]'}" : 'border-[var(--color-border)] text-[var(--color-text-muted)] hover:border-[var(--color-border-mid)] hover:text-[var(--color-text-secondary)]'}"
@ -405,7 +405,7 @@
<button <button
onclick={() => { showCreateDialog = true; createError = null; }} onclick={() => { showCreateDialog = true; createError = null; }}
class="flex items-center gap-2 rounded-[var(--radius-button)] bg-[var(--color-accent)] px-4 py-2 text-[13px] font-semibold text-white transition-all duration-150 hover:brightness-115 hover:-translate-y-px active:translate-y-0" class="flex items-center gap-2 rounded-[var(--radius-button)] bg-[var(--color-accent)] px-4 py-2 text-ui font-semibold text-white transition-all duration-150 hover:brightness-115 hover:-translate-y-px active:translate-y-0"
> >
<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"> <svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
<line x1="12" y1="5" x2="12" y2="19" /><line x1="5" y1="12" x2="19" y2="12" /> <line x1="12" y1="5" x2="12" y2="19" /><line x1="5" y1="12" x2="19" y2="12" />
@ -415,7 +415,7 @@
</div> </div>
{#if error} {#if error}
<div class="mb-4 rounded-[var(--radius-card)] border border-[var(--color-red)]/30 bg-[var(--color-red)]/5 px-4 py-3 text-[13px] text-[var(--color-red)]"> <div class="mb-4 rounded-[var(--radius-card)] border border-[var(--color-red)]/30 bg-[var(--color-red)]/5 px-4 py-3 text-ui text-[var(--color-red)]">
{error} {error}
</div> </div>
{/if} {/if}
@ -424,8 +424,8 @@
<div class="rounded-[var(--radius-card)] border border-[var(--color-border)] overflow-hidden"> <div class="rounded-[var(--radius-card)] border border-[var(--color-border)] overflow-hidden">
<!-- Table header --> <!-- Table header -->
<div class="grid grid-cols-[1.6fr_0.8fr_0.5fr_0.5fr_0.6fr_1fr_0.9fr] rounded-t-[var(--radius-card)] border-b border-[var(--color-border)] bg-[var(--color-bg-3)]"> <div class="grid grid-cols-[1.6fr_0.8fr_0.5fr_0.5fr_0.6fr_1fr_0.9fr] rounded-t-[var(--radius-card)] border-b border-[var(--color-border)] bg-[var(--color-bg-3)]">
<div class="px-4 py-[11px] text-[11px] font-semibold uppercase tracking-[0.05em] text-[var(--color-text-muted)]">ID</div> <div class="px-5 py-3 text-label font-semibold uppercase tracking-[0.05em] text-[var(--color-text-muted)]">ID</div>
<div class="px-4 py-[11px] text-[11px] font-semibold uppercase tracking-[0.05em] text-[var(--color-text-muted)]">Template</div> <div class="px-5 py-3 text-label font-semibold uppercase tracking-[0.05em] text-[var(--color-text-muted)]">Template</div>
{@render sortableHeader('CPU', 'vcpus')} {@render sortableHeader('CPU', 'vcpus')}
{@render sortableHeader('Memory', 'memory_mb')} {@render sortableHeader('Memory', 'memory_mb')}
{@render sortableHeader('Idle Timeout', 'timeout_sec')} {@render sortableHeader('Idle Timeout', 'timeout_sec')}
@ -435,7 +435,7 @@
{#if loading && capsules.length === 0} {#if loading && capsules.length === 0}
<div class="flex items-center justify-center py-16"> <div class="flex items-center justify-center py-16">
<div class="flex items-center gap-3 text-[13px] text-[var(--color-text-secondary)]"> <div class="flex items-center gap-3 text-ui text-[var(--color-text-secondary)]">
<svg class="animate-spin" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"> <svg class="animate-spin" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="M21 12a9 9 0 1 1-6.219-8.56" /> <path d="M21 12a9 9 0 1 1-6.219-8.56" />
</svg> </svg>
@ -451,17 +451,17 @@
<line x1="12" y1="17" x2="12" y2="21" /> <line x1="12" y1="17" x2="12" y2="21" />
</svg> </svg>
</div> </div>
<p class="font-serif text-[20px] tracking-[-0.02em] text-[var(--color-text-bright)]"> <p class="font-serif text-heading tracking-[-0.02em] text-[var(--color-text-bright)]">
No capsules yet No capsules yet
</p> </p>
<p class="mt-1.5 text-[13px] text-[var(--color-text-tertiary)]"> <p class="mt-1.5 text-ui text-[var(--color-text-tertiary)]">
Active capsules will appear here. Launch a capsule to start running isolated code.
</p> </p>
<button <button
onclick={() => { showCreateDialog = true; createError = null; }} onclick={() => { showCreateDialog = true; createError = null; }}
class="mt-6 flex items-center gap-2 rounded-[var(--radius-button)] bg-[var(--color-accent)] px-5 py-2.5 text-[13px] font-semibold text-white transition-all duration-150 hover:brightness-115 hover:-translate-y-px active:translate-y-0" class="mt-6 flex items-center gap-2 rounded-[var(--radius-button)] bg-[var(--color-accent)] px-5 py-2.5 text-ui font-semibold text-white transition-all duration-150 hover:brightness-115 hover:-translate-y-px active:translate-y-0"
> >
Create a Capsule Launch a Capsule
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"> <svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
<line x1="12" y1="5" x2="12" y2="19" /><line x1="5" y1="12" x2="19" y2="12" /> <line x1="12" y1="5" x2="12" y2="19" /><line x1="5" y1="12" x2="19" y2="12" />
</svg> </svg>
@ -474,7 +474,7 @@
style="animation: fadeUp 0.35s ease both; animation-delay: {i * 40}ms" style="animation: fadeUp 0.35s ease both; animation-delay: {i * 40}ms"
> >
<!-- ID with status dot --> <!-- ID with status dot -->
<div class="flex items-center gap-2.5 px-4 py-3"> <div class="flex items-center gap-2.5 px-5 py-4">
{#if capsule.status === 'running'} {#if capsule.status === 'running'}
<span class="relative flex h-[6px] w-[6px] shrink-0"> <span class="relative flex h-[6px] w-[6px] shrink-0">
<span class="absolute inline-flex h-full w-full rounded-full bg-[var(--color-accent)]" style="animation: wrenn-glow 2.5s ease-in-out infinite"></span> <span class="absolute inline-flex h-full w-full rounded-full bg-[var(--color-accent)]" style="animation: wrenn-glow 2.5s ease-in-out infinite"></span>
@ -485,41 +485,41 @@
{:else} {:else}
<span class="inline-flex h-[6px] w-[6px] shrink-0 rounded-full bg-[var(--color-text-muted)]"></span> <span class="inline-flex h-[6px] w-[6px] shrink-0 rounded-full bg-[var(--color-text-muted)]"></span>
{/if} {/if}
<span class="font-mono text-[13px] text-[var(--color-text-bright)]">{capsule.id}</span> <span class="font-mono text-ui text-[var(--color-text-bright)]">{capsule.id}</span>
</div> </div>
<!-- Template --> <!-- Template -->
<div class="px-4 py-3"> <div class="min-w-0 px-5 py-4">
<span class="text-[13px] text-[var(--color-text-secondary)]">{capsule.template}</span> <span class="block truncate text-ui text-[var(--color-text-secondary)]">{capsule.template}</span>
</div> </div>
<!-- CPU --> <!-- CPU -->
<div class="px-4 py-3"> <div class="px-5 py-4">
<span class="font-mono text-[13px] text-[var(--color-text-secondary)]">{capsule.vcpus}</span> <span class="font-mono text-ui text-[var(--color-text-secondary)]">{capsule.vcpus}</span>
</div> </div>
<!-- Memory --> <!-- Memory -->
<div class="px-4 py-3"> <div class="px-5 py-4">
<span class="font-mono text-[13px] text-[var(--color-text-secondary)]">{capsule.memory_mb}MB</span> <span class="font-mono text-ui text-[var(--color-text-secondary)]">{capsule.memory_mb}MB</span>
</div> </div>
<!-- Idle Timeout --> <!-- Idle Timeout -->
<div class="px-4 py-3"> <div class="px-5 py-4">
<span class="font-mono text-[13px] text-[var(--color-text-secondary)]">{capsule.timeout_sec ? `${capsule.timeout_sec}s` : '—'}</span> <span class="font-mono text-ui text-[var(--color-text-secondary)]">{capsule.timeout_sec ? `${capsule.timeout_sec}s` : '—'}</span>
</div> </div>
<!-- Started --> <!-- Started -->
<div class="px-4 py-3"> <div class="px-5 py-4">
<span class="text-[13px] text-[var(--color-text-secondary)]" title={capsule.started_at ?? ''}>{formatTime(capsule.started_at)}</span> <span class="text-ui text-[var(--color-text-secondary)]" title={capsule.started_at ?? ''}>{formatTime(capsule.started_at)}</span>
{#if capsule.last_active_at} {#if capsule.last_active_at}
<span class="ml-1.5 text-[11px] text-[var(--color-text-muted)]">{timeAgo(capsule.last_active_at)}</span> <span class="ml-1.5 text-label text-[var(--color-text-muted)]">{timeAgo(capsule.last_active_at)}</span>
{/if} {/if}
</div> </div>
<!-- Status button with popover --> <!-- Status button with popover -->
<div class="relative px-4 py-3 status-menu-container"> <div class="relative px-5 py-4 status-menu-container">
{#if actionLoading === capsule.id} {#if actionLoading === capsule.id}
<span class="inline-flex items-center gap-1.5 text-[13px] text-[var(--color-text-secondary)]"> <span class="inline-flex items-center gap-1.5 text-ui text-[var(--color-text-secondary)]">
<svg class="animate-spin" width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"> <svg class="animate-spin" width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="M21 12a9 9 0 1 1-6.219-8.56" /> <path d="M21 12a9 9 0 1 1-6.219-8.56" />
</svg> </svg>
@ -536,7 +536,7 @@
openMenuId = capsule.id; openMenuId = capsule.id;
} }
}} }}
class="inline-flex items-center gap-1.5 rounded-[var(--radius-button)] border border-[var(--color-border)] bg-[var(--color-bg-2)] px-2.5 py-1 text-[11px] font-semibold uppercase tracking-[0.04em] text-[var(--color-text-secondary)] transition-colors duration-150 hover:border-[var(--color-border-mid)] hover:text-[var(--color-text-primary)]" class="inline-flex items-center gap-1.5 rounded-[var(--radius-button)] border border-[var(--color-border)] bg-[var(--color-bg-2)] px-2.5 py-1 text-label font-semibold uppercase tracking-[0.04em] text-[var(--color-text-secondary)] transition-colors duration-150 hover:border-[var(--color-border-mid)] hover:text-[var(--color-text-primary)]"
> >
{capsule.status} {capsule.status}
<svg <svg
@ -563,7 +563,7 @@
> >
<div class="flex items-center gap-1.5"> <div class="flex items-center gap-1.5">
<span class="inline-flex h-[5px] w-[5px] rounded-full bg-[var(--color-accent)]"></span> <span class="inline-flex h-[5px] w-[5px] rounded-full bg-[var(--color-accent)]"></span>
<span class="font-mono text-[11px] uppercase tracking-[0.04em] text-[var(--color-text-secondary)]">All systems operational</span> <span class="font-mono text-label uppercase tracking-[0.04em] text-[var(--color-text-secondary)]">All systems operational</span>
</div> </div>
</footer> </footer>
</div> </div>
@ -580,7 +580,7 @@
{#if openCapsule.status === 'running'} {#if openCapsule.status === 'running'}
<button <button
onclick={() => handlePause(openCapsule.id)} onclick={() => handlePause(openCapsule.id)}
class="flex w-full items-center gap-2.5 px-3 py-2 text-[12px] text-[var(--color-text-secondary)] transition-colors duration-150 hover:bg-[var(--color-bg-3)] hover:text-[var(--color-text-primary)]" class="flex w-full items-center gap-2.5 px-3 py-2 text-meta text-[var(--color-text-secondary)] transition-colors duration-150 hover:bg-[var(--color-bg-3)] hover:text-[var(--color-text-primary)]"
> >
<svg width="13" height="13" viewBox="0 0 24 24" fill="currentColor" class="shrink-0"> <svg width="13" height="13" viewBox="0 0 24 24" fill="currentColor" class="shrink-0">
<rect x="6" y="4" width="4" height="16" rx="1" /> <rect x="6" y="4" width="4" height="16" rx="1" />
@ -590,7 +590,7 @@
</button> </button>
<button <button
onclick={() => handlePauseAndSnapshot(openCapsule.id)} onclick={() => handlePauseAndSnapshot(openCapsule.id)}
class="flex w-full items-center gap-2.5 px-3 py-2 text-[12px] text-[var(--color-text-secondary)] transition-colors duration-150 hover:bg-[var(--color-bg-3)] hover:text-[var(--color-text-primary)]" class="flex w-full items-center gap-2.5 px-3 py-2 text-meta text-[var(--color-text-secondary)] transition-colors duration-150 hover:bg-[var(--color-bg-3)] hover:text-[var(--color-text-primary)]"
> >
<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="shrink-0"> <svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="shrink-0">
<path d="M14.5 4h-5L7 7H2v13a2 2 0 002 2h16a2 2 0 002-2V7h-5l-2.5-3z" /> <path d="M14.5 4h-5L7 7H2v13a2 2 0 002 2h16a2 2 0 002-2V7h-5l-2.5-3z" />
@ -601,7 +601,7 @@
{:else if openCapsule.status === 'paused'} {:else if openCapsule.status === 'paused'}
<button <button
onclick={() => handleResume(openCapsule.id)} onclick={() => handleResume(openCapsule.id)}
class="flex w-full items-center gap-2.5 px-3 py-2 text-[12px] text-[var(--color-text-secondary)] transition-colors duration-150 hover:bg-[var(--color-bg-3)] hover:text-[var(--color-text-primary)]" class="flex w-full items-center gap-2.5 px-3 py-2 text-meta text-[var(--color-text-secondary)] transition-colors duration-150 hover:bg-[var(--color-bg-3)] hover:text-[var(--color-text-primary)]"
> >
<svg width="13" height="13" viewBox="0 0 24 24" fill="currentColor" class="shrink-0"> <svg width="13" height="13" viewBox="0 0 24 24" fill="currentColor" class="shrink-0">
<polygon points="5 3 19 12 5 21 5 3" /> <polygon points="5 3 19 12 5 21 5 3" />
@ -610,7 +610,7 @@
</button> </button>
<button <button
onclick={() => handleSnapshot(openCapsule.id)} onclick={() => handleSnapshot(openCapsule.id)}
class="flex w-full items-center gap-2.5 px-3 py-2 text-[12px] text-[var(--color-text-secondary)] transition-colors duration-150 hover:bg-[var(--color-bg-3)] hover:text-[var(--color-text-primary)]" class="flex w-full items-center gap-2.5 px-3 py-2 text-meta text-[var(--color-text-secondary)] transition-colors duration-150 hover:bg-[var(--color-bg-3)] hover:text-[var(--color-text-primary)]"
> >
<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="shrink-0"> <svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="shrink-0">
<path d="M14.5 4h-5L7 7H2v13a2 2 0 002 2h16a2 2 0 002-2V7h-5l-2.5-3z" /> <path d="M14.5 4h-5L7 7H2v13a2 2 0 002 2h16a2 2 0 002-2V7h-5l-2.5-3z" />
@ -622,7 +622,7 @@
<div class="my-1 border-t border-[var(--color-border)]"></div> <div class="my-1 border-t border-[var(--color-border)]"></div>
<button <button
onclick={() => { const target = openCapsule; openMenuId = null; destroyError = null; destroyTarget = target; }} onclick={() => { const target = openCapsule; openMenuId = null; destroyError = null; destroyTarget = target; }}
class="flex w-full items-center gap-2.5 px-3 py-2 text-[12px] text-[var(--color-red)] transition-colors duration-150 hover:bg-[var(--color-red)]/5" class="flex w-full items-center gap-2.5 px-3 py-2 text-meta text-[var(--color-red)] transition-colors duration-150 hover:bg-[var(--color-red)]/5"
> >
<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="shrink-0"> <svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="shrink-0">
<polyline points="3 6 5 6 21 6" /> <polyline points="3 6 5 6 21 6" />
@ -645,41 +645,41 @@
></div> ></div>
<div class="relative w-full max-w-[420px] rounded-[var(--radius-card)] border border-[var(--color-border-mid)] bg-[var(--color-bg-2)] p-6" style="animation: fadeUp 0.2s ease both"> <div class="relative w-full max-w-[420px] rounded-[var(--radius-card)] border border-[var(--color-border-mid)] bg-[var(--color-bg-2)] p-6" style="animation: fadeUp 0.2s ease both">
<h2 class="font-serif text-[20px] tracking-[-0.02em] text-[var(--color-text-bright)]">Launch Capsule</h2> <h2 class="font-serif text-heading tracking-[-0.02em] text-[var(--color-text-bright)]">Launch Capsule</h2>
<p class="mt-1 text-[13px] text-[var(--color-text-tertiary)]">Launch a new isolated VM.</p> <p class="mt-1 text-ui text-[var(--color-text-tertiary)]">Configure and launch a new isolated capsule.</p>
{#if createError} {#if createError}
<div class="mt-4 rounded-[var(--radius-input)] border border-[var(--color-red)]/30 bg-[var(--color-red)]/5 px-3 py-2 text-[12px] text-[var(--color-red)]"> <div class="mt-4 rounded-[var(--radius-input)] border border-[var(--color-red)]/30 bg-[var(--color-red)]/5 px-3 py-2 text-meta text-[var(--color-red)]">
{createError} {createError}
</div> </div>
{/if} {/if}
<div class="mt-5 space-y-4"> <div class="mt-5 space-y-4">
<div> <div>
<label class="mb-1.5 block text-[11px] font-semibold uppercase tracking-[0.05em] text-[var(--color-text-tertiary)]" for="create-template">Template</label> <label class="mb-1.5 block text-label font-semibold uppercase tracking-[0.05em] text-[var(--color-text-tertiary)]" for="create-template">Template</label>
<input <input
id="create-template" id="create-template"
type="text" type="text"
bind:value={createForm.template} bind:value={createForm.template}
class="w-full rounded-[var(--radius-input)] border border-[var(--color-border)] bg-[var(--color-bg-4)] px-3 py-2 font-mono text-[13px] text-[var(--color-text-bright)] outline-none placeholder:text-[var(--color-text-muted)] transition-colors duration-150 focus:border-[var(--color-accent)]" class="w-full rounded-[var(--radius-input)] border border-[var(--color-border)] bg-[var(--color-bg-4)] px-3 py-2 font-mono text-ui text-[var(--color-text-bright)] outline-none placeholder:text-[var(--color-text-muted)] transition-colors duration-150 focus:border-[var(--color-accent)]"
placeholder="minimal" placeholder="minimal"
/> />
</div> </div>
<div class="grid grid-cols-2 gap-3"> <div class="grid grid-cols-2 gap-3">
<div> <div>
<label class="mb-1.5 block text-[11px] font-semibold uppercase tracking-[0.05em] text-[var(--color-text-tertiary)]" for="create-vcpus">vCPUs</label> <label class="mb-1.5 block text-label font-semibold uppercase tracking-[0.05em] text-[var(--color-text-tertiary)]" for="create-vcpus">vCPUs</label>
<input <input
id="create-vcpus" id="create-vcpus"
type="number" type="number"
min="1" min="1"
max="8" max="8"
bind:value={createForm.vcpus} bind:value={createForm.vcpus}
class="w-full rounded-[var(--radius-input)] border border-[var(--color-border)] bg-[var(--color-bg-4)] px-3 py-2 font-mono text-[13px] text-[var(--color-text-bright)] outline-none transition-colors duration-150 focus:border-[var(--color-accent)]" class="w-full rounded-[var(--radius-input)] border border-[var(--color-border)] bg-[var(--color-bg-4)] px-3 py-2 font-mono text-ui text-[var(--color-text-bright)] outline-none transition-colors duration-150 focus:border-[var(--color-accent)]"
/> />
</div> </div>
<div> <div>
<label class="mb-1.5 block text-[11px] font-semibold uppercase tracking-[0.05em] text-[var(--color-text-tertiary)]" for="create-memory">Memory (MB)</label> <label class="mb-1.5 block text-label font-semibold uppercase tracking-[0.05em] text-[var(--color-text-tertiary)]" for="create-memory">Memory (MB)</label>
<input <input
id="create-memory" id="create-memory"
type="number" type="number"
@ -687,19 +687,19 @@
max="8192" max="8192"
step="128" step="128"
bind:value={createForm.memory_mb} bind:value={createForm.memory_mb}
class="w-full rounded-[var(--radius-input)] border border-[var(--color-border)] bg-[var(--color-bg-4)] px-3 py-2 font-mono text-[13px] text-[var(--color-text-bright)] outline-none transition-colors duration-150 focus:border-[var(--color-accent)]" class="w-full rounded-[var(--radius-input)] border border-[var(--color-border)] bg-[var(--color-bg-4)] px-3 py-2 font-mono text-ui text-[var(--color-text-bright)] outline-none transition-colors duration-150 focus:border-[var(--color-accent)]"
/> />
</div> </div>
</div> </div>
<div> <div>
<label class="mb-1.5 block text-[11px] font-semibold uppercase tracking-[0.05em] text-[var(--color-text-tertiary)]" for="create-timeout">Auto-pause timeout (seconds, 0 = never)</label> <label class="mb-1.5 block text-label font-semibold uppercase tracking-[0.05em] text-[var(--color-text-tertiary)]" for="create-timeout">Auto-pause timeout (seconds, 0 = never)</label>
<input <input
id="create-timeout" id="create-timeout"
type="number" type="number"
min="0" min="0"
bind:value={createForm.timeout_sec} bind:value={createForm.timeout_sec}
class="w-full rounded-[var(--radius-input)] border border-[var(--color-border)] bg-[var(--color-bg-4)] px-3 py-2 font-mono text-[13px] text-[var(--color-text-bright)] outline-none transition-colors duration-150 focus:border-[var(--color-accent)]" class="w-full rounded-[var(--radius-input)] border border-[var(--color-border)] bg-[var(--color-bg-4)] px-3 py-2 font-mono text-ui text-[var(--color-text-bright)] outline-none transition-colors duration-150 focus:border-[var(--color-accent)]"
/> />
</div> </div>
</div> </div>
@ -708,14 +708,14 @@
<button <button
onclick={() => { showCreateDialog = false; }} onclick={() => { showCreateDialog = false; }}
disabled={creating} disabled={creating}
class="rounded-[var(--radius-button)] border border-[var(--color-border)] px-4 py-2 text-[13px] text-[var(--color-text-secondary)] transition-colors duration-150 hover:border-[var(--color-border-mid)] hover:text-[var(--color-text-primary)] disabled:opacity-50" class="rounded-[var(--radius-button)] border border-[var(--color-border)] px-4 py-2 text-ui text-[var(--color-text-secondary)] transition-colors duration-150 hover:border-[var(--color-border-mid)] hover:text-[var(--color-text-primary)] disabled:opacity-50"
> >
Cancel Cancel
</button> </button>
<button <button
onclick={handleCreate} onclick={handleCreate}
disabled={creating} disabled={creating}
class="flex items-center gap-2 rounded-[var(--radius-button)] bg-[var(--color-accent)] px-5 py-2 text-[13px] font-semibold text-white transition-all duration-150 hover:brightness-115 hover:-translate-y-px active:translate-y-0 disabled:opacity-50 disabled:hover:translate-y-0" class="flex items-center gap-2 rounded-[var(--radius-button)] bg-[var(--color-accent)] px-5 py-2 text-ui font-semibold text-white transition-all duration-150 hover:brightness-115 hover:-translate-y-px active:translate-y-0 disabled:opacity-50 disabled:hover:translate-y-0"
> >
{#if creating} {#if creating}
<svg class="animate-spin" width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"> <svg class="animate-spin" width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
@ -742,13 +742,13 @@
></div> ></div>
<div class="relative w-full max-w-[380px] rounded-[var(--radius-card)] border border-[var(--color-border-mid)] bg-[var(--color-bg-2)] p-6" style="animation: fadeUp 0.2s ease both"> <div class="relative w-full max-w-[380px] rounded-[var(--radius-card)] border border-[var(--color-border-mid)] bg-[var(--color-bg-2)] p-6" style="animation: fadeUp 0.2s ease both">
<h2 class="font-serif text-[20px] tracking-[-0.02em] text-[var(--color-text-bright)]">Destroy Capsule</h2> <h2 class="font-serif text-heading tracking-[-0.02em] text-[var(--color-text-bright)]">Destroy Capsule</h2>
<p class="mt-2 text-[13px] text-[var(--color-text-tertiary)]"> <p class="mt-2 text-ui text-[var(--color-text-tertiary)]">
This will permanently destroy <span class="font-mono text-[var(--color-text-secondary)]">{destroyTarget.id}</span>. This action cannot be undone. Destroy <span class="font-mono text-[var(--color-text-secondary)]">{destroyTarget.id}</span>? The capsule will be terminated and all data inside it lost.
</p> </p>
{#if destroyError} {#if destroyError}
<div class="mt-4 rounded-[var(--radius-input)] border border-[var(--color-red)]/30 bg-[var(--color-red)]/5 px-3 py-2 text-[12px] text-[var(--color-red)]"> <div class="mt-4 rounded-[var(--radius-input)] border border-[var(--color-red)]/30 bg-[var(--color-red)]/5 px-3 py-2 text-meta text-[var(--color-red)]">
{destroyError} {destroyError}
</div> </div>
{/if} {/if}
@ -757,14 +757,14 @@
<button <button
onclick={() => { destroyTarget = null; }} onclick={() => { destroyTarget = null; }}
disabled={destroying} disabled={destroying}
class="rounded-[var(--radius-button)] border border-[var(--color-border)] px-4 py-2 text-[13px] text-[var(--color-text-secondary)] transition-colors duration-150 hover:border-[var(--color-border-mid)] hover:text-[var(--color-text-primary)] disabled:opacity-50" class="rounded-[var(--radius-button)] border border-[var(--color-border)] px-4 py-2 text-ui text-[var(--color-text-secondary)] transition-colors duration-150 hover:border-[var(--color-border-mid)] hover:text-[var(--color-text-primary)] disabled:opacity-50"
> >
Cancel Cancel
</button> </button>
<button <button
onclick={handleDestroy} onclick={handleDestroy}
disabled={destroying} disabled={destroying}
class="flex items-center gap-2 rounded-[var(--radius-button)] bg-[var(--color-red)] px-5 py-2 text-[13px] font-semibold text-white transition-all duration-150 hover:brightness-115 hover:-translate-y-px active:translate-y-0 disabled:opacity-50 disabled:hover:translate-y-0" class="flex items-center gap-2 rounded-[var(--radius-button)] bg-[var(--color-red)] px-5 py-2 text-ui font-semibold text-white transition-all duration-150 hover:brightness-115 hover:-translate-y-px active:translate-y-0 disabled:opacity-50 disabled:hover:translate-y-0"
> >
{#if destroying} {#if destroying}
<svg class="animate-spin" width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"> <svg class="animate-spin" width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
@ -784,7 +784,7 @@
{#snippet sortableHeader(label: string, key: SortKey)} {#snippet sortableHeader(label: string, key: SortKey)}
<button <button
onclick={() => toggleSort(key)} onclick={() => toggleSort(key)}
class="flex items-center gap-1 px-4 py-[11px] text-[11px] font-semibold uppercase tracking-[0.05em] text-[var(--color-text-muted)] transition-colors duration-150 hover:text-[var(--color-text-secondary)]" class="flex items-center gap-1 px-5 py-3 text-label font-semibold uppercase tracking-[0.05em] text-[var(--color-text-muted)] transition-colors duration-150 hover:text-[var(--color-text-secondary)]"
> >
{label} {label}
{#if sortKey === key} {#if sortKey === key}
@ -799,16 +799,16 @@
{/snippet} {/snippet}
{#snippet metricCell(label: string, value: string, sublabel: string, extra: string | null, hasBorderRight: boolean)} {#snippet metricCell(label: string, value: string, sublabel: string, extra: string | null, hasBorderRight: boolean)}
<div class="flex-1 bg-[var(--color-bg-2)] px-5 py-[18px] transition-colors duration-150 hover:bg-[var(--color-bg-3)] {hasBorderRight ? 'border-r border-[var(--color-border)]' : ''}"> <div class="flex-1 bg-[var(--color-bg-2)] px-5 py-5 transition-colors duration-150 hover:bg-[var(--color-bg-3)] {hasBorderRight ? 'border-r border-[var(--color-border)]' : ''}">
<div class="flex items-center gap-2"> <div class="flex items-center gap-2">
<span class="text-[12px] font-semibold uppercase tracking-[0.05em] text-[var(--color-text-tertiary)]">{label}</span> <span class="text-meta font-semibold uppercase tracking-[0.05em] text-[var(--color-text-tertiary)]">{label}</span>
<span class="rounded-[3px] bg-[var(--color-accent-glow-mid)] px-1.5 py-0.5 text-[10px] font-semibold uppercase tracking-[0.04em] text-[var(--color-accent-mid)]"> <span class="rounded-[3px] bg-[var(--color-accent-glow-mid)] px-1.5 py-0.5 text-badge font-semibold uppercase tracking-[0.04em] text-[var(--color-accent-mid)]">
<span class="mr-0.5 inline-block h-[5px] w-[5px] rounded-full bg-[var(--color-accent)]" style="animation: wrenn-glow 2.5s ease-in-out infinite"></span> <span class="mr-0.5 inline-block h-[5px] w-[5px] rounded-full bg-[var(--color-accent)]" style="animation: wrenn-glow 2.5s ease-in-out infinite"></span>
Live Live
</span> </span>
</div> </div>
<div class="mt-1 font-serif text-[36px] tracking-[-0.04em] text-[var(--color-text-bright)]">{value}</div> <div class="mt-1 font-serif text-[2.571rem] tracking-[-0.04em] text-[var(--color-text-bright)]">{value}</div>
<div class="mt-1 flex items-center gap-1.5 text-[11px] text-[var(--color-text-tertiary)]"> <div class="mt-1 flex items-center gap-1.5 text-label text-[var(--color-text-tertiary)]">
<span>{sublabel}</span> <span>{sublabel}</span>
{#if extra} {#if extra}
<span class="text-[var(--color-text-muted)]">|</span> <span class="text-[var(--color-text-muted)]">|</span>
@ -820,13 +820,13 @@
{#snippet chartCard(label: string, value: string, sublabel: string)} {#snippet chartCard(label: string, value: string, sublabel: string)}
<div class="rounded-[var(--radius-card)] border border-[var(--color-border)] bg-[var(--color-bg-2)]"> <div class="rounded-[var(--radius-card)] border border-[var(--color-border)] bg-[var(--color-bg-2)]">
<div class="flex items-center justify-between px-5 pt-[18px] pb-3"> <div class="flex items-center justify-between px-5 pt-5 pb-3">
<div> <div>
<div class="text-[12px] font-semibold uppercase tracking-[0.05em] text-[var(--color-text-tertiary)]">{label}</div> <div class="text-meta font-semibold uppercase tracking-[0.05em] text-[var(--color-text-tertiary)]">{label}</div>
<div class="mt-0.5 flex items-baseline gap-2"> <div class="mt-0.5 flex items-baseline gap-2">
<span class="font-serif text-[30px] tracking-[-0.04em] text-[var(--color-text-bright)]">{value}</span> <span class="font-serif text-[2.143rem] tracking-[-0.04em] text-[var(--color-text-bright)]">{value}</span>
<span class="text-[13px] text-[var(--color-text-secondary)]">{sublabel}</span> <span class="text-ui text-[var(--color-text-secondary)]">{sublabel}</span>
<span class="rounded-[3px] bg-[var(--color-accent-glow-mid)] px-1.5 py-0.5 text-[10px] font-semibold uppercase tracking-[0.04em] text-[var(--color-accent-mid)]"> <span class="rounded-[3px] bg-[var(--color-accent-glow-mid)] px-1.5 py-0.5 text-badge font-semibold uppercase tracking-[0.04em] text-[var(--color-accent-mid)]">
<span class="mr-0.5 inline-block h-[5px] w-[5px] rounded-full bg-[var(--color-accent)]" style="animation: wrenn-glow 2.5s ease-in-out infinite"></span> <span class="mr-0.5 inline-block h-[5px] w-[5px] rounded-full bg-[var(--color-accent)]" style="animation: wrenn-glow 2.5s ease-in-out infinite"></span>
Live Live
</span> </span>
@ -836,7 +836,7 @@
<div class="flex overflow-hidden rounded-[var(--radius-button)] border border-[var(--color-border)]"> <div class="flex overflow-hidden rounded-[var(--radius-button)] border border-[var(--color-border)]">
{#each ['5m', '1H', '6H', '24H', '30D'] as range, i} {#each ['5m', '1H', '6H', '24H', '30D'] as range, i}
<button <button
class="px-2.5 py-1 font-mono text-[11px] transition-colors duration-150 {range === '1H' class="px-2.5 py-1 font-mono text-label transition-colors duration-150 {range === '1H'
? 'bg-[var(--color-bg-5)] text-[var(--color-text-bright)]' ? 'bg-[var(--color-bg-5)] text-[var(--color-text-bright)]'
: 'text-[var(--color-text-tertiary)] hover:text-[var(--color-text-secondary)]'} {i > 0 : 'text-[var(--color-text-tertiary)] hover:text-[var(--color-text-secondary)]'} {i > 0
? 'border-l border-[var(--color-border)]' ? 'border-l border-[var(--color-border)]'
@ -850,11 +850,11 @@
<div class="relative h-[200px] px-5 pb-3"> <div class="relative h-[200px] px-5 pb-3">
<div class="absolute left-0 top-0 flex h-full w-12 flex-col justify-between py-1 text-right"> <div class="absolute left-0 top-0 flex h-full w-12 flex-col justify-between py-1 text-right">
<span class="font-mono text-[10px] text-[var(--color-text-muted)]">4</span> <span class="font-mono text-badge text-[var(--color-text-muted)]">4</span>
<span class="font-mono text-[10px] text-[var(--color-text-muted)]">3</span> <span class="font-mono text-badge text-[var(--color-text-muted)]">3</span>
<span class="font-mono text-[10px] text-[var(--color-text-muted)]">2</span> <span class="font-mono text-badge text-[var(--color-text-muted)]">2</span>
<span class="font-mono text-[10px] text-[var(--color-text-muted)]">1</span> <span class="font-mono text-badge text-[var(--color-text-muted)]">1</span>
<span class="font-mono text-[10px] text-[var(--color-text-muted)]">0</span> <span class="font-mono text-badge text-[var(--color-text-muted)]">0</span>
</div> </div>
<svg class="ml-8 h-full w-[calc(100%-2rem)]" viewBox="0 0 400 180" preserveAspectRatio="none"> <svg class="ml-8 h-full w-[calc(100%-2rem)]" viewBox="0 0 400 180" preserveAspectRatio="none">
@ -866,7 +866,7 @@
<div class="ml-8 flex justify-between pt-2"> <div class="ml-8 flex justify-between pt-2">
{#each ['03:01', '03:02', '03:03', '03:04', '03:05'] as t} {#each ['03:01', '03:02', '03:03', '03:04', '03:05'] as t}
<span class="font-mono text-[10px] text-[var(--color-text-muted)]">{t}</span> <span class="font-mono text-badge text-[var(--color-text-muted)]">{t}</span>
{/each} {/each}
</div> </div>
</div> </div>

View File

@ -2,6 +2,7 @@
import Sidebar from '$lib/components/Sidebar.svelte'; import Sidebar from '$lib/components/Sidebar.svelte';
import { onMount } from 'svelte'; import { onMount } from 'svelte';
import { listKeys, createKey, revokeKey, type APIKey } from '$lib/api/keys'; import { listKeys, createKey, revokeKey, type APIKey } from '$lib/api/keys';
import { toast } from '$lib/toast.svelte';
let collapsed = $state( let collapsed = $state(
typeof window !== 'undefined' typeof window !== 'undefined'
@ -75,9 +76,13 @@
async function copyKey() { async function copyKey() {
if (!newKey?.key) return; if (!newKey?.key) return;
await navigator.clipboard.writeText(newKey.key); try {
copied = true; await navigator.clipboard.writeText(newKey.key);
setTimeout(() => (copied = false), 2000); copied = true;
setTimeout(() => (copied = false), 2000);
} catch {
toast.error('Copy failed — select the key text and copy it manually.');
}
} }
function formatDate(iso: string | undefined): string { function formatDate(iso: string | undefined): string {
@ -115,20 +120,20 @@
<div class="flex flex-1 flex-col overflow-hidden"> <div class="flex flex-1 flex-col overflow-hidden">
<main class="flex-1 overflow-y-auto bg-[var(--color-bg-0)]"> <main class="flex-1 overflow-y-auto bg-[var(--color-bg-0)]">
<!-- Header --> <!-- Header -->
<div class="px-7 pt-6"> <div class="px-7 pt-8">
<div class="flex items-center justify-between"> <div class="flex items-center justify-between">
<div> <div>
<h1 class="font-serif text-[24px] tracking-[-0.02em] text-[var(--color-text-bright)]"> <h1 class="font-serif text-page tracking-[-0.02em] text-[var(--color-text-bright)]">
API Keys API Keys
</h1> </h1>
<p class="mt-1 text-[13px] text-[var(--color-text-tertiary)]"> <p class="mt-2 text-ui text-[var(--color-text-tertiary)]">
Keys authenticate SDK and direct API requests. Treat them like passwords. Keys authenticate SDK and direct API requests. Treat them like passwords.
</p> </p>
</div> </div>
<button <button
onclick={() => { showCreate = true; createError = null; createName = ''; }} onclick={() => { showCreate = true; createError = null; createName = ''; }}
class="flex items-center gap-2 rounded-[var(--radius-button)] bg-[var(--color-accent)] px-4 py-2 text-[13px] font-semibold text-white transition-all duration-150 hover:brightness-115 hover:-translate-y-px active:translate-y-0" class="flex items-center gap-2 rounded-[var(--radius-button)] bg-[var(--color-accent)] px-4 py-2 text-ui font-semibold text-white transition-all duration-150 hover:brightness-115 hover:-translate-y-px active:translate-y-0"
> >
<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"> <svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
<line x1="12" y1="5" x2="12" y2="19" /><line x1="5" y1="12" x2="19" y2="12" /> <line x1="12" y1="5" x2="12" y2="19" /><line x1="5" y1="12" x2="19" y2="12" />
@ -137,20 +142,26 @@
</button> </button>
</div> </div>
<div class="mt-5 border-b border-[var(--color-border)]"></div> <div class="mt-6 border-b border-[var(--color-border)]"></div>
</div> </div>
<!-- Content --> <!-- Content -->
<div class="p-7" style="animation: fadeUp 0.35s ease both"> <div class="p-8" style="animation: fadeUp 0.35s ease both">
{#if error} {#if error}
<div class="mb-4 rounded-[var(--radius-card)] border border-[var(--color-red)]/30 bg-[var(--color-red)]/5 px-4 py-3 text-[13px] text-[var(--color-red)]"> <div class="mb-4 flex items-center justify-between gap-4 rounded-[var(--radius-card)] border border-[var(--color-red)]/30 bg-[var(--color-red)]/5 px-4 py-3 text-ui text-[var(--color-red)]">
{error} <span>{error}</span>
<button
onclick={fetchKeys}
class="shrink-0 font-semibold underline-offset-2 hover:underline"
>
Try again
</button>
</div> </div>
{/if} {/if}
{#if loading} {#if loading}
<div class="flex items-center justify-center py-24"> <div class="flex items-center justify-center py-24">
<div class="flex items-center gap-3 text-[13px] text-[var(--color-text-secondary)]"> <div class="flex items-center gap-3 text-ui text-[var(--color-text-secondary)]">
<svg class="animate-spin" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"> <svg class="animate-spin" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="M21 12a9 9 0 1 1-6.219-8.56" /> <path d="M21 12a9 9 0 1 1-6.219-8.56" />
</svg> </svg>
@ -164,13 +175,13 @@
<path d="M21 2l-2 2m-7.61 7.61a5.5 5.5 0 1 1-7.778 7.778 5.5 5.5 0 0 1 7.777-7.777zm0 0L15.5 7.5m0 0l3 3L22 7l-3-3m-3.5 3.5L19 4" /> <path d="M21 2l-2 2m-7.61 7.61a5.5 5.5 0 1 1-7.778 7.778 5.5 5.5 0 0 1 7.777-7.777zm0 0L15.5 7.5m0 0l3 3L22 7l-3-3m-3.5 3.5L19 4" />
</svg> </svg>
</div> </div>
<p class="font-serif text-[20px] tracking-[-0.02em] text-[var(--color-text-bright)]">No API keys yet</p> <p class="font-serif text-heading tracking-[-0.02em] text-[var(--color-text-bright)]">No API keys yet</p>
<p class="mt-1.5 text-[13px] text-[var(--color-text-tertiary)]">Create a key to authenticate SDK and API requests.</p> <p class="mt-1.5 text-ui text-[var(--color-text-tertiary)]">Create your first key to start making API requests.</p>
<button <button
onclick={() => { showCreate = true; createError = null; createName = ''; }} onclick={() => { showCreate = true; createError = null; createName = ''; }}
class="mt-6 flex items-center gap-2 rounded-[var(--radius-button)] bg-[var(--color-accent)] px-5 py-2.5 text-[13px] font-semibold text-white transition-all duration-150 hover:brightness-115 hover:-translate-y-px active:translate-y-0" class="mt-6 flex items-center gap-2 rounded-[var(--radius-button)] bg-[var(--color-accent)] px-5 py-2.5 text-ui font-semibold text-white transition-all duration-150 hover:brightness-115 hover:-translate-y-px active:translate-y-0"
> >
Create a Key New Key
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"> <svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
<line x1="12" y1="5" x2="12" y2="19" /><line x1="5" y1="12" x2="19" y2="12" /> <line x1="12" y1="5" x2="12" y2="19" /><line x1="5" y1="12" x2="19" y2="12" />
</svg> </svg>
@ -180,11 +191,11 @@
<div class="rounded-[var(--radius-card)] border border-[var(--color-border)] overflow-hidden"> <div class="rounded-[var(--radius-card)] border border-[var(--color-border)] overflow-hidden">
<!-- Table header --> <!-- Table header -->
<div class="grid grid-cols-[2fr_1.2fr_1.4fr_1.4fr_80px] border-b border-[var(--color-border)] bg-[var(--color-bg-3)]"> <div class="grid grid-cols-[2fr_1.2fr_1.4fr_1.4fr_80px] border-b border-[var(--color-border)] bg-[var(--color-bg-3)]">
<div class="px-4 py-[11px] text-[11px] font-semibold uppercase tracking-[0.05em] text-[var(--color-text-muted)]">Name / Key</div> <div class="px-5 py-3 text-label font-semibold uppercase tracking-[0.05em] text-[var(--color-text-muted)]">Name / Key</div>
<div class="px-4 py-[11px] text-[11px] font-semibold uppercase tracking-[0.05em] text-[var(--color-text-muted)]">Created By</div> <div class="px-5 py-3 text-label font-semibold uppercase tracking-[0.05em] text-[var(--color-text-muted)]">Created By</div>
<div class="px-4 py-[11px] text-[11px] font-semibold uppercase tracking-[0.05em] text-[var(--color-text-muted)]">Created</div> <div class="px-5 py-3 text-label font-semibold uppercase tracking-[0.05em] text-[var(--color-text-muted)]">Created</div>
<div class="px-4 py-[11px] text-[11px] font-semibold uppercase tracking-[0.05em] text-[var(--color-text-muted)]">Last Used</div> <div class="px-5 py-3 text-label font-semibold uppercase tracking-[0.05em] text-[var(--color-text-muted)]">Last Used</div>
<div class="px-4 py-[11px] text-[11px] font-semibold uppercase tracking-[0.05em] text-[var(--color-text-muted)]"></div> <div class="px-5 py-3 text-label font-semibold uppercase tracking-[0.05em] text-[var(--color-text-muted)]"></div>
</div> </div>
{#each keys as key, i (key.id)} {#each keys as key, i (key.id)}
@ -193,37 +204,37 @@
style="animation: fadeUp 0.35s ease both; animation-delay: {i * 40}ms" style="animation: fadeUp 0.35s ease both; animation-delay: {i * 40}ms"
> >
<!-- Name + prefix --> <!-- Name + prefix -->
<div class="flex flex-col gap-0.5 px-4 py-3"> <div class="min-w-0 flex flex-col gap-0.5 px-5 py-4">
<span class="text-[13px] font-medium text-[var(--color-text-bright)]">{key.name || '—'}</span> <span class="truncate text-ui font-medium text-[var(--color-text-bright)]">{key.name || '—'}</span>
<span class="font-mono text-[12px] text-[var(--color-text-muted)]">{key.key_prefix}...</span> <span class="font-mono text-meta text-[var(--color-text-muted)]">{key.key_prefix}</span>
</div> </div>
<!-- Created by --> <!-- Created by -->
<div class="px-4 py-3"> <div class="min-w-0 px-5 py-4">
<span class="text-[13px] text-[var(--color-text-secondary)]">{key.creator_email ?? key.created_by}</span> <span class="block truncate text-ui text-[var(--color-text-secondary)]">{key.creator_email ?? key.created_by}</span>
</div> </div>
<!-- Created at --> <!-- Created at -->
<div class="px-4 py-3"> <div class="px-5 py-4">
<span class="text-[13px] text-[var(--color-text-secondary)]">{formatDate(key.created_at)}</span> <span class="text-ui text-[var(--color-text-secondary)]">{formatDate(key.created_at)}</span>
</div> </div>
<!-- Last used --> <!-- Last used -->
<div class="px-4 py-3"> <div class="px-5 py-4">
{#if key.last_used} {#if key.last_used}
<span class="text-[13px] text-[var(--color-text-secondary)]" title={formatDate(key.last_used)}> <span class="text-ui text-[var(--color-text-secondary)]" title={formatDate(key.last_used)}>
{timeAgo(key.last_used)} {timeAgo(key.last_used)}
</span> </span>
{:else} {:else}
<span class="text-[13px] text-[var(--color-text-muted)]">Never</span> <span class="text-ui text-[var(--color-text-muted)]">Never</span>
{/if} {/if}
</div> </div>
<!-- Revoke --> <!-- Revoke -->
<div class="flex justify-end px-4 py-3"> <div class="flex justify-end px-5 py-4">
<button <button
onclick={() => { revokeTarget = key; revokeError = null; }} onclick={() => { revokeTarget = key; revokeError = null; }}
class="rounded-[var(--radius-button)] border border-[var(--color-border)] px-2.5 py-1 text-[11px] font-semibold uppercase tracking-[0.04em] text-[var(--color-text-tertiary)] transition-colors duration-150 hover:border-[var(--color-red)]/40 hover:text-[var(--color-red)]" class="rounded-[var(--radius-button)] border border-[var(--color-border)] px-2.5 py-1 text-label font-semibold uppercase tracking-[0.04em] text-[var(--color-text-tertiary)] transition-colors duration-150 hover:border-[var(--color-red)]/40 hover:text-[var(--color-red)]"
> >
Revoke Revoke
</button> </button>
@ -232,20 +243,14 @@
{/each} {/each}
</div> </div>
<p class="mt-3 text-[12px] text-[var(--color-text-muted)]"> <p class="mt-3 text-meta text-[var(--color-text-muted)]">
{keys.length} {keys.length === 1 ? 'key' : 'keys'} total {keys.length} {keys.length === 1 ? 'key' : 'keys'} total
</p> </p>
{/if} {/if}
</div> </div>
</main> </main>
<!-- Status bar --> <footer class="h-px shrink-0 bg-[var(--color-border)]"></footer>
<footer class="flex h-7 shrink-0 items-center justify-end border-t border-[var(--color-border)] bg-[var(--color-bg-1)] px-7">
<div class="flex items-center gap-1.5">
<span class="inline-flex h-[5px] w-[5px] rounded-full bg-[var(--color-accent)]"></span>
<span class="font-mono text-[11px] uppercase tracking-[0.04em] text-[var(--color-text-secondary)]">All systems operational</span>
</div>
</footer>
</div> </div>
</div> </div>
@ -260,17 +265,17 @@
></div> ></div>
<div class="relative w-full max-w-[400px] rounded-[var(--radius-card)] border border-[var(--color-border-mid)] bg-[var(--color-bg-2)] p-6" style="animation: fadeUp 0.2s ease both"> <div class="relative w-full max-w-[400px] rounded-[var(--radius-card)] border border-[var(--color-border-mid)] bg-[var(--color-bg-2)] p-6" style="animation: fadeUp 0.2s ease both">
<h2 class="font-serif text-[20px] tracking-[-0.02em] text-[var(--color-text-bright)]">New API Key</h2> <h2 class="font-serif text-heading tracking-[-0.02em] text-[var(--color-text-bright)]">New API Key</h2>
<p class="mt-1 text-[13px] text-[var(--color-text-tertiary)]">Give your key a name to identify it later.</p> <p class="mt-1 text-ui text-[var(--color-text-tertiary)]">Give your key a name to identify it later.</p>
{#if createError} {#if createError}
<div class="mt-4 rounded-[var(--radius-input)] border border-[var(--color-red)]/30 bg-[var(--color-red)]/5 px-3 py-2 text-[12px] text-[var(--color-red)]"> <div class="mt-4 rounded-[var(--radius-input)] border border-[var(--color-red)]/30 bg-[var(--color-red)]/5 px-3 py-2 text-meta text-[var(--color-red)]">
{createError} {createError}
</div> </div>
{/if} {/if}
<div class="mt-5"> <div class="mt-5">
<label class="mb-1.5 block text-[11px] font-semibold uppercase tracking-[0.05em] text-[var(--color-text-tertiary)]" for="key-name"> <label class="mb-1.5 block text-label font-semibold uppercase tracking-[0.05em] text-[var(--color-text-tertiary)]" for="key-name">
Key name Key name
</label> </label>
<input <input
@ -279,7 +284,7 @@
placeholder="e.g. Production SDK" placeholder="e.g. Production SDK"
bind:value={createName} bind:value={createName}
onkeydown={(e) => { if (e.key === 'Enter' && !creating) handleCreate(); }} onkeydown={(e) => { if (e.key === 'Enter' && !creating) handleCreate(); }}
class="w-full rounded-[var(--radius-input)] border border-[var(--color-border)] bg-[var(--color-bg-4)] px-3 py-2 text-[13px] text-[var(--color-text-bright)] outline-none placeholder:text-[var(--color-text-muted)] transition-colors duration-150 focus:border-[var(--color-accent)]" class="w-full rounded-[var(--radius-input)] border border-[var(--color-border)] bg-[var(--color-bg-4)] px-3 py-2 text-ui text-[var(--color-text-bright)] outline-none placeholder:text-[var(--color-text-muted)] transition-colors duration-150 focus:border-[var(--color-accent)]"
/> />
</div> </div>
@ -287,14 +292,14 @@
<button <button
onclick={() => { showCreate = false; }} onclick={() => { showCreate = false; }}
disabled={creating} disabled={creating}
class="rounded-[var(--radius-button)] border border-[var(--color-border)] px-4 py-2 text-[13px] text-[var(--color-text-secondary)] transition-colors duration-150 hover:border-[var(--color-border-mid)] hover:text-[var(--color-text-primary)] disabled:opacity-50" class="rounded-[var(--radius-button)] border border-[var(--color-border)] px-4 py-2 text-ui text-[var(--color-text-secondary)] transition-colors duration-150 hover:border-[var(--color-border-mid)] hover:text-[var(--color-text-primary)] disabled:opacity-50"
> >
Cancel Cancel
</button> </button>
<button <button
onclick={handleCreate} onclick={handleCreate}
disabled={creating || !createName.trim()} disabled={creating || !createName.trim()}
class="flex items-center gap-2 rounded-[var(--radius-button)] bg-[var(--color-accent)] px-5 py-2 text-[13px] font-semibold text-white transition-all duration-150 hover:brightness-115 hover:-translate-y-px active:translate-y-0 disabled:opacity-50 disabled:hover:translate-y-0" class="flex items-center gap-2 rounded-[var(--radius-button)] bg-[var(--color-accent)] px-5 py-2 text-ui font-semibold text-white transition-all duration-150 hover:brightness-115 hover:-translate-y-px active:translate-y-0 disabled:opacity-50 disabled:hover:translate-y-0"
> >
{#if creating} {#if creating}
<svg class="animate-spin" width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"> <svg class="animate-spin" width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
@ -328,23 +333,23 @@
<polyline points="20 6 9 17 4 12" /> <polyline points="20 6 9 17 4 12" />
</svg> </svg>
</span> </span>
<span class="text-[12px] font-semibold text-[var(--color-accent-mid)]">Key created successfully</span> <span class="text-meta font-semibold text-[var(--color-accent-mid)]">Key created successfully</span>
</div> </div>
<h2 class="font-serif text-[20px] tracking-[-0.02em] text-[var(--color-text-bright)]">{newKey.name || 'API Key'}</h2> <h2 class="font-serif text-heading tracking-[-0.02em] text-[var(--color-text-bright)]">{newKey.name || 'API Key'}</h2>
<p class="mt-1 text-[13px] text-[var(--color-text-tertiary)]"> <p class="mt-1 text-ui text-[var(--color-text-tertiary)]">
Copy this key now — it won't be shown again. Copy this key now — it won't be shown again.
</p> </p>
<!-- Key display --> <!-- Key display -->
<div class="mt-5 rounded-[var(--radius-input)] border border-[var(--color-border-mid)] bg-[var(--color-bg-0)] p-4"> <div class="mt-5 rounded-[var(--radius-input)] border border-[var(--color-border-mid)] bg-[var(--color-bg-0)] p-4">
<div class="flex items-center gap-3"> <div class="flex items-center gap-3">
<span class="min-w-0 flex-1 break-all font-mono text-[13px] leading-relaxed text-[var(--color-text-bright)]"> <span class="min-w-0 flex-1 break-all font-mono text-ui leading-relaxed text-[var(--color-text-bright)]">
{newKey.key ?? ''} {newKey.key ?? ''}
</span> </span>
<button <button
onclick={copyKey} onclick={copyKey}
class="shrink-0 flex items-center gap-1.5 rounded-[var(--radius-button)] border px-3 py-1.5 text-[12px] font-semibold transition-all duration-150 class="shrink-0 flex items-center gap-1.5 rounded-[var(--radius-button)] border px-3 py-1.5 text-meta font-semibold transition-all duration-150
{copied {copied
? 'border-[var(--color-accent)]/40 bg-[var(--color-accent-glow-mid)] text-[var(--color-accent-mid)]' ? 'border-[var(--color-accent)]/40 bg-[var(--color-accent-glow-mid)] text-[var(--color-accent-mid)]'
: 'border-[var(--color-border-mid)] text-[var(--color-text-secondary)] hover:border-[var(--color-border-mid)] hover:text-[var(--color-text-primary)]'}" : 'border-[var(--color-border-mid)] text-[var(--color-text-secondary)] hover:border-[var(--color-border-mid)] hover:text-[var(--color-text-primary)]'}"
@ -371,15 +376,15 @@
<path d="M10.29 3.86L1.82 18a2 2 0 0 0 1.71 3h16.94a2 2 0 0 0 1.71-3L13.71 3.86a2 2 0 0 0-3.42 0z" /> <path d="M10.29 3.86L1.82 18a2 2 0 0 0 1.71 3h16.94a2 2 0 0 0 1.71-3L13.71 3.86a2 2 0 0 0-3.42 0z" />
<line x1="12" y1="9" x2="12" y2="13" /><line x1="12" y1="17" x2="12.01" y2="17" /> <line x1="12" y1="9" x2="12" y2="13" /><line x1="12" y1="17" x2="12.01" y2="17" />
</svg> </svg>
<p class="text-[12px] leading-relaxed text-[var(--color-amber)]"> <p class="text-meta leading-relaxed text-[var(--color-amber)]">
Store this key securely. For security reasons, we only show it once and cannot retrieve it later. Treat this like a password — store it in a secrets manager, not a document or chat message.
</p> </p>
</div> </div>
<div class="mt-6 flex justify-end"> <div class="mt-6 flex justify-end">
<button <button
onclick={() => { newKey = null; }} onclick={() => { newKey = null; }}
class="rounded-[var(--radius-button)] bg-[var(--color-bg-4)] border border-[var(--color-border-mid)] px-5 py-2 text-[13px] font-semibold text-[var(--color-text-primary)] transition-colors duration-150 hover:border-[var(--color-border-mid)] hover:bg-[var(--color-bg-5)]" class="rounded-[var(--radius-button)] bg-[var(--color-bg-4)] border border-[var(--color-border-mid)] px-5 py-2 text-ui font-semibold text-[var(--color-text-primary)] transition-colors duration-150 hover:border-[var(--color-border-mid)] hover:bg-[var(--color-bg-5)]"
> >
Done Done
</button> </button>
@ -399,15 +404,15 @@
></div> ></div>
<div class="relative w-full max-w-[380px] rounded-[var(--radius-card)] border border-[var(--color-border-mid)] bg-[var(--color-bg-2)] p-6" style="animation: fadeUp 0.2s ease both"> <div class="relative w-full max-w-[380px] rounded-[var(--radius-card)] border border-[var(--color-border-mid)] bg-[var(--color-bg-2)] p-6" style="animation: fadeUp 0.2s ease both">
<h2 class="font-serif text-[20px] tracking-[-0.02em] text-[var(--color-text-bright)]">Revoke Key</h2> <h2 class="font-serif text-heading tracking-[-0.02em] text-[var(--color-text-bright)]">Revoke Key</h2>
<p class="mt-2 text-[13px] text-[var(--color-text-tertiary)]"> <p class="mt-2 text-ui text-[var(--color-text-tertiary)]">
Revoke <span class="font-medium text-[var(--color-text-secondary)]">{revokeTarget.name || revokeTarget.id}</span>? Revoke <span class="font-medium text-[var(--color-text-secondary)]">{revokeTarget.name || revokeTarget.id}</span>?
Any request using it will stop working immediately. Any request using it will stop working immediately.
</p> </p>
<p class="mt-1.5 font-mono text-[12px] text-[var(--color-text-muted)]">{revokeTarget.key_prefix}...</p> <p class="mt-1.5 font-mono text-meta text-[var(--color-text-muted)]">{revokeTarget.key_prefix}</p>
{#if revokeError} {#if revokeError}
<div class="mt-4 rounded-[var(--radius-input)] border border-[var(--color-red)]/30 bg-[var(--color-red)]/5 px-3 py-2 text-[12px] text-[var(--color-red)]"> <div class="mt-4 rounded-[var(--radius-input)] border border-[var(--color-red)]/30 bg-[var(--color-red)]/5 px-3 py-2 text-meta text-[var(--color-red)]">
{revokeError} {revokeError}
</div> </div>
{/if} {/if}
@ -416,14 +421,14 @@
<button <button
onclick={() => { revokeTarget = null; }} onclick={() => { revokeTarget = null; }}
disabled={revoking} disabled={revoking}
class="rounded-[var(--radius-button)] border border-[var(--color-border)] px-4 py-2 text-[13px] text-[var(--color-text-secondary)] transition-colors duration-150 hover:border-[var(--color-border-mid)] hover:text-[var(--color-text-primary)] disabled:opacity-50" class="rounded-[var(--radius-button)] border border-[var(--color-border)] px-4 py-2 text-ui text-[var(--color-text-secondary)] transition-colors duration-150 hover:border-[var(--color-border-mid)] hover:text-[var(--color-text-primary)] disabled:opacity-50"
> >
Cancel Cancel
</button> </button>
<button <button
onclick={handleRevoke} onclick={handleRevoke}
disabled={revoking} disabled={revoking}
class="flex items-center gap-2 rounded-[var(--radius-button)] bg-[var(--color-red)] px-5 py-2 text-[13px] font-semibold text-white transition-all duration-150 hover:brightness-115 hover:-translate-y-px active:translate-y-0 disabled:opacity-50 disabled:hover:translate-y-0" class="flex items-center gap-2 rounded-[var(--radius-button)] bg-[var(--color-red)] px-5 py-2 text-ui font-semibold text-white transition-all duration-150 hover:brightness-115 hover:-translate-y-px active:translate-y-0 disabled:opacity-50 disabled:hover:translate-y-0"
> >
{#if revoking} {#if revoking}
<svg class="animate-spin" width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"> <svg class="animate-spin" width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">

View File

@ -171,14 +171,14 @@
<div class="flex flex-1 flex-col overflow-hidden"> <div class="flex flex-1 flex-col overflow-hidden">
<main class="flex-1 overflow-y-auto bg-[var(--color-bg-0)]"> <main class="flex-1 overflow-y-auto bg-[var(--color-bg-0)]">
<!-- Header --> <!-- Header -->
<div class="px-7 pt-6"> <div class="px-7 pt-8">
<div class="flex items-start justify-between"> <div class="flex items-start justify-between">
<div> <div>
<h1 class="font-serif text-[24px] tracking-[-0.02em] text-[var(--color-text-bright)]"> <h1 class="font-serif text-page tracking-[-0.02em] text-[var(--color-text-bright)]">
Templates Templates
</h1> </h1>
<p class="mt-1 text-[13px] text-[var(--color-text-tertiary)]"> <p class="mt-2 text-ui text-[var(--color-text-tertiary)]">
Point-in-time captures and base environments for launching capsules. Saved capsule states and base images. Launch a new capsule from any template.
</p> </p>
</div> </div>
</div> </div>
@ -188,7 +188,7 @@
<!-- Snapshots tab (active) --> <!-- Snapshots tab (active) -->
<button <button
onclick={() => (pageTab = 'snapshots')} onclick={() => (pageTab = 'snapshots')}
class="flex items-center gap-2 border-b-2 px-4 py-2.5 text-[13px] font-medium transition-colors duration-150 {pageTab === 'snapshots' class="flex items-center gap-2 border-b-2 px-4 py-2.5 text-ui font-medium transition-colors duration-150 {pageTab === 'snapshots'
? 'border-[var(--color-accent)] text-[var(--color-accent-bright)]' ? 'border-[var(--color-accent)] text-[var(--color-accent-bright)]'
: 'border-transparent text-[var(--color-text-secondary)] hover:text-[var(--color-text-primary)]'}" : 'border-transparent text-[var(--color-text-secondary)] hover:text-[var(--color-text-primary)]'}"
> >
@ -203,7 +203,7 @@
<button <button
disabled disabled
title="Coming soon" title="Coming soon"
class="flex cursor-not-allowed items-center gap-2 border-b-2 border-transparent px-4 py-2.5 text-[13px] font-medium opacity-40" class="flex cursor-not-allowed items-center gap-2 border-b-2 border-transparent px-4 py-2.5 text-ui font-medium opacity-40"
> >
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"> <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<rect x="3" y="3" width="18" height="18" rx="2" ry="2" /> <rect x="3" y="3" width="18" height="18" rx="2" ry="2" />
@ -211,7 +211,7 @@
<polyline points="21 15 16 10 5 21" /> <polyline points="21 15 16 10 5 21" />
</svg> </svg>
Images Images
<span class="rounded-[3px] bg-[var(--color-bg-4)] px-1.5 py-0.5 text-[9px] font-semibold uppercase tracking-[0.06em] text-[var(--color-text-muted)]"> <span class="rounded-[3px] bg-[var(--color-bg-4)] px-1.5 py-0.5 text-badge font-semibold uppercase tracking-[0.06em] text-[var(--color-text-muted)]">
Soon Soon
</span> </span>
</button> </button>
@ -220,16 +220,16 @@
<!-- Snapshots tab content --> <!-- Snapshots tab content -->
{#if pageTab === 'snapshots'} {#if pageTab === 'snapshots'}
<div class="p-7" style="animation: fadeUp 0.35s ease both"> <div class="p-8" style="animation: fadeUp 0.35s ease both">
{#if error} {#if error}
<div class="mb-4 rounded-[var(--radius-card)] border border-[var(--color-red)]/30 bg-[var(--color-red)]/5 px-4 py-3 text-[13px] text-[var(--color-red)]"> <div class="mb-4 rounded-[var(--radius-card)] border border-[var(--color-red)]/30 bg-[var(--color-red)]/5 px-4 py-3 text-ui text-[var(--color-red)]">
{error} {error}
</div> </div>
{/if} {/if}
{#if loading} {#if loading}
<div class="flex items-center justify-center py-24"> <div class="flex items-center justify-center py-24">
<div class="flex items-center gap-3 text-[13px] text-[var(--color-text-secondary)]"> <div class="flex items-center gap-3 text-ui text-[var(--color-text-secondary)]">
<svg class="animate-spin" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"> <svg class="animate-spin" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="M21 12a9 9 0 1 1-6.219-8.56" /> <path d="M21 12a9 9 0 1 1-6.219-8.56" />
</svg> </svg>
@ -243,7 +243,7 @@
{#each ([['all', 'All'], ['snapshot', 'Snapshots'], ['base', 'Images']] as const) as [val, label]} {#each ([['all', 'All'], ['snapshot', 'Snapshots'], ['base', 'Images']] as const) as [val, label]}
<button <button
onclick={() => (typeFilter = val)} onclick={() => (typeFilter = val)}
class="rounded-full border px-3 py-1 text-[12px] font-medium transition-colors duration-150 {typeFilter === val class="rounded-full border px-3 py-1 text-meta font-medium transition-colors duration-150 {typeFilter === val
? 'border-[var(--color-border-mid)] bg-[var(--color-bg-5)] text-[var(--color-text-bright)]' ? 'border-[var(--color-border-mid)] bg-[var(--color-bg-5)] text-[var(--color-text-bright)]'
: 'border-[var(--color-border)] bg-[var(--color-bg-3)] text-[var(--color-text-secondary)] hover:border-[var(--color-border-mid)] hover:text-[var(--color-text-primary)]'}" : 'border-[var(--color-border)] bg-[var(--color-bg-3)] text-[var(--color-text-secondary)] hover:border-[var(--color-border-mid)] hover:text-[var(--color-text-primary)]'}"
> >
@ -251,7 +251,7 @@
</button> </button>
{/each} {/each}
</div> </div>
<span class="text-[12px] text-[var(--color-text-muted)]"> <span class="text-meta text-[var(--color-text-muted)]">
{filteredSnapshots.length} {filteredSnapshots.length}
{filteredSnapshots.length === 1 ? 'snapshot' : 'snapshots'} {filteredSnapshots.length === 1 ? 'snapshot' : 'snapshots'}
</span> </span>
@ -266,16 +266,16 @@
<polyline points="3.27 6.96 12 12.01 20.73 6.96" /><line x1="12" y1="22.08" x2="12" y2="12" /> <polyline points="3.27 6.96 12 12.01 20.73 6.96" /><line x1="12" y1="22.08" x2="12" y2="12" />
</svg> </svg>
</div> </div>
<p class="font-serif text-[20px] tracking-[-0.02em] text-[var(--color-text-bright)]"> <p class="font-serif text-heading tracking-[-0.02em] text-[var(--color-text-bright)]">
{emptyHeading(typeFilter)} {emptyHeading(typeFilter)}
</p> </p>
<p class="mt-1.5 max-w-[340px] text-center text-[13px] text-[var(--color-text-tertiary)]"> <p class="mt-1.5 max-w-[340px] text-center text-ui text-[var(--color-text-tertiary)]">
{emptyDescription(typeFilter)} {emptyDescription(typeFilter)}
</p> </p>
{#if typeFilter === 'all' || typeFilter === 'snapshot'} {#if typeFilter === 'all' || typeFilter === 'snapshot'}
<a <a
href="/dashboard/capsules" href="/dashboard/capsules"
class="mt-6 flex items-center gap-2 rounded-[var(--radius-button)] border border-[var(--color-border-mid)] bg-[var(--color-bg-3)] px-4 py-2 text-[13px] font-medium text-[var(--color-text-secondary)] transition-colors duration-150 hover:border-[var(--color-border-mid)] hover:bg-[var(--color-bg-4)] hover:text-[var(--color-text-primary)]" class="mt-6 flex items-center gap-2 rounded-[var(--radius-button)] border border-[var(--color-border-mid)] bg-[var(--color-bg-3)] px-4 py-2 text-ui font-medium text-[var(--color-text-secondary)] transition-colors duration-150 hover:border-[var(--color-border-mid)] hover:bg-[var(--color-bg-4)] hover:text-[var(--color-text-primary)]"
> >
Go to Capsules Go to Capsules
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"> <svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
@ -290,13 +290,13 @@
<div class="overflow-hidden rounded-[var(--radius-card)] border border-[var(--color-border)]"> <div class="overflow-hidden rounded-[var(--radius-card)] border border-[var(--color-border)]">
<!-- Header --> <!-- Header -->
<div class="grid border-b border-[var(--color-border)] bg-[var(--color-bg-3)]" style="grid-template-columns: 2fr 1fr 0.7fr 0.9fr 0.8fr 1.3fr 140px"> <div class="grid border-b border-[var(--color-border)] bg-[var(--color-bg-3)]" style="grid-template-columns: 2fr 1fr 0.7fr 0.9fr 0.8fr 1.3fr 140px">
<div class="px-4 py-[11px] text-[11px] font-semibold uppercase tracking-[0.05em] text-[var(--color-text-muted)]">Name</div> <div class="px-5 py-3 text-label font-semibold uppercase tracking-[0.05em] text-[var(--color-text-muted)]">Name</div>
<div class="px-4 py-[11px] text-[11px] font-semibold uppercase tracking-[0.05em] text-[var(--color-text-muted)]">Type</div> <div class="px-5 py-3 text-label font-semibold uppercase tracking-[0.05em] text-[var(--color-text-muted)]">Type</div>
<div class="px-4 py-[11px] text-[11px] font-semibold uppercase tracking-[0.05em] text-[var(--color-text-muted)]">vCPUs</div> <div class="px-5 py-3 text-label font-semibold uppercase tracking-[0.05em] text-[var(--color-text-muted)]">vCPUs</div>
<div class="px-4 py-[11px] text-[11px] font-semibold uppercase tracking-[0.05em] text-[var(--color-text-muted)]">Memory</div> <div class="px-5 py-3 text-label font-semibold uppercase tracking-[0.05em] text-[var(--color-text-muted)]">Memory</div>
<div class="px-4 py-[11px] text-[11px] font-semibold uppercase tracking-[0.05em] text-[var(--color-text-muted)]">Size</div> <div class="px-5 py-3 text-label font-semibold uppercase tracking-[0.05em] text-[var(--color-text-muted)]">Size</div>
<div class="px-4 py-[11px] text-[11px] font-semibold uppercase tracking-[0.05em] text-[var(--color-text-muted)]">Created</div> <div class="px-5 py-3 text-label font-semibold uppercase tracking-[0.05em] text-[var(--color-text-muted)]">Created</div>
<div class="px-4 py-[11px] text-right text-[11px] font-semibold uppercase tracking-[0.05em] text-[var(--color-text-muted)]">Actions</div> <div class="px-5 py-3 text-right text-label font-semibold uppercase tracking-[0.05em] text-[var(--color-text-muted)]">Actions</div>
</div> </div>
<!-- Rows --> <!-- Rows -->
@ -306,19 +306,19 @@
style="grid-template-columns: 2fr 1fr 0.7fr 0.9fr 0.8fr 1.3fr 140px; animation: fadeUp 0.35s ease both; animation-delay: {i * 40}ms" style="grid-template-columns: 2fr 1fr 0.7fr 0.9fr 0.8fr 1.3fr 140px; animation: fadeUp 0.35s ease both; animation-delay: {i * 40}ms"
> >
<!-- Name --> <!-- Name -->
<div class="px-4 py-3"> <div class="min-w-0 px-5 py-4">
<span class="font-mono text-[13px] text-[var(--color-text-bright)]">{snapshot.name}</span> <span class="block truncate font-mono text-ui text-[var(--color-text-bright)]">{snapshot.name}</span>
</div> </div>
<!-- Type badge --> <!-- Type badge -->
<div class="px-4 py-3"> <div class="px-5 py-4">
{#if snapshot.type === 'snapshot'} {#if snapshot.type === 'snapshot'}
<span class="inline-flex items-center gap-1.5 rounded-[3px] border border-[var(--color-accent)]/20 bg-[var(--color-accent-glow-mid)] px-2 py-0.5 text-[10px] font-semibold uppercase tracking-[0.04em] text-[var(--color-accent-mid)]"> <span class="inline-flex items-center gap-1.5 rounded-[3px] border border-[var(--color-accent)]/20 bg-[var(--color-accent-glow-mid)] px-2 py-0.5 text-badge font-semibold uppercase tracking-[0.04em] text-[var(--color-accent-mid)]">
<span class="inline-block h-[5px] w-[5px] rounded-full bg-[var(--color-accent)]" style="box-shadow: 0 0 6px rgba(94,140,88,0.5)"></span> <span class="inline-block h-[5px] w-[5px] rounded-full bg-[var(--color-accent)]" style="box-shadow: 0 0 6px rgba(94,140,88,0.5)"></span>
Snapshot Snapshot
</span> </span>
{:else} {:else}
<span class="inline-flex items-center gap-1.5 rounded-[3px] border border-[var(--color-blue)]/20 bg-[var(--color-blue)]/10 px-2 py-0.5 text-[10px] font-semibold uppercase tracking-[0.04em] text-[var(--color-blue)]"> <span class="inline-flex items-center gap-1.5 rounded-[3px] border border-[var(--color-blue)]/20 bg-[var(--color-blue)]/10 px-2 py-0.5 text-badge font-semibold uppercase tracking-[0.04em] text-[var(--color-blue)]">
<span class="inline-block h-[5px] w-[5px] rounded-full bg-[var(--color-blue)]"></span> <span class="inline-block h-[5px] w-[5px] rounded-full bg-[var(--color-blue)]"></span>
Image Image
</span> </span>
@ -326,31 +326,31 @@
</div> </div>
<!-- vCPUs --> <!-- vCPUs -->
<div class="px-4 py-3"> <div class="px-5 py-4">
{#if snapshot.type === 'snapshot' && snapshot.vcpus != null} {#if snapshot.type === 'snapshot' && snapshot.vcpus != null}
<span class="font-mono text-[13px] text-[var(--color-text-secondary)]">{snapshot.vcpus}</span> <span class="font-mono text-ui text-[var(--color-text-secondary)]">{snapshot.vcpus}</span>
{:else} {:else}
<span class="text-[13px] text-[var(--color-text-muted)]"></span> <span class="text-ui text-[var(--color-text-muted)]"></span>
{/if} {/if}
</div> </div>
<!-- Memory --> <!-- Memory -->
<div class="px-4 py-3"> <div class="px-5 py-4">
{#if snapshot.type === 'snapshot' && snapshot.memory_mb != null} {#if snapshot.type === 'snapshot' && snapshot.memory_mb != null}
<span class="font-mono text-[13px] text-[var(--color-text-secondary)]">{snapshot.memory_mb} MB</span> <span class="font-mono text-ui text-[var(--color-text-secondary)]">{snapshot.memory_mb} MB</span>
{:else} {:else}
<span class="text-[13px] text-[var(--color-text-muted)]"></span> <span class="text-ui text-[var(--color-text-muted)]"></span>
{/if} {/if}
</div> </div>
<!-- Size --> <!-- Size -->
<div class="px-4 py-3"> <div class="px-5 py-4">
<span class="font-mono text-[13px] text-[var(--color-text-muted)]">{formatBytes(snapshot.size_bytes)}</span> <span class="font-mono text-ui text-[var(--color-text-muted)]">{formatBytes(snapshot.size_bytes)}</span>
</div> </div>
<!-- Created --> <!-- Created -->
<div class="px-4 py-3" title={formatDate(snapshot.created_at)}> <div class="px-5 py-4" title={formatDate(snapshot.created_at)}>
<span class="text-[13px] text-[var(--color-text-secondary)]">{timeAgo(snapshot.created_at)}</span> <span class="text-ui text-[var(--color-text-secondary)]">{timeAgo(snapshot.created_at)}</span>
</div> </div>
<!-- Actions: split button --> <!-- Actions: split button -->
@ -359,7 +359,7 @@
<!-- Launch part --> <!-- Launch part -->
<button <button
onclick={() => openLaunch(snapshot)} onclick={() => openLaunch(snapshot)}
class="flex items-center px-3 py-1.5 text-[12px] font-medium text-[var(--color-text-primary)] transition-colors duration-150 hover:bg-[var(--color-bg-4)] hover:text-[var(--color-text-bright)]" class="flex items-center px-3 py-1.5 text-meta font-medium text-[var(--color-text-primary)] transition-colors duration-150 hover:bg-[var(--color-bg-4)] hover:text-[var(--color-text-bright)]"
> >
Launch Launch
</button> </button>
@ -392,7 +392,7 @@
{/each} {/each}
</div> </div>
<p class="mt-3 text-[12px] text-[var(--color-text-muted)]"> <p class="mt-3 text-meta text-[var(--color-text-muted)]">
{filteredSnapshots.length} {filteredSnapshots.length === 1 ? 'snapshot' : 'snapshots'} {filteredSnapshots.length} {filteredSnapshots.length === 1 ? 'snapshot' : 'snapshots'}
{typeFilter !== 'all' ? `· filtered` : '· total'} {typeFilter !== 'all' ? `· filtered` : '· total'}
</p> </p>
@ -406,7 +406,7 @@
<footer class="flex h-7 shrink-0 items-center justify-end border-t border-[var(--color-border)] bg-[var(--color-bg-1)] px-7"> <footer class="flex h-7 shrink-0 items-center justify-end border-t border-[var(--color-border)] bg-[var(--color-bg-1)] px-7">
<div class="flex items-center gap-1.5"> <div class="flex items-center gap-1.5">
<span class="inline-flex h-[5px] w-[5px] rounded-full bg-[var(--color-accent)]"></span> <span class="inline-flex h-[5px] w-[5px] rounded-full bg-[var(--color-accent)]"></span>
<span class="font-mono text-[11px] uppercase tracking-[0.04em] text-[var(--color-text-secondary)]">All systems operational</span> <span class="font-mono text-label uppercase tracking-[0.04em] text-[var(--color-text-secondary)]">All systems operational</span>
</div> </div>
</footer> </footer>
</div> </div>
@ -427,7 +427,7 @@
openDropdownName = null; openDropdownName = null;
if (target) { deleteTarget = target; deleteError = null; } if (target) { deleteTarget = target; deleteError = null; }
}} }}
class="flex w-full items-center gap-2 px-3 py-2 text-[12px] text-[var(--color-red)] transition-colors duration-150 hover:bg-[var(--color-red)]/5" class="flex w-full items-center gap-2 px-3 py-2 text-meta text-[var(--color-red)] transition-colors duration-150 hover:bg-[var(--color-red)]/5"
> >
<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="shrink-0"> <svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="shrink-0">
<polyline points="3 6 5 6 21 6" /> <polyline points="3 6 5 6 21 6" />
@ -452,8 +452,8 @@
class="relative w-full max-w-[380px] rounded-[var(--radius-card)] border border-[var(--color-border-mid)] bg-[var(--color-bg-2)] p-6" class="relative w-full max-w-[380px] rounded-[var(--radius-card)] border border-[var(--color-border-mid)] bg-[var(--color-bg-2)] p-6"
style="animation: fadeUp 0.2s ease both" style="animation: fadeUp 0.2s ease both"
> >
<h2 class="font-serif text-[20px] tracking-[-0.02em] text-[var(--color-text-bright)]">Delete Snapshot</h2> <h2 class="font-serif text-heading tracking-[-0.02em] text-[var(--color-text-bright)]">Delete Snapshot</h2>
<p class="mt-2 text-[13px] text-[var(--color-text-tertiary)]"> <p class="mt-2 text-ui text-[var(--color-text-tertiary)]">
Delete <span class="font-mono font-medium text-[var(--color-text-secondary)]">{deleteTarget.name}</span>? Delete <span class="font-mono font-medium text-[var(--color-text-secondary)]">{deleteTarget.name}</span>?
This action cannot be undone. This action cannot be undone.
</p> </p>
@ -464,14 +464,14 @@
<path d="M10.29 3.86L1.82 18a2 2 0 0 0 1.71 3h16.94a2 2 0 0 0 1.71-3L13.71 3.86a2 2 0 0 0-3.42 0z" /> <path d="M10.29 3.86L1.82 18a2 2 0 0 0 1.71 3h16.94a2 2 0 0 0 1.71-3L13.71 3.86a2 2 0 0 0-3.42 0z" />
<line x1="12" y1="9" x2="12" y2="13" /><line x1="12" y1="17" x2="12.01" y2="17" /> <line x1="12" y1="9" x2="12" y2="13" /><line x1="12" y1="17" x2="12.01" y2="17" />
</svg> </svg>
<p class="text-[12px] leading-relaxed text-[var(--color-amber)]"> <p class="text-meta leading-relaxed text-[var(--color-amber)]">
This live capture includes saved memory state. Any capsule relying on it will be unable to resume. This live capture includes saved memory state. Any capsule relying on it will be unable to resume.
</p> </p>
</div> </div>
{/if} {/if}
{#if deleteError} {#if deleteError}
<div class="mt-4 rounded-[var(--radius-input)] border border-[var(--color-red)]/30 bg-[var(--color-red)]/5 px-3 py-2 text-[12px] text-[var(--color-red)]"> <div class="mt-4 rounded-[var(--radius-input)] border border-[var(--color-red)]/30 bg-[var(--color-red)]/5 px-3 py-2 text-meta text-[var(--color-red)]">
{deleteError} {deleteError}
</div> </div>
{/if} {/if}
@ -480,14 +480,14 @@
<button <button
onclick={() => (deleteTarget = null)} onclick={() => (deleteTarget = null)}
disabled={deleting} disabled={deleting}
class="rounded-[var(--radius-button)] border border-[var(--color-border)] px-4 py-2 text-[13px] text-[var(--color-text-secondary)] transition-colors duration-150 hover:border-[var(--color-border-mid)] hover:text-[var(--color-text-primary)] disabled:opacity-50" class="rounded-[var(--radius-button)] border border-[var(--color-border)] px-4 py-2 text-ui text-[var(--color-text-secondary)] transition-colors duration-150 hover:border-[var(--color-border-mid)] hover:text-[var(--color-text-primary)] disabled:opacity-50"
> >
Cancel Cancel
</button> </button>
<button <button
onclick={handleDelete} onclick={handleDelete}
disabled={deleting} disabled={deleting}
class="flex items-center gap-2 rounded-[var(--radius-button)] bg-[var(--color-red)] px-5 py-2 text-[13px] font-semibold text-white transition-all duration-150 hover:brightness-115 hover:-translate-y-px active:translate-y-0 disabled:opacity-50 disabled:hover:translate-y-0" class="flex items-center gap-2 rounded-[var(--radius-button)] bg-[var(--color-red)] px-5 py-2 text-ui font-semibold text-white transition-all duration-150 hover:brightness-115 hover:-translate-y-px active:translate-y-0 disabled:opacity-50 disabled:hover:translate-y-0"
> >
{#if deleting} {#if deleting}
<svg class="animate-spin" width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"> <svg class="animate-spin" width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
@ -517,20 +517,20 @@
class="relative w-full max-w-[420px] rounded-[var(--radius-card)] border border-[var(--color-border-mid)] bg-[var(--color-bg-2)] p-6" class="relative w-full max-w-[420px] rounded-[var(--radius-card)] border border-[var(--color-border-mid)] bg-[var(--color-bg-2)] p-6"
style="animation: fadeUp 0.2s ease both" style="animation: fadeUp 0.2s ease both"
> >
<h2 class="font-serif text-[20px] tracking-[-0.02em] text-[var(--color-text-bright)]">Launch Capsule</h2> <h2 class="font-serif text-heading tracking-[-0.02em] text-[var(--color-text-bright)]">Launch Capsule</h2>
<p class="mt-1 text-[13px] text-[var(--color-text-tertiary)]"> <p class="mt-1 text-ui text-[var(--color-text-tertiary)]">
Start a new capsule from this template. Start a new capsule from this template.
</p> </p>
{#if launchError} {#if launchError}
<div class="mt-4 rounded-[var(--radius-input)] border border-[var(--color-red)]/30 bg-[var(--color-red)]/5 px-3 py-2 text-[12px] text-[var(--color-red)]"> <div class="mt-4 rounded-[var(--radius-input)] border border-[var(--color-red)]/30 bg-[var(--color-red)]/5 px-3 py-2 text-meta text-[var(--color-red)]">
{launchError} {launchError}
</div> </div>
{/if} {/if}
<!-- Template name (readonly) --> <!-- Template name (readonly) -->
<div class="mt-5"> <div class="mt-5">
<label class="mb-1.5 block text-[11px] font-semibold uppercase tracking-[0.05em] text-[var(--color-text-tertiary)]"> <label class="mb-1.5 block text-label font-semibold uppercase tracking-[0.05em] text-[var(--color-text-tertiary)]">
Template Template
</label> </label>
<div class="flex items-center gap-2 rounded-[var(--radius-input)] border border-[var(--color-border)] bg-[var(--color-bg-0)] px-3 py-2"> <div class="flex items-center gap-2 rounded-[var(--radius-input)] border border-[var(--color-border)] bg-[var(--color-bg-0)] px-3 py-2">
@ -539,8 +539,8 @@
{:else} {:else}
<span class="inline-block h-[6px] w-[6px] shrink-0 rounded-full bg-[var(--color-blue)]"></span> <span class="inline-block h-[6px] w-[6px] shrink-0 rounded-full bg-[var(--color-blue)]"></span>
{/if} {/if}
<span class="flex-1 font-mono text-[13px] text-[var(--color-text-bright)]">{launchTarget.name}</span> <span class="flex-1 font-mono text-ui text-[var(--color-text-bright)]">{launchTarget.name}</span>
<span class="text-[11px] text-[var(--color-text-muted)]"> <span class="text-label text-[var(--color-text-muted)]">
{launchTarget.type === 'snapshot' ? 'Snapshot' : 'Image'} {launchTarget.type === 'snapshot' ? 'Snapshot' : 'Image'}
</span> </span>
</div> </div>
@ -549,11 +549,11 @@
<!-- vCPUs + Memory --> <!-- vCPUs + Memory -->
<div class="mt-4 grid grid-cols-2 gap-3"> <div class="mt-4 grid grid-cols-2 gap-3">
<div> <div>
<label class="mb-1.5 block text-[11px] font-semibold uppercase tracking-[0.05em] text-[var(--color-text-tertiary)]" for="launch-vcpus"> <label class="mb-1.5 block text-label font-semibold uppercase tracking-[0.05em] text-[var(--color-text-tertiary)]" for="launch-vcpus">
vCPUs vCPUs
</label> </label>
{#if launchTarget.type === 'snapshot'} {#if launchTarget.type === 'snapshot'}
<div class="rounded-[var(--radius-input)] border border-[var(--color-border)] bg-[var(--color-bg-0)] px-3 py-2 font-mono text-[13px] text-[var(--color-text-muted)]"> <div class="rounded-[var(--radius-input)] border border-[var(--color-border)] bg-[var(--color-bg-0)] px-3 py-2 font-mono text-ui text-[var(--color-text-muted)]">
{launchTarget.vcpus ?? 1} {launchTarget.vcpus ?? 1}
</div> </div>
{:else} {:else}
@ -563,17 +563,17 @@
min="1" min="1"
max="32" max="32"
bind:value={launchVcpus} bind:value={launchVcpus}
class="w-full rounded-[var(--radius-input)] border border-[var(--color-border)] bg-[var(--color-bg-4)] px-3 py-2 font-mono text-[13px] text-[var(--color-text-bright)] outline-none transition-colors duration-150 focus:border-[var(--color-accent)]" class="w-full rounded-[var(--radius-input)] border border-[var(--color-border)] bg-[var(--color-bg-4)] px-3 py-2 font-mono text-ui text-[var(--color-text-bright)] outline-none transition-colors duration-150 focus:border-[var(--color-accent)]"
/> />
{/if} {/if}
</div> </div>
<div> <div>
<label class="mb-1.5 block text-[11px] font-semibold uppercase tracking-[0.05em] text-[var(--color-text-tertiary)]" for="launch-memory"> <label class="mb-1.5 block text-label font-semibold uppercase tracking-[0.05em] text-[var(--color-text-tertiary)]" for="launch-memory">
Memory (MB) Memory (MB)
</label> </label>
{#if launchTarget.type === 'snapshot'} {#if launchTarget.type === 'snapshot'}
<div class="rounded-[var(--radius-input)] border border-[var(--color-border)] bg-[var(--color-bg-0)] px-3 py-2 font-mono text-[13px] text-[var(--color-text-muted)]"> <div class="rounded-[var(--radius-input)] border border-[var(--color-border)] bg-[var(--color-bg-0)] px-3 py-2 font-mono text-ui text-[var(--color-text-muted)]">
{launchTarget.memory_mb ?? 512} {launchTarget.memory_mb ?? 512}
</div> </div>
{:else} {:else}
@ -583,7 +583,7 @@
min="128" min="128"
step="128" step="128"
bind:value={launchMemoryMb} bind:value={launchMemoryMb}
class="w-full rounded-[var(--radius-input)] border border-[var(--color-border)] bg-[var(--color-bg-4)] px-3 py-2 font-mono text-[13px] text-[var(--color-text-bright)] outline-none transition-colors duration-150 focus:border-[var(--color-accent)]" class="w-full rounded-[var(--radius-input)] border border-[var(--color-border)] bg-[var(--color-bg-4)] px-3 py-2 font-mono text-ui text-[var(--color-text-bright)] outline-none transition-colors duration-150 focus:border-[var(--color-accent)]"
/> />
{/if} {/if}
</div> </div>
@ -591,13 +591,13 @@
<!-- Timeout --> <!-- Timeout -->
<div class="mt-4"> <div class="mt-4">
<label class="mb-1.5 block text-[11px] font-semibold uppercase tracking-[0.05em] text-[var(--color-text-tertiary)]" for="launch-timeout">Auto-pause timeout (seconds, 0 = never)</label> <label class="mb-1.5 block text-label font-semibold uppercase tracking-[0.05em] text-[var(--color-text-tertiary)]" for="launch-timeout">Auto-pause timeout (seconds, 0 = never)</label>
<input <input
id="launch-timeout" id="launch-timeout"
type="number" type="number"
min="0" min="0"
bind:value={launchTimeoutSec} bind:value={launchTimeoutSec}
class="w-full rounded-[var(--radius-input)] border border-[var(--color-border)] bg-[var(--color-bg-4)] px-3 py-2 font-mono text-[13px] text-[var(--color-text-bright)] outline-none transition-colors duration-150 focus:border-[var(--color-accent)]" class="w-full rounded-[var(--radius-input)] border border-[var(--color-border)] bg-[var(--color-bg-4)] px-3 py-2 font-mono text-ui text-[var(--color-text-bright)] outline-none transition-colors duration-150 focus:border-[var(--color-accent)]"
/> />
</div> </div>
@ -605,14 +605,14 @@
<button <button
onclick={() => (launchTarget = null)} onclick={() => (launchTarget = null)}
disabled={launching} disabled={launching}
class="rounded-[var(--radius-button)] border border-[var(--color-border)] px-4 py-2 text-[13px] text-[var(--color-text-secondary)] transition-colors duration-150 hover:border-[var(--color-border-mid)] hover:text-[var(--color-text-primary)] disabled:opacity-50" class="rounded-[var(--radius-button)] border border-[var(--color-border)] px-4 py-2 text-ui text-[var(--color-text-secondary)] transition-colors duration-150 hover:border-[var(--color-border-mid)] hover:text-[var(--color-text-primary)] disabled:opacity-50"
> >
Cancel Cancel
</button> </button>
<button <button
onclick={handleLaunch} onclick={handleLaunch}
disabled={launching} disabled={launching}
class="flex items-center gap-2 rounded-[var(--radius-button)] bg-[var(--color-accent)] px-5 py-2 text-[13px] font-semibold text-white transition-all duration-150 hover:brightness-115 hover:-translate-y-px active:translate-y-0 disabled:opacity-50 disabled:hover:translate-y-0" class="flex items-center gap-2 rounded-[var(--radius-button)] bg-[var(--color-accent)] px-5 py-2 text-ui font-semibold text-white transition-all duration-150 hover:brightness-115 hover:-translate-y-px active:translate-y-0 disabled:opacity-50 disabled:hover:translate-y-0"
> >
{#if launching} {#if launching}
<svg class="animate-spin" width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"> <svg class="animate-spin" width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">

View File

@ -1,5 +1,7 @@
<script lang="ts"> <script lang="ts">
import { goto } from '$app/navigation'; import { goto } from '$app/navigation';
import { onMount } from 'svelte';
import { page } from '$app/stores';
import { auth } from '$lib/auth.svelte'; import { auth } from '$lib/auth.svelte';
import { apiLogin, apiSignup } from '$lib/api/auth'; import { apiLogin, apiSignup } from '$lib/api/auth';
import { import {
@ -17,6 +19,12 @@
let error = $state(''); let error = $state('');
let loading = $state(false); let loading = $state(false);
// Read OAuth error forwarded from /auth/github/callback
onMount(() => {
const urlErr = $page.url.searchParams.get('error');
if (urlErr) error = decodeURIComponent(urlErr);
});
// Mouse-reactive glow — moves opposite to cursor with viscous drag // Mouse-reactive glow — moves opposite to cursor with viscous drag
let glowX = $state(50); let glowX = $state(50);
let glowY = $state(50); let glowY = $state(50);
@ -107,7 +115,7 @@
<!-- Mouse-reactive radial glow --> <!-- Mouse-reactive radial glow -->
<div <div
class="pointer-events-none absolute inset-0" class="pointer-events-none absolute inset-0"
style="background: radial-gradient(ellipse 55% 45% at {glowX}% {glowY}%, rgba(94, 140, 88, 0.09) 0%, transparent 70%)" style="background: radial-gradient(ellipse 55% 45% at {glowX}% {glowY}%, rgba(94, 140, 88, 0.14) 0%, transparent 70%)"
aria-hidden="true" aria-hidden="true"
></div> ></div>
@ -118,7 +126,7 @@
> >
<img src="/logo.svg" alt="Wrenn" class="h-20 w-20 rounded-[var(--radius-card)]" /> <img src="/logo.svg" alt="Wrenn" class="h-20 w-20 rounded-[var(--radius-card)]" />
<span <span
class="mt-5 font-brand text-[44px] tracking-[-0.01em] text-[var(--color-text-bright)]" class="mt-5 font-brand text-[3.143rem] tracking-[-0.01em] text-[var(--color-text-bright)]"
> >
Wrenn Wrenn
</span> </span>
@ -126,19 +134,19 @@
<!-- Tagline below logo --> <!-- Tagline below logo -->
<div <div
class="relative z-10 mt-10 text-center" class="relative z-10 mt-16 max-w-[360px] text-center"
style="animation: fadeUp 0.35s ease 0.1s both" style="animation: fadeUp 0.35s ease 0.1s both"
> >
<h1 <h1
class="font-serif text-[42px] leading-[1.15] tracking-[-0.03em] text-[var(--color-text-bright)]" class="font-serif text-[5rem] leading-[1.1] tracking-[-0.04em] text-[var(--color-text-bright)]"
> >
Scale Up. Spin Out. Scale Up.<br />Spin Out.
</h1> </h1>
</div> </div>
<!-- Sub-tagline --> <!-- Sub-tagline -->
<p <p
class="relative z-10 mt-6 font-mono text-[13px] uppercase tracking-[0.06em] text-[var(--color-text-tertiary)]" class="relative z-10 mt-12 font-mono text-ui uppercase tracking-[0.08em] text-[var(--color-text-tertiary)]"
style="animation: fadeUp 0.35s ease 0.2s both" style="animation: fadeUp 0.35s ease 0.2s both"
> >
Run Anything. Worry about Nothing. Run Anything. Worry about Nothing.
@ -156,7 +164,7 @@
> >
<img src="/logo.svg" alt="Wrenn" class="h-12 w-12 rounded-[var(--radius-card)]" /> <img src="/logo.svg" alt="Wrenn" class="h-12 w-12 rounded-[var(--radius-card)]" />
<span <span
class="mt-2 font-brand text-[24px] tracking-[-0.01em] text-[var(--color-text-bright)]" class="mt-2 font-brand text-page tracking-[-0.01em] text-[var(--color-text-bright)]"
> >
Wrenn Wrenn
</span> </span>
@ -166,11 +174,11 @@
<!-- Header --> <!-- Header -->
<div class="mb-8"> <div class="mb-8">
<h2 <h2
class="font-serif text-[34px] tracking-[-0.02em] text-[var(--color-text-bright)]" class="font-serif text-display tracking-[-0.02em] text-[var(--color-text-bright)]"
> >
{title} {title}
</h2> </h2>
<p class="mt-2 text-[14px] text-[var(--color-text-secondary)]"> <p class="mt-2 text-body text-[var(--color-text-secondary)]">
{subtitle} {subtitle}
</p> </p>
</div> </div>
@ -178,7 +186,7 @@
<!-- GitHub OAuth --> <!-- GitHub OAuth -->
<a <a
href="/api/auth/oauth/github" href="/api/auth/oauth/github"
class="flex w-full items-center justify-center gap-2.5 rounded-[var(--radius-button)] border border-[var(--color-border-mid)] bg-[var(--color-bg-2)] px-4 py-3 text-[14px] font-medium text-[var(--color-text-bright)] no-underline transition-all duration-150 hover:border-[var(--color-accent)] hover:text-[var(--color-text-bright)]" class="flex w-full items-center justify-center gap-2.5 rounded-[var(--radius-button)] border border-[var(--color-border-mid)] bg-[var(--color-bg-2)] px-4 py-3 text-body font-medium text-[var(--color-text-bright)] no-underline transition-all duration-150 hover:border-[var(--color-accent)] hover:text-[var(--color-text-bright)]"
> >
<IconGithub size={16} /> <IconGithub size={16} />
Continue with GitHub Continue with GitHub
@ -188,7 +196,7 @@
<div class="my-6 flex items-center gap-3"> <div class="my-6 flex items-center gap-3">
<div class="h-px flex-1 bg-[var(--color-border)]"></div> <div class="h-px flex-1 bg-[var(--color-border)]"></div>
<span <span
class="font-mono text-[10px] uppercase tracking-[0.1em] text-[var(--color-text-muted)]" class="font-mono text-badge uppercase tracking-[0.1em] text-[var(--color-text-muted)]"
>or</span >or</span
> >
<div class="h-px flex-1 bg-[var(--color-border)]"></div> <div class="h-px flex-1 bg-[var(--color-border)]"></div>
@ -207,7 +215,7 @@
bind:value={email} bind:value={email}
placeholder="Email address" placeholder="Email address"
autocomplete="email" autocomplete="email"
class="w-full rounded-[var(--radius-input)] border border-[var(--color-border)] bg-[var(--color-bg-2)] py-3 pl-9 pr-3 text-[14px] text-[var(--color-text-bright)] outline-none transition-all duration-150 placeholder:text-[var(--color-text-muted)] focus:border-[var(--color-accent)]" class="w-full rounded-[var(--radius-input)] border border-[var(--color-border)] bg-[var(--color-bg-2)] py-3 pl-9 pr-3 text-body text-[var(--color-text-bright)] outline-none transition-all duration-150 placeholder:text-[var(--color-text-muted)] focus:border-[var(--color-accent)]"
/> />
</div> </div>
@ -222,7 +230,7 @@
bind:value={password} bind:value={password}
placeholder="Password" placeholder="Password"
autocomplete={mode === 'signin' ? 'current-password' : 'new-password'} autocomplete={mode === 'signin' ? 'current-password' : 'new-password'}
class="w-full rounded-[var(--radius-input)] border border-[var(--color-border)] bg-[var(--color-bg-2)] py-3 pl-9 pr-10 text-[14px] text-[var(--color-text-bright)] outline-none transition-all duration-150 placeholder:text-[var(--color-text-muted)] focus:border-[var(--color-accent)]" class="w-full rounded-[var(--radius-input)] border border-[var(--color-border)] bg-[var(--color-bg-2)] py-3 pl-9 pr-10 text-body text-[var(--color-text-bright)] outline-none transition-all duration-150 placeholder:text-[var(--color-text-muted)] focus:border-[var(--color-accent)]"
/> />
<button <button
type="button" type="button"
@ -242,7 +250,7 @@
<div class="flex justify-end"> <div class="flex justify-end">
<button <button
type="button" type="button"
class="text-[13px] text-[var(--color-text-secondary)] transition-colors duration-150 hover:text-[var(--color-accent-mid)]" class="text-ui text-[var(--color-text-secondary)] transition-colors duration-150 hover:text-[var(--color-accent-mid)]"
> >
Forgot password? Forgot password?
</button> </button>
@ -250,13 +258,13 @@
{/if} {/if}
{#if error} {#if error}
<p class="text-[13px] text-[var(--color-red)]">{error}</p> <p class="text-ui text-[var(--color-red)]">{error}</p>
{/if} {/if}
<button <button
type="submit" type="submit"
disabled={loading} disabled={loading}
class="!mt-5 w-full rounded-[var(--radius-button)] bg-[var(--color-accent)] px-4 py-3 text-[14px] font-semibold text-white transition-all duration-150 hover:brightness-115 hover:-translate-y-px active:translate-y-0 disabled:pointer-events-none disabled:opacity-50" class="!mt-5 w-full rounded-[var(--radius-button)] bg-[var(--color-accent)] px-4 py-3 text-body font-semibold text-white transition-all duration-150 hover:brightness-115 hover:-translate-y-px active:translate-y-0 disabled:pointer-events-none disabled:opacity-50"
> >
{#if loading} {#if loading}
<span class="inline-flex items-center gap-2"> <span class="inline-flex items-center gap-2">
@ -272,7 +280,7 @@
</form> </form>
<!-- Switch mode --> <!-- Switch mode -->
<p class="mt-6 text-center text-[13px] text-[var(--color-text-secondary)]"> <p class="mt-6 text-center text-ui text-[var(--color-text-secondary)]">
{switchText} {switchText}
<button <button
type="button" type="button"