1
0
forked from wrenn/wrenn

Merge pull request 'Minor frontend enhancement' (#3) from frontend into dev

Reviewed-on: wrenn/sandbox#3
This commit is contained in:
2026-03-24 06:36:17 +00:00
11 changed files with 369 additions and 438 deletions

1
.gitignore vendored
View File

@ -34,6 +34,7 @@ go.work.sum
## AI
.claude/
e2b/
.impeccable.md
## Builds
builds/

246
CLAUDE.md
View File

@ -230,186 +230,68 @@ The main module (`go.mod`) and envd (`envd/go.mod`) are fully independent. `make
- Sandbox clones: `/var/lib/wrenn/sandboxes/`
- Firecracker: `/usr/local/bin/firecracker` (e2b's fork of firecracker)
## Web UI Styling
## Design Context
### Identity
Warm, confident developer tool with industrial precision and crafted organic character. The feel is sharp and data-forward — not cold or sterile, but not soft either. Think: an engineer's favorite tool, built with care.
---
### Color Palette (Dark Mode)
**Background scale (6 steps, near-black-green):**
`#0a0c0b` (bg-0, page base) → `#0f1211` (bg-1, sidebar/topbar) → `#141817` (bg-2, cards/surfaces) → `#1a1e1c` (bg-3, hover states/elevated) → `#212624` (bg-4, inputs/avatars) → `#2a302d` (bg-5, active controls)
**Text hierarchy (5 levels):**
- Bright `#eae7e2` — page titles, metric values, active states
- Primary `#d0cdc6` — body text, nav labels, readable content
- Secondary `#9b9790` — supporting text, inactive nav, descriptions
- Tertiary `#6b6862` — labels, section headers, timestamps
- Muted `#454340` — ghost text, disabled states, grid labels
**Sage green brand accent (3 tiers + 2 glows):**
- Solid `#5e8c58` — primary accent, buttons, borders, active indicators
- Mid `#89a785` — badges, chart lines, secondary accent
- Bright `#a4c89f` — active nav text, live counts, chart dots
- Glow `rgba(94,140,88,0.07)` — active nav backgrounds, subtle highlights
- Glow Mid `rgba(94,140,88,0.14)` — live badges, status badge backgrounds
**Borders (2 levels):**
- Default `#1f2321` — card edges, dividers, sidebar borders
- Mid `#2a2f2c` — hover states, interactive borders, stronger separation
**Semantic status colors:**
- Amber `#d4a73c` — warning, building, countdown timers
- Red `#cf8172` — error, failed, destructive actions
- Blue `#5a9fd4` — info, stopped (use sparingly)
**Light mode:** (TBD — follow same warm-tinted approach. Background scale from `#f8f6f1``#dedbd5`. Text hierarchy inverts. Accent stays `#5e8c58` for solid.)
---
### Typography
Four fonts, each with a clear role:
| Font | Role | Weights | Where |
|------|------|---------|-------|
| **Manrope** (variable) | Body, UI | 400700 | All body text, nav labels, buttons, descriptions, section headers |
| **Instrument Serif** | Display, metrics | 400 | Page titles (h1), large metric values, empty-state headings only |
| **JetBrains Mono** | Code, data | 400600 | Status bar, time range buttons, search inputs, IDs, commit SHAs, countdown timers, log viewer, URL paths, code blocks |
| **Alice** | Brand wordmark | 400 | Sidebar wordmark only — never used elsewhere |
**Sizing:**
- Base body: `14px`
- Page title (h1): `24px` serif, `letter-spacing: -0.02em`
- Card metric values: `36px` serif, `letter-spacing: -0.04em`
- Chart inline metric: `30px` serif, `letter-spacing: -0.04em`
- Nav items: `13px` body, weight 500
- Section/group labels: `11px` body, uppercase, `letter-spacing: 0.06em`, weight 600
- Chart section labels: `12px` body, uppercase, `letter-spacing: 0.05em`, weight 600
- Stat cell labels: `11px` body, uppercase, `letter-spacing: 0.05em`, weight 600
- Badge text: `10px`, uppercase, `letter-spacing: 0.04em`, weight 600
- Status bar / footer links: `1112px` mono
- Table headers: `11px` body, uppercase, `letter-spacing: 0.05em`, weight 600, color muted
- Table body cells: `13px`
**Key rule:** Instrument Serif is reserved exclusively for page-level titles and large numeric values. It provides warmth and character without softness. Everything else uses Manrope (UI) or JetBrains Mono (data/code).
---
### Spacing
4px base unit (Tailwind scale). Moderate density — functional and confident, never cramped.
- Page content padding: `2428px`
- Card/surface internal padding: `1820px`
- Sidebar width: `230px`
- Sidebar nav item padding: `8px 10px`
- Sidebar brand area: `18px 16px 16px`
- Tab bar items: `10px 16px`
- Topbar: `16px 28px`
- Metric strip cell: `18px 20px`
- Chart header: `18px 20px`
- Chart canvas: `14px 20px 12px`
- Table header cells: `11px 16px`
- Table body cells: `12px 16px`
- Status bar: `6px 28px`
- Between sections (cards): `2024px` margin-bottom
---
### Borders & Depth
**Flat aesthetic — no drop shadows.** Depth comes from background color stepping (bg-0 → bg-1 → bg-2 → bg-3), not shadows. `--shadow-sm: 0 0 #0000`.
- All borders: `1px solid` in warm muted tones
- Corner radii: cards/surfaces `8px`, inputs/buttons `5px`, logo mark `6px`, avatars `5px`, dots `50%`
- Connected metric cells use shared border container with `border-left: 1px solid` between cells (no gap/grid trick) — creates the industrial panel look
- Tables wrapped in `border-radius: 8px` container with overflow hidden
---
### Components
**Sidebar navigation:**
- Active items use `3px left-border` in sage solid (`#5e8c58`) with accent glow background (`rgba(94,140,88,0.07)`)
- Active text color: accent-bright (`#a4c89f`)
- Icons at `16px`, opacity 0.5 default, 1.0 on active
- Group labels: `11px` uppercase with `0.06em` tracking, muted color
**Status chip (live indicator):**
- Rounded `8px` border, `bg-2` background, `border-mid` border
- Pulsing dot: `7px`, accent-solid fill, `box-shadow: 0 0 8px rgba(94,140,88,0.5)` with glow animation
- Count in mono at `14px` accent-bright, label in secondary text
**Live badges (inline):**
- `10px` text, uppercase, `3px` border-radius
- Background: accent-glow-mid (`rgba(94,140,88,0.14)`), text: accent mid
- Includes `5px` pulsing dot with box-shadow
**Metric strip:**
- 3-column grid, connected cells (single outer border, inner dividers)
- Hover: background steps from bg-2 to bg-3
- Value: `36px` serif, bright text
- Label: `11px` uppercase, tertiary
- Sub-metadata row with `1px` divider between items
**Chart cards:**
- `8px` border-radius, bg-2 background, default border
- Header: section label (12px uppercase) + large serif metric + live badge
- Range group: segmented buttons with `1px` borders, mono text, active state uses bg-5
- Chart area: SVG with `0.5px` grid lines in border color, `10px` mono axis labels in muted
- Data line: `1.5px` accent-solid stroke, `stroke-linejoin: round`
- Area fill: gradient from `rgba(94,140,88,0.28)` → transparent
- Data dot: accent-bright fill, `2.5px` bg-2 stroke, `4px` radius
**Buttons hierarchy:**
1. Ghost (icon-btn): transparent bg, default border, tertiary color → border-mid + secondary on hover
2. Outline: no bg, border-mid border → accent-solid border + primary text on hover
3. Tool: bg-2 background, default border → border-mid + primary on hover
4. Filled/CTA: accent-solid background, white text → lighter green on hover, subtle `translateY(-1px)` lift
**Tables:**
- Container: `8px` border-radius, border, overflow hidden
- Header: bg-3 background, `11px` uppercase muted text
- Body: default bg, `1px` border-bottom between rows
- Row hover: bg-3
**Empty states:**
- Centered, `72px` vertical padding
- Icon container: `56px` square, bg-3, border-mid border, `8px` radius
- Heading: `20px` serif, bright text
- Description: `13px` body, tertiary text
- CTA button below
**Inputs:**
- bg-2 background, default border, `5px` radius
- Mono font for search/filter inputs
- Focus: `border-color: accent-solid` (clean single ring, no double-ring)
- Placeholder: muted color
**Focus rings:** Single accent-solid border-color change on focus. Clean and minimal — no double-ring outlines.
---
### Animation
- **All interactive transitions:** `150ms ease`
- **Page load / section entrance:** `fadeUp``opacity: 0, translateY(6px)` → visible, `0.35s ease`, staggered with `6080ms` delays between elements
- **Chart data animation:** SVG `<animate>` on path `d`, polyline `points`, and circle `cy``0.50.6s` duration, `0.20.35s` begin delay, `fill: freeze`
- **Live status dot:** `glow` keyframe — `2.5s ease infinite` box-shadow bloom from `0 0 6px rgba(94,140,88,0.5)``0 0 14px rgba(94,140,88,0.2)`
- **CTA buttons:** subtle `translateY(-1px)` on hover for lift feel
---
### Dark Mode
Primary and default mode. Very dark near-black-green backgrounds (`#0a0c0b` base) with warm off-white text and desaturated sage accent. Completely flat — no card shadows anywhere. System preference detection + localStorage persistence.
---
### Overall Feel
Sharp, warm, industrial-confident. Avoids cold grays entirely — palette leans slightly warm/brown-tinted throughout. The serif display type provides organic character and warmth on titles and metrics, while Manrope handles readable UI text and JetBrains Mono anchors the data-forward, developer-tool identity. Connected metric panels, tight chart cards, and uppercase section labels create engineering density without sacrificing readability. The result is a tool that feels crafted and precise — designed by someone who uses developer tools daily.
### Users
Developers across the full spectrum — solo engineers building side projects, startup teams integrating sandboxed execution into products, and platform/infra engineers at larger organizations. The interface must feel at home for all three: approachable enough not to intimidate a hacker, precise enough to earn the trust of a production ops team. Never condescend, never oversimplify. Trust the user to understand what they're looking at.
### Brand Personality
**Precise. Warm. Uncompromising.**
Wrenn is an engineer's favorite tool — built with visible care, not assembled from defaults. It runs real infrastructure (Firecracker microVMs), so the UI should reflect that seriousness without becoming cold or corporate. The warmth comes from the typography and color palette; the precision comes from hierarchy, density, and data fidelity.
Emotional goal: **in control.** Users leave a session with full confidence in what's running, what happened, and what comes next. Nothing is hidden, nothing is ambiguous.
### Aesthetic Direction
**Dark-first, industrial-warm, data-forward.**
The near-black-green background palette (`#0a0c0b` through `#2a302d`) reads as "black with intention" — not pitch black (cold) and not charcoal (dated). The sage green accent (`#5e8c58`) is muted and organic, a meaningful departure from the startup-green neon that saturates the developer tool space.
**Anti-references:**
- **Supabase**: avoid the friendly, approachable startup-green energy — too generic, too eager to please
- **AWS / GCP consoles**: avoid utility-first density without craft — functional but joyless, visually dated
**References that capture the right spirit:**
- The precision of a well-calibrated instrument
- Editorial typography from technical publications
- The quiet confidence of tools that don't need to explain themselves
### Type System
Four fonts with strict roles — this is the design system's strongest personality trait and must be respected:
| Font | Role | When to use |
|------|------|-------------|
| **Manrope** (variable, sans) | UI workhorse | All body copy, nav, labels, buttons, form text |
| **Instrument Serif** | Display / editorial | Page titles (h1), dialog headings, metric values, hero moments |
| **JetBrains Mono** (variable) | Data / code | IDs, timestamps, key prefixes, file paths, terminal output, metrics |
| **Alice** | Brand wordmark | "Wrenn" in sidebar and login only — nowhere else |
Instrument Serif at scale creates the signature editorial moments. Mono provides the precision signal for technical data. Never swap these roles.
### Color System
```
Backgrounds: bg-0 (#0a0c0b) through bg-5 (#2a302d) — 6 steps
Text: bright > primary > secondary > tertiary > muted — 5 levels
Accent: accent (#5e8c58) / accent-mid / accent-bright / glow / glow-mid
Status: amber (#d4a73c) / red (#cf8172) / blue (#5a9fd4)
```
Use accent sparingly. It should feel earned — reserved for live/active state indicators, primary CTAs, focus rings, and active nav. When accent appears, it should register.
### Upcoming Surfaces (design must accommodate)
- **Terminal / shell output**: streaming exec output, TTY sessions. Needs strong mono treatment, high contrast for long sessions.
- **File browser**: filesystem tree inside capsule. Density matters — breadcrumbs, file icons, permission bits.
- **SDK / docs embedding**: code samples, quickstart flows inline in dashboard. Code blocks must feel premium, not afterthought.
- **Billing / usage charts**: pool consumption, cost curves, usage over time. Instrument Serif at large scale for metrics; chart containers should feel like instruments, not dashboards.
### Design Principles
1. **Precision over friendliness.** Every element earns its place. Wrenn doesn't need to tell you it's developer-friendly — that should be self-evident from the quality of the information architecture.
2. **Density with breathing room.** Data-forward doesn't mean cramped. Strategic whitespace creates calm hierarchy within dense contexts. Sections breathe; rows don't waste space.
3. **Industrial warmth.** The serif + mono + warm-black combination prevents sterility. This is a forge, not a gallery. The warmth is in the details, not the primary colors.
4. **Legible at speed.** Users scan dashboards in seconds. Strong typographic contrast (serif h1, mono IDs, sans body), consistent patterns, and predictable placement let users orientate instantly without reading everything.
5. **Craft signals trust.** For infrastructure that runs production code, the quality of the UI is a proxy for the quality of the product. Pixel-level decisions matter. Polish is not decoration — it's a trust signal.

View File

@ -48,6 +48,20 @@
--font-mono: 'JetBrains Mono Variable', monospace;
--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 */
--radius-card: 8px;
--radius-input: 5px;
@ -62,9 +76,13 @@
/* Base styles */
html {
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);
background-color: var(--color-bg-0);
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
text-rendering: optimizeLegibility;
}
body {
@ -72,6 +90,11 @@ body {
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 {
background: rgba(94, 140, 88, 0.25);
@ -97,14 +120,14 @@ body {
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 {
0%,
100% {
box-shadow: 0 0 6px rgba(94, 140, 88, 0.5);
opacity: 1;
}
50% {
box-shadow: 0 0 14px rgba(94, 140, 88, 0.2);
opacity: 0.3;
}
}
@ -119,3 +142,15 @@ body {
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 -->
<div class="mb-7">
<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}
</Dialog.Title>
<Dialog.Description
class="mt-1 text-[13px] text-[var(--color-text-secondary)]"
class="mt-1 text-ui text-[var(--color-text-secondary)]"
>
{subtitle}
</Dialog.Description>
@ -75,7 +75,7 @@
<!-- GitHub OAuth -->
<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} />
Continue with GitHub
@ -85,7 +85,7 @@
<div class="my-5 flex items-center gap-3">
<div class="h-px flex-1 bg-[var(--color-border)]"></div>
<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
>
<div class="h-px flex-1 bg-[var(--color-border)]"></div>
@ -105,7 +105,7 @@
bind:value={name}
placeholder="Full 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>
{/if}
@ -121,7 +121,7 @@
bind:value={email}
placeholder="Email address"
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>
@ -136,7 +136,7 @@
bind:value={password}
placeholder="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
type="button"
@ -156,7 +156,7 @@
<div class="flex justify-end">
<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?
</button>
@ -165,14 +165,14 @@
<button
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}
</button>
</form>
<!-- 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}
<button
type="button"

View File

@ -74,7 +74,7 @@
alt="Wrenn"
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>
{/if}
<button
@ -95,18 +95,18 @@
: 'gap-2 px-2.5'}"
>
<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]}
</div>
{#if !collapsed}
<div class="min-w-0 flex-1 overflow-hidden whitespace-nowrap">
<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
</div>
<div class="truncate text-[13px] text-[var(--color-text-primary)]">
<div class="truncate text-ui text-[var(--color-text-primary)]">
{currentTeam}
</div>
</div>
@ -126,20 +126,20 @@
style="animation: popoverSlideIn 150ms ease"
>
<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
</div>
{#each teams as team}
<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
? 'bg-[var(--color-accent-glow)]'
: ''}"
onclick={() => (teamPopoverOpen = false)}
>
<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
? 'bg-[var(--color-accent)]'
: 'bg-[var(--color-bg-5)]'}"
@ -157,7 +157,7 @@
{/each}
<div class="mt-0.5 border-t border-[var(--color-border)] pt-0.5">
<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} />
Create team
@ -186,7 +186,7 @@
title={collapsed ? 'Docs' : undefined}
>
<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
href="/dashboard/notifications"
@ -194,7 +194,7 @@
title={collapsed ? 'Notifications' : undefined}
>
<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
href="/dashboard/settings"
@ -202,7 +202,7 @@
title={collapsed ? 'Settings' : undefined}
>
<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>
</div>
@ -214,11 +214,11 @@
>
{#if !collapsed}
<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] ?? ''}
</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}
</span>
{/if}
@ -242,7 +242,7 @@
{/if}
{:else}
<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}
</div>
@ -263,7 +263,7 @@
{/if}
<item.icon size={16} class="shrink-0 text-[var(--color-accent-bright)]" />
{#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}
</span>
{/if}
@ -282,7 +282,7 @@
/>
{#if !collapsed}
<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}
</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">
{#each toast.list as t (t.id)}
<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-accent)]/30 text-[var(--color-accent-bright)]'}"
style="animation: fadeUp 0.2s ease both"

View File

@ -24,5 +24,5 @@
</script>
<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>

View File

@ -282,14 +282,14 @@
<div class="flex flex-1 flex-col overflow-hidden">
<main class="flex-1 overflow-y-auto bg-[var(--color-bg-0)]">
<!-- Header area -->
<div class="px-7 pt-6">
<div class="px-7 pt-8">
<!-- Top row: title + status chip -->
<div class="flex items-center justify-between">
<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
</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.
</p>
</div>
@ -306,17 +306,17 @@
></span>
<span class="relative inline-flex h-[7px] w-[7px] rounded-full bg-[var(--color-accent)]"></span>
</span>
<span class="font-mono text-[14px] font-semibold text-[var(--color-accent-bright)]">{runningCount}</span>
<span class="text-[13px] text-[var(--color-text-secondary)]">concurrent capsules</span>
<span class="font-mono text-body font-semibold text-[var(--color-accent-bright)]">{runningCount}</span>
<span class="text-ui text-[var(--color-text-secondary)]">concurrent capsules</span>
</div>
</div>
</div>
<!-- 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
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-transparent text-[var(--color-text-secondary)] hover:text-[var(--color-text-primary)]'}"
>
@ -328,7 +328,7 @@
</button>
<button
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-transparent text-[var(--color-text-secondary)] hover:text-[var(--color-text-primary)]'}"
>
@ -342,7 +342,7 @@
<!-- Tab content -->
{#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)]">
{@render metricCell('Concurrent Capsules', String(runningCount), '5-sec avg', 'limit: 20', 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')}
</div>
{: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 -->
<div class="mb-4 flex items-center gap-3">
<div class="relative flex-1 max-w-[300px]">
@ -364,10 +364,10 @@
type="text"
placeholder="Search by ID..."
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>
<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>
@ -390,7 +390,7 @@
<!-- Auto-refresh countdown toggle -->
<button
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
? '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)]'}"
@ -405,7 +405,7 @@
<button
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">
<line x1="12" y1="5" x2="12" y2="19" /><line x1="5" y1="12" x2="19" y2="12" />
@ -415,7 +415,7 @@
</div>
{#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}
</div>
{/if}
@ -424,8 +424,8 @@
<div class="rounded-[var(--radius-card)] border border-[var(--color-border)] overflow-hidden">
<!-- 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="px-4 py-[11px] text-[11px] 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)]">ID</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('Memory', 'memory_mb')}
{@render sortableHeader('Idle Timeout', 'timeout_sec')}
@ -435,7 +435,7 @@
{#if loading && capsules.length === 0}
<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">
<path d="M21 12a9 9 0 1 1-6.219-8.56" />
</svg>
@ -451,17 +451,17 @@
<line x1="12" y1="17" x2="12" y2="21" />
</svg>
</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
</p>
<p class="mt-1.5 text-[13px] text-[var(--color-text-tertiary)]">
Active capsules will appear here.
<p class="mt-1.5 text-ui text-[var(--color-text-tertiary)]">
Launch a capsule to start running isolated code.
</p>
<button
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">
<line x1="12" y1="5" x2="12" y2="19" /><line x1="5" y1="12" x2="19" y2="12" />
</svg>
@ -474,7 +474,7 @@
style="animation: fadeUp 0.35s ease both; animation-delay: {i * 40}ms"
>
<!-- 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'}
<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>
@ -485,41 +485,41 @@
{:else}
<span class="inline-flex h-[6px] w-[6px] shrink-0 rounded-full bg-[var(--color-text-muted)]"></span>
{/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>
<!-- Template -->
<div class="px-4 py-3">
<span class="text-[13px] text-[var(--color-text-secondary)]">{capsule.template}</span>
<div class="min-w-0 px-5 py-4">
<span class="block truncate text-ui text-[var(--color-text-secondary)]">{capsule.template}</span>
</div>
<!-- CPU -->
<div class="px-4 py-3">
<span class="font-mono text-[13px] text-[var(--color-text-secondary)]">{capsule.vcpus}</span>
<div class="px-5 py-4">
<span class="font-mono text-ui text-[var(--color-text-secondary)]">{capsule.vcpus}</span>
</div>
<!-- Memory -->
<div class="px-4 py-3">
<span class="font-mono text-[13px] text-[var(--color-text-secondary)]">{capsule.memory_mb}MB</span>
<div class="px-5 py-4">
<span class="font-mono text-ui text-[var(--color-text-secondary)]">{capsule.memory_mb}MB</span>
</div>
<!-- Idle Timeout -->
<div class="px-4 py-3">
<span class="font-mono text-[13px] text-[var(--color-text-secondary)]">{capsule.timeout_sec ? `${capsule.timeout_sec}s` : '—'}</span>
<div class="px-5 py-4">
<span class="font-mono text-ui text-[var(--color-text-secondary)]">{capsule.timeout_sec ? `${capsule.timeout_sec}s` : '—'}</span>
</div>
<!-- Started -->
<div class="px-4 py-3">
<span class="text-[13px] text-[var(--color-text-secondary)]" title={capsule.started_at ?? ''}>{formatTime(capsule.started_at)}</span>
<div class="px-5 py-4">
<span class="text-ui text-[var(--color-text-secondary)]" title={capsule.started_at ?? ''}>{formatTime(capsule.started_at)}</span>
{#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}
</div>
<!-- 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}
<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">
<path d="M21 12a9 9 0 1 1-6.219-8.56" />
</svg>
@ -536,7 +536,7 @@
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}
<svg
@ -563,7 +563,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>
<span class="font-mono text-label uppercase tracking-[0.04em] text-[var(--color-text-secondary)]">All systems operational</span>
</div>
</footer>
</div>
@ -580,7 +580,7 @@
{#if openCapsule.status === 'running'}
<button
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">
<rect x="6" y="4" width="4" height="16" rx="1" />
@ -590,7 +590,7 @@
</button>
<button
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">
<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'}
<button
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">
<polygon points="5 3 19 12 5 21 5 3" />
@ -610,7 +610,7 @@
</button>
<button
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">
<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>
<button
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">
<polyline points="3 6 5 6 21 6" />
@ -645,41 +645,41 @@
></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">
<h2 class="font-serif text-[20px] 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>
<h2 class="font-serif text-heading tracking-[-0.02em] text-[var(--color-text-bright)]">Launch Capsule</h2>
<p class="mt-1 text-ui text-[var(--color-text-tertiary)]">Configure and launch a new isolated capsule.</p>
{#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}
</div>
{/if}
<div class="mt-5 space-y-4">
<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
id="create-template"
type="text"
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"
/>
</div>
<div class="grid grid-cols-2 gap-3">
<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
id="create-vcpus"
type="number"
min="1"
max="8"
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>
<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
id="create-memory"
type="number"
@ -687,19 +687,19 @@
max="8192"
step="128"
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>
<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
id="create-timeout"
type="number"
min="0"
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>
@ -708,14 +708,14 @@
<button
onclick={() => { showCreateDialog = false; }}
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
</button>
<button
onclick={handleCreate}
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}
<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 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>
<p class="mt-2 text-[13px] 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.
<h2 class="font-serif text-heading tracking-[-0.02em] text-[var(--color-text-bright)]">Destroy Capsule</h2>
<p class="mt-2 text-ui text-[var(--color-text-tertiary)]">
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>
{#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}
</div>
{/if}
@ -757,14 +757,14 @@
<button
onclick={() => { destroyTarget = null; }}
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
</button>
<button
onclick={handleDestroy}
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}
<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)}
<button
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}
{#if sortKey === key}
@ -799,16 +799,16 @@
{/snippet}
{#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">
<span class="text-[12px] 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="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-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>
Live
</span>
</div>
<div class="mt-1 font-serif text-[36px] 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 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-label text-[var(--color-text-tertiary)]">
<span>{sublabel}</span>
{#if extra}
<span class="text-[var(--color-text-muted)]">|</span>
@ -820,13 +820,13 @@
{#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="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 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">
<span class="font-serif text-[30px] tracking-[-0.04em] text-[var(--color-text-bright)]">{value}</span>
<span class="text-[13px] 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="font-serif text-[2.143rem] tracking-[-0.04em] text-[var(--color-text-bright)]">{value}</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-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>
Live
</span>
@ -836,7 +836,7 @@
<div class="flex overflow-hidden rounded-[var(--radius-button)] border border-[var(--color-border)]">
{#each ['5m', '1H', '6H', '24H', '30D'] as range, i}
<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)]'
: 'text-[var(--color-text-tertiary)] hover:text-[var(--color-text-secondary)]'} {i > 0
? 'border-l border-[var(--color-border)]'
@ -850,11 +850,11 @@
<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">
<span class="font-mono text-[10px] 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-[10px] 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-[10px] text-[var(--color-text-muted)]">0</span>
<span class="font-mono text-badge text-[var(--color-text-muted)]">4</span>
<span class="font-mono text-badge text-[var(--color-text-muted)]">3</span>
<span class="font-mono text-badge text-[var(--color-text-muted)]">2</span>
<span class="font-mono text-badge text-[var(--color-text-muted)]">1</span>
<span class="font-mono text-badge text-[var(--color-text-muted)]">0</span>
</div>
<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">
{#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}
</div>
</div>

View File

@ -2,6 +2,7 @@
import Sidebar from '$lib/components/Sidebar.svelte';
import { onMount } from 'svelte';
import { listKeys, createKey, revokeKey, type APIKey } from '$lib/api/keys';
import { toast } from '$lib/toast.svelte';
let collapsed = $state(
typeof window !== 'undefined'
@ -75,9 +76,13 @@
async function copyKey() {
if (!newKey?.key) return;
await navigator.clipboard.writeText(newKey.key);
copied = true;
setTimeout(() => (copied = false), 2000);
try {
await navigator.clipboard.writeText(newKey.key);
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 {
@ -115,20 +120,20 @@
<div class="flex flex-1 flex-col overflow-hidden">
<main class="flex-1 overflow-y-auto bg-[var(--color-bg-0)]">
<!-- Header -->
<div class="px-7 pt-6">
<div class="px-7 pt-8">
<div class="flex items-center justify-between">
<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
</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.
</p>
</div>
<button
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">
<line x1="12" y1="5" x2="12" y2="19" /><line x1="5" y1="12" x2="19" y2="12" />
@ -137,20 +142,26 @@
</button>
</div>
<div class="mt-5 border-b border-[var(--color-border)]"></div>
<div class="mt-6 border-b border-[var(--color-border)]"></div>
</div>
<!-- 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}
<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)]">
{error}
<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)]">
<span>{error}</span>
<button
onclick={fetchKeys}
class="shrink-0 font-semibold underline-offset-2 hover:underline"
>
Try again
</button>
</div>
{/if}
{#if loading}
<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">
<path d="M21 12a9 9 0 1 1-6.219-8.56" />
</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" />
</svg>
</div>
<p class="font-serif text-[20px] 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="font-serif text-heading tracking-[-0.02em] text-[var(--color-text-bright)]">No API keys yet</p>
<p class="mt-1.5 text-ui text-[var(--color-text-tertiary)]">Create your first key to start making API requests.</p>
<button
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">
<line x1="12" y1="5" x2="12" y2="19" /><line x1="5" y1="12" x2="19" y2="12" />
</svg>
@ -180,11 +191,11 @@
<div class="rounded-[var(--radius-card)] border border-[var(--color-border)] overflow-hidden">
<!-- 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="px-4 py-[11px] text-[11px] 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-4 py-[11px] text-[11px] 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-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)]">Name / Key</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-5 py-3 text-label 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)]">Last Used</div>
<div class="px-5 py-3 text-label font-semibold uppercase tracking-[0.05em] text-[var(--color-text-muted)]"></div>
</div>
{#each keys as key, i (key.id)}
@ -193,37 +204,37 @@
style="animation: fadeUp 0.35s ease both; animation-delay: {i * 40}ms"
>
<!-- Name + prefix -->
<div class="flex flex-col gap-0.5 px-4 py-3">
<span class="text-[13px] 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>
<div class="min-w-0 flex flex-col gap-0.5 px-5 py-4">
<span class="truncate text-ui font-medium text-[var(--color-text-bright)]">{key.name || '—'}</span>
<span class="font-mono text-meta text-[var(--color-text-muted)]">{key.key_prefix}</span>
</div>
<!-- Created by -->
<div class="px-4 py-3">
<span class="text-[13px] text-[var(--color-text-secondary)]">{key.creator_email ?? key.created_by}</span>
<div class="min-w-0 px-5 py-4">
<span class="block truncate text-ui text-[var(--color-text-secondary)]">{key.creator_email ?? key.created_by}</span>
</div>
<!-- Created at -->
<div class="px-4 py-3">
<span class="text-[13px] text-[var(--color-text-secondary)]">{formatDate(key.created_at)}</span>
<div class="px-5 py-4">
<span class="text-ui text-[var(--color-text-secondary)]">{formatDate(key.created_at)}</span>
</div>
<!-- Last used -->
<div class="px-4 py-3">
<div class="px-5 py-4">
{#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)}
</span>
{:else}
<span class="text-[13px] text-[var(--color-text-muted)]">Never</span>
<span class="text-ui text-[var(--color-text-muted)]">Never</span>
{/if}
</div>
<!-- Revoke -->
<div class="flex justify-end px-4 py-3">
<div class="flex justify-end px-5 py-4">
<button
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
</button>
@ -232,20 +243,14 @@
{/each}
</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
</p>
{/if}
</div>
</main>
<!-- Status bar -->
<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>
<footer class="h-px shrink-0 bg-[var(--color-border)]"></footer>
</div>
</div>
@ -260,17 +265,17 @@
></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">
<h2 class="font-serif text-[20px] 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>
<h2 class="font-serif text-heading tracking-[-0.02em] text-[var(--color-text-bright)]">New API Key</h2>
<p class="mt-1 text-ui text-[var(--color-text-tertiary)]">Give your key a name to identify it later.</p>
{#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}
</div>
{/if}
<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
</label>
<input
@ -279,7 +284,7 @@
placeholder="e.g. Production SDK"
bind:value={createName}
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>
@ -287,14 +292,14 @@
<button
onclick={() => { showCreate = false; }}
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
</button>
<button
onclick={handleCreate}
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}
<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" />
</svg>
</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>
<h2 class="font-serif text-[20px] tracking-[-0.02em] text-[var(--color-text-bright)]">{newKey.name || 'API Key'}</h2>
<p class="mt-1 text-[13px] text-[var(--color-text-tertiary)]">
<h2 class="font-serif text-heading tracking-[-0.02em] text-[var(--color-text-bright)]">{newKey.name || 'API Key'}</h2>
<p class="mt-1 text-ui text-[var(--color-text-tertiary)]">
Copy this key now — it won't be shown again.
</p>
<!-- 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="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 ?? ''}
</span>
<button
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
? '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)]'}"
@ -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" />
<line x1="12" y1="9" x2="12" y2="13" /><line x1="12" y1="17" x2="12.01" y2="17" />
</svg>
<p class="text-[12px] leading-relaxed text-[var(--color-amber)]">
Store this key securely. For security reasons, we only show it once and cannot retrieve it later.
<p class="text-meta leading-relaxed text-[var(--color-amber)]">
Treat this like a password — store it in a secrets manager, not a document or chat message.
</p>
</div>
<div class="mt-6 flex justify-end">
<button
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
</button>
@ -399,15 +404,15 @@
></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">
<h2 class="font-serif text-[20px] tracking-[-0.02em] text-[var(--color-text-bright)]">Revoke Key</h2>
<p class="mt-2 text-[13px] text-[var(--color-text-tertiary)]">
<h2 class="font-serif text-heading tracking-[-0.02em] text-[var(--color-text-bright)]">Revoke Key</h2>
<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>?
Any request using it will stop working immediately.
</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}
<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}
</div>
{/if}
@ -416,14 +421,14 @@
<button
onclick={() => { revokeTarget = null; }}
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
</button>
<button
onclick={handleRevoke}
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}
<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">
<main class="flex-1 overflow-y-auto bg-[var(--color-bg-0)]">
<!-- Header -->
<div class="px-7 pt-6">
<div class="px-7 pt-8">
<div class="flex items-start justify-between">
<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
</h1>
<p class="mt-1 text-[13px] text-[var(--color-text-tertiary)]">
Point-in-time captures and base environments for launching capsules.
<p class="mt-2 text-ui text-[var(--color-text-tertiary)]">
Saved capsule states and base images. Launch a new capsule from any template.
</p>
</div>
</div>
@ -188,7 +188,7 @@
<!-- Snapshots tab (active) -->
<button
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-transparent text-[var(--color-text-secondary)] hover:text-[var(--color-text-primary)]'}"
>
@ -203,7 +203,7 @@
<button
disabled
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">
<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" />
</svg>
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
</span>
</button>
@ -220,16 +220,16 @@
<!-- Snapshots tab content -->
{#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}
<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}
</div>
{/if}
{#if loading}
<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">
<path d="M21 12a9 9 0 1 1-6.219-8.56" />
</svg>
@ -243,7 +243,7 @@
{#each ([['all', 'All'], ['snapshot', 'Snapshots'], ['base', 'Images']] as const) as [val, label]}
<button
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)] 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>
{/each}
</div>
<span class="text-[12px] text-[var(--color-text-muted)]">
<span class="text-meta text-[var(--color-text-muted)]">
{filteredSnapshots.length}
{filteredSnapshots.length === 1 ? 'snapshot' : 'snapshots'}
</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" />
</svg>
</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)}
</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)}
</p>
{#if typeFilter === 'all' || typeFilter === 'snapshot'}
<a
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
<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)]">
<!-- 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="px-4 py-[11px] text-[11px] 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-4 py-[11px] text-[11px] 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-4 py-[11px] text-[11px] 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-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-label 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)]">Type</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-5 py-3 text-label 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)]">Size</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-5 py-3 text-right text-label font-semibold uppercase tracking-[0.05em] text-[var(--color-text-muted)]">Actions</div>
</div>
<!-- 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"
>
<!-- Name -->
<div class="px-4 py-3">
<span class="font-mono text-[13px] text-[var(--color-text-bright)]">{snapshot.name}</span>
<div class="min-w-0 px-5 py-4">
<span class="block truncate font-mono text-ui text-[var(--color-text-bright)]">{snapshot.name}</span>
</div>
<!-- Type badge -->
<div class="px-4 py-3">
<div class="px-5 py-4">
{#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>
Snapshot
</span>
{: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>
Image
</span>
@ -326,31 +326,31 @@
</div>
<!-- vCPUs -->
<div class="px-4 py-3">
<div class="px-5 py-4">
{#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}
<span class="text-[13px] text-[var(--color-text-muted)]"></span>
<span class="text-ui text-[var(--color-text-muted)]"></span>
{/if}
</div>
<!-- Memory -->
<div class="px-4 py-3">
<div class="px-5 py-4">
{#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}
<span class="text-[13px] text-[var(--color-text-muted)]"></span>
<span class="text-ui text-[var(--color-text-muted)]"></span>
{/if}
</div>
<!-- Size -->
<div class="px-4 py-3">
<span class="font-mono text-[13px] text-[var(--color-text-muted)]">{formatBytes(snapshot.size_bytes)}</span>
<div class="px-5 py-4">
<span class="font-mono text-ui text-[var(--color-text-muted)]">{formatBytes(snapshot.size_bytes)}</span>
</div>
<!-- Created -->
<div class="px-4 py-3" title={formatDate(snapshot.created_at)}>
<span class="text-[13px] text-[var(--color-text-secondary)]">{timeAgo(snapshot.created_at)}</span>
<div class="px-5 py-4" title={formatDate(snapshot.created_at)}>
<span class="text-ui text-[var(--color-text-secondary)]">{timeAgo(snapshot.created_at)}</span>
</div>
<!-- Actions: split button -->
@ -359,7 +359,7 @@
<!-- Launch part -->
<button
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
</button>
@ -392,7 +392,7 @@
{/each}
</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'}
{typeFilter !== 'all' ? `· filtered` : '· total'}
</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">
<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>
<span class="font-mono text-label uppercase tracking-[0.04em] text-[var(--color-text-secondary)]">All systems operational</span>
</div>
</footer>
</div>
@ -427,7 +427,7 @@
openDropdownName = 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">
<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"
style="animation: fadeUp 0.2s ease both"
>
<h2 class="font-serif text-[20px] tracking-[-0.02em] text-[var(--color-text-bright)]">Delete Snapshot</h2>
<p class="mt-2 text-[13px] text-[var(--color-text-tertiary)]">
<h2 class="font-serif text-heading tracking-[-0.02em] text-[var(--color-text-bright)]">Delete Snapshot</h2>
<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>?
This action cannot be undone.
</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" />
<line x1="12" y1="9" x2="12" y2="13" /><line x1="12" y1="17" x2="12.01" y2="17" />
</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.
</p>
</div>
{/if}
{#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}
</div>
{/if}
@ -480,14 +480,14 @@
<button
onclick={() => (deleteTarget = null)}
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
</button>
<button
onclick={handleDelete}
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}
<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"
style="animation: fadeUp 0.2s ease both"
>
<h2 class="font-serif text-[20px] tracking-[-0.02em] text-[var(--color-text-bright)]">Launch Capsule</h2>
<p class="mt-1 text-[13px] text-[var(--color-text-tertiary)]">
<h2 class="font-serif text-heading tracking-[-0.02em] text-[var(--color-text-bright)]">Launch Capsule</h2>
<p class="mt-1 text-ui text-[var(--color-text-tertiary)]">
Start a new capsule from this template.
</p>
{#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}
</div>
{/if}
<!-- Template name (readonly) -->
<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
</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">
@ -539,8 +539,8 @@
{:else}
<span class="inline-block h-[6px] w-[6px] shrink-0 rounded-full bg-[var(--color-blue)]"></span>
{/if}
<span class="flex-1 font-mono text-[13px] text-[var(--color-text-bright)]">{launchTarget.name}</span>
<span class="text-[11px] text-[var(--color-text-muted)]">
<span class="flex-1 font-mono text-ui text-[var(--color-text-bright)]">{launchTarget.name}</span>
<span class="text-label text-[var(--color-text-muted)]">
{launchTarget.type === 'snapshot' ? 'Snapshot' : 'Image'}
</span>
</div>
@ -549,11 +549,11 @@
<!-- vCPUs + Memory -->
<div class="mt-4 grid grid-cols-2 gap-3">
<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
</label>
{#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}
</div>
{:else}
@ -563,17 +563,17 @@
min="1"
max="32"
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}
</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)
</label>
{#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}
</div>
{:else}
@ -583,7 +583,7 @@
min="128"
step="128"
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}
</div>
@ -591,13 +591,13 @@
<!-- Timeout -->
<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
id="launch-timeout"
type="number"
min="0"
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>
@ -605,14 +605,14 @@
<button
onclick={() => (launchTarget = null)}
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
</button>
<button
onclick={handleLaunch}
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}
<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">
import { goto } from '$app/navigation';
import { onMount } from 'svelte';
import { page } from '$app/stores';
import { auth } from '$lib/auth.svelte';
import { apiLogin, apiSignup } from '$lib/api/auth';
import {
@ -17,6 +19,12 @@
let error = $state('');
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
let glowX = $state(50);
let glowY = $state(50);
@ -107,7 +115,7 @@
<!-- Mouse-reactive radial glow -->
<div
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"
></div>
@ -118,7 +126,7 @@
>
<img src="/logo.svg" alt="Wrenn" class="h-20 w-20 rounded-[var(--radius-card)]" />
<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
</span>
@ -126,19 +134,19 @@
<!-- Tagline below logo -->
<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"
>
<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>
</div>
<!-- Sub-tagline -->
<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"
>
Run Anything. Worry about Nothing.
@ -156,7 +164,7 @@
>
<img src="/logo.svg" alt="Wrenn" class="h-12 w-12 rounded-[var(--radius-card)]" />
<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
</span>
@ -166,11 +174,11 @@
<!-- Header -->
<div class="mb-8">
<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}
</h2>
<p class="mt-2 text-[14px] text-[var(--color-text-secondary)]">
<p class="mt-2 text-body text-[var(--color-text-secondary)]">
{subtitle}
</p>
</div>
@ -178,7 +186,7 @@
<!-- GitHub OAuth -->
<a
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} />
Continue with GitHub
@ -188,7 +196,7 @@
<div class="my-6 flex items-center gap-3">
<div class="h-px flex-1 bg-[var(--color-border)]"></div>
<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
>
<div class="h-px flex-1 bg-[var(--color-border)]"></div>
@ -207,7 +215,7 @@
bind:value={email}
placeholder="Email address"
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>
@ -222,7 +230,7 @@
bind:value={password}
placeholder="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
type="button"
@ -242,7 +250,7 @@
<div class="flex justify-end">
<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?
</button>
@ -250,13 +258,13 @@
{/if}
{#if error}
<p class="text-[13px] text-[var(--color-red)]">{error}</p>
<p class="text-ui text-[var(--color-red)]">{error}</p>
{/if}
<button
type="submit"
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}
<span class="inline-flex items-center gap-2">
@ -272,7 +280,7 @@
</form>
<!-- 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}
<button
type="button"