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 ## AI
.claude/ .claude/
e2b/ e2b/
.impeccable.md
## Builds ## Builds
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/` - Sandbox clones: `/var/lib/wrenn/sandboxes/`
- Firecracker: `/usr/local/bin/firecracker` (e2b's fork of firecracker) - Firecracker: `/usr/local/bin/firecracker` (e2b's fork of firecracker)
## Web UI Styling ## Design Context
### Identity ### 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.
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.
### Brand Personality
--- **Precise. Warm. Uncompromising.**
### Color Palette (Dark Mode) 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.
**Background scale (6 steps, near-black-green):** 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.
`#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)
### Aesthetic Direction
**Text hierarchy (5 levels):** **Dark-first, industrial-warm, data-forward.**
- Bright `#eae7e2` — page titles, metric values, active states
- Primary `#d0cdc6` — body text, nav labels, readable content 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.
- Secondary `#9b9790` — supporting text, inactive nav, descriptions
- Tertiary `#6b6862` — labels, section headers, timestamps **Anti-references:**
- Muted `#454340` — ghost text, disabled states, grid labels - **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
**Sage green brand accent (3 tiers + 2 glows):**
- Solid `#5e8c58` — primary accent, buttons, borders, active indicators **References that capture the right spirit:**
- Mid `#89a785` — badges, chart lines, secondary accent - The precision of a well-calibrated instrument
- Bright `#a4c89f` — active nav text, live counts, chart dots - Editorial typography from technical publications
- Glow `rgba(94,140,88,0.07)` — active nav backgrounds, subtle highlights - The quiet confidence of tools that don't need to explain themselves
- Glow Mid `rgba(94,140,88,0.14)` — live badges, status badge backgrounds
### Type System
**Borders (2 levels):** Four fonts with strict roles — this is the design system's strongest personality trait and must be respected:
- Default `#1f2321` — card edges, dividers, sidebar borders
- Mid `#2a2f2c` — hover states, interactive borders, stronger separation | Font | Role | When to use |
|------|------|-------------|
**Semantic status colors:** | **Manrope** (variable, sans) | UI workhorse | All body copy, nav, labels, buttons, form text |
- Amber `#d4a73c` — warning, building, countdown timers | **Instrument Serif** | Display / editorial | Page titles (h1), dialog headings, metric values, hero moments |
- Red `#cf8172` — error, failed, destructive actions | **JetBrains Mono** (variable) | Data / code | IDs, timestamps, key prefixes, file paths, terminal output, metrics |
- Blue `#5a9fd4` — info, stopped (use sparingly) | **Alice** | Brand wordmark | "Wrenn" in sidebar and login only — nowhere else |
**Light mode:** (TBD — follow same warm-tinted approach. Background scale from `#f8f6f1``#dedbd5`. Text hierarchy inverts. Accent stays `#5e8c58` for solid.) Instrument Serif at scale creates the signature editorial moments. Mono provides the precision signal for technical data. Never swap these roles.
--- ### Color System
```
### Typography Backgrounds: bg-0 (#0a0c0b) through bg-5 (#2a302d) — 6 steps
Text: bright > primary > secondary > tertiary > muted — 5 levels
Four fonts, each with a clear role: Accent: accent (#5e8c58) / accent-mid / accent-bright / glow / glow-mid
Status: amber (#d4a73c) / red (#cf8172) / blue (#5a9fd4)
| Font | Role | Weights | Where | ```
|------|------|---------|-------|
| **Manrope** (variable) | Body, UI | 400700 | All body text, nav labels, buttons, descriptions, section headers | 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.
| **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 | ### Upcoming Surfaces (design must accommodate)
| **Alice** | Brand wordmark | 400 | Sidebar wordmark only — never used elsewhere | - **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.
**Sizing:** - **SDK / docs embedding**: code samples, quickstart flows inline in dashboard. Code blocks must feel premium, not afterthought.
- Base body: `14px` - **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.
- Page title (h1): `24px` serif, `letter-spacing: -0.02em`
- Card metric values: `36px` serif, `letter-spacing: -0.04em` ### Design Principles
- Chart inline metric: `30px` serif, `letter-spacing: -0.04em`
- Nav items: `13px` body, weight 500 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.
- Section/group labels: `11px` body, uppercase, `letter-spacing: 0.06em`, weight 600
- Chart section labels: `12px` body, uppercase, `letter-spacing: 0.05em`, weight 600 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.
- Stat cell labels: `11px` body, uppercase, `letter-spacing: 0.05em`, weight 600
- Badge text: `10px`, uppercase, `letter-spacing: 0.04em`, weight 600 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.
- Status bar / footer links: `1112px` mono
- Table headers: `11px` body, uppercase, `letter-spacing: 0.05em`, weight 600, color muted 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.
- Table body cells: `13px`
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.
**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.

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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