diff --git a/docs/superpowers/specs/2026-03-27-ui-de-slop-design.md b/docs/superpowers/specs/2026-03-27-ui-de-slop-design.md new file mode 100644 index 0000000..be7e0d1 --- /dev/null +++ b/docs/superpowers/specs/2026-03-27-ui-de-slop-design.md @@ -0,0 +1,126 @@ +# UI De-Slop Design: agent-render + +**Date:** 2026-03-27 +**Direction:** Linear/Raycast clean — sharp, minimal, developer-tool aesthetic +**Scope:** Full visual overhaul — CSS tokens + component restructuring + layout changes + +## Context + +The agent-render UI has accumulated AI-generated visual patterns ("slop") that make it look like a template rather than a crafted tool. The warm color palette and component structure are solid, but the visual layer is overdecorated with glassmorphism, oversized radii, dramatic shadows, stacked gradients, and generic hero patterns. This design strips those patterns to achieve a Linear/Raycast-quality developer tool aesthetic while preserving the warm personality. + +## Design Decisions + +| Decision | Choice | Rationale | +|----------|--------|-----------| +| Aesthetic | Linear/Raycast clean | Developer tool, not marketing site | +| Colors | Keep warm, desaturate ~30% | Preserve personality, remove trend signal | +| Typography | Fraunces for wordmark only | Serif+sans combo is an AI slop tell | +| Approach | Full visual overhaul | CSS tokens + component structure + layout | + +## Token Changes + +### Border Radius +| Token | Current | Proposed | +|-------|---------|----------| +| `--radius-xl` | 28px | 10px | +| `--radius-lg` | 20px | 8px | +| `--radius-md` | 14px | 6px | +| `--radius-sm` | 999px (pill) | 4px | + +### Shadows +| Token | Current | Proposed | +|-------|---------|----------| +| `--shadow-lg` | `0 26px 72px rgba(21,27,39,0.11)` | `0 1px 3px rgba(0,0,0,0.08), 0 0 0 1px rgba(0,0,0,0.04)` | +| `--shadow-md` | `0 14px 34px rgba(21,27,39,0.075)` | `0 1px 2px rgba(0,0,0,0.06)` | + +### Colors (Light) +| Token | Current | Proposed | +|-------|---------|----------| +| `--page-bg` | `#f2ecdf` | `#f7f5f2` | +| `--surface` | `rgba(255,249,241,0.84)` | `#ffffff` | +| `--surface-strong` | `rgba(255,252,247,0.96)` | `#fafaf9` | +| `--accent` | `#ba5937` | `#9c6b52` | +| `--accent-secondary` | `#0e6c77` | `#5c8a8e` | + +### Colors (Dark) +| Token | Current | Proposed | +|-------|---------|----------| +| `--page-bg` | `#0a0f15` | `#111113` | +| `--surface` | `rgba(13,19,27,0.92)` | `#1a1a1c` | +| `--accent` | `#de7f58` | `#c09478` | +| `--accent-secondary` | `#72c4cf` | `#8ab5b9` | + +### Motion +| Property | Current | Proposed | +|----------|---------|----------| +| `--duration-base` | 280ms | 150ms | +| Page-load animation | 720ms fade-up + staggered delays | Removed entirely | +| Hover effects | translateY(-2px) + shadow + color | Background color change only | + +## Removals + +1. **Glassmorphism**: All `backdrop-filter: blur(14px)` → solid backgrounds +2. **Shine overlays**: All `.panel::after` gradient gloss → deleted +3. **Stacked body gradients**: 3-layer radial gradients → solid `var(--page-bg)` +4. **Grid overlay**: Decorative `body::before` grid → deleted +5. **Hero icon pills**: ShieldCheck/Sparkles/FolderKanban badge row → deleted +6. **Gradient card backgrounds**: On hero-link-card, sample-link, creator-result → solid `var(--surface-elevated)` +7. **Gradient toolbar/code-frame headers** → solid `var(--surface-code-raised)` +8. **`fade-up` animation class** and `getAnimationStyle` helper → deleted +9. **`translateY` hover transforms** on all interactive elements → deleted +10. **Hover shadow additions** → deleted + +## Typography + +- **Wordmark ("agent-render")**: Fraunces 600 — keep +- **All headings (h1-h6)**: IBM Plex Sans 600 — change from Fraunces +- **Body text**: IBM Plex Sans 400 — keep +- **Code**: IBM Plex Mono — keep +- **Fraunces font import**: Reduce from weights [500, 600, 700] to [600] only + +## Structural Changes + +### Home Page +- Hero heading sizes reduced (~30% smaller, developer-tool scale) +- Hero subtitle reduced from `sm:text-lg` to `sm:text-base` +- Three icon-pill feature badges removed entirely +- Sample link cards: gradient backgrounds → solid borders +- Inspector/metrics section: keep, but simplified styling + +### Viewer +- Toolbar: reduced min-height (2.7rem → 2.25rem) and padding +- Viewer header: tightened padding +- Content area: maximized by reducing chrome + +## Files Modified + +| File | Scope | +|------|-------| +| `src/app/globals.css` | ~60% of changes — tokens, panels, hovers, animations, gradients | +| `src/components/viewer-shell.tsx` | Hero restructuring, fade-up removal, font-display removal, icon cleanup | +| `src/components/home/link-creator.tsx` | fade-up removal, font-display removal | +| `src/components/theme-toggle.tsx` | Hover translateY removal | +| `src/app/layout.tsx` | Fraunces weight reduction | + +## What's Preserved + +- CodeMirror renderer styling (functional) +- Markdown article body typography (reading experience) +- Diff renderer layout (functional) +- CSV table styling (functional) +- JSON tree styling (functional) +- Syntax highlight colors (functional) +- Responsive breakpoints (correct) +- Print styles (correct) +- Accessibility: focus-visible outlines, semantic HTML, contrast ratios +- Dark mode support (all changes have corresponding dark overrides) + +## Verification + +1. `npm run dev` — visual inspection of home page, all 5 artifact types, dark mode +2. `npm run typecheck` — no TS errors from removed imports/props +3. `npm run test` — unit tests pass (no visual changes affect logic) +4. `npx playwright test tests/e2e/visual.spec.ts --update-snapshots` — regenerate all snapshots +5. `npx playwright test` — visual regression passes with new snapshots +6. Manual check: light mode, dark mode, mobile viewport (360px), tablet (768px) +7. Contrast check: verify `--accent` on `--page-bg` meets WCAG AA for large text diff --git a/src/app/globals.css b/src/app/globals.css index 06a7d20..1212700 100644 --- a/src/app/globals.css +++ b/src/app/globals.css @@ -2,13 +2,14 @@ @import "@git-diff-view/react/styles/diff-view-pure.css"; :root { + /* Original warm palette — restored */ --page-bg: #f2ecdf; --page-bg-secondary: rgba(14, 108, 119, 0.08); --page-bg-accent: rgba(186, 89, 55, 0.1); - --surface: rgba(255, 249, 241, 0.84); - --surface-strong: rgba(255, 252, 247, 0.96); + --surface: #f8f4ea; + --surface-strong: #f5f0e4; --surface-muted: rgba(19, 28, 41, 0.035); - --surface-elevated: rgba(255, 255, 255, 0.82); + --surface-elevated: #faf7ef; --border: rgba(23, 32, 51, 0.11); --border-strong: rgba(23, 32, 51, 0.18); --text-primary: #172033; @@ -20,46 +21,71 @@ --success: #287357; --warning: #ad6b1f; --danger: #a33b2b; - --shadow-lg: 0 26px 72px rgba(21, 27, 39, 0.11); - --shadow-md: 0 14px 34px rgba(21, 27, 39, 0.075); - --radius-xl: 28px; - --radius-lg: 20px; - --radius-md: 14px; - --radius-sm: 999px; - --duration-base: 280ms; - --surface-code: rgba(18, 27, 40, 0.95); - --surface-code-raised: rgba(28, 38, 54, 0.82); - --surface-code-text: #eff3f7; + --shadow-lg: 0 2px 8px rgba(21, 27, 39, 0.07); + --shadow-md: 0 1px 3px rgba(21, 27, 39, 0.05); + --radius-xl: 12px; + --radius-lg: 8px; + --radius-md: 6px; + --radius-sm: 4px; + --duration-base: 150ms; + --surface-code: #ede7da; + --surface-code-raised: #e5dfd2; + --surface-code-text: #2d2418; + --code-gutter-text: rgba(45, 36, 24, 0.4); + --code-gutter-border: rgba(45, 36, 24, 0.08); + --code-active-line: rgba(45, 36, 24, 0.04); + --code-selection: rgba(14, 108, 119, 0.14); + --code-string: #2a7e4f; + --code-number: #b44d3e; + --code-keyword: #8b5fcf; + --code-boolean: #0e6c77; + --code-null: #6b5fb8; + --code-property: #a06020; + --code-operator: #7a5030; + --code-comment: #8a7d72; --print-paper: #ffffff; --print-ink: #172033; --print-muted: #566073; --print-border: rgba(23, 32, 51, 0.14); } +/* Cool blue-charcoal dark mode — warm accents on cool base */ .dark { - --page-bg: #0a0f15; - --page-bg-secondary: rgba(82, 155, 168, 0.05); - --page-bg-accent: rgba(201, 111, 76, 0.04); - --surface: rgba(13, 19, 27, 0.92); - --surface-strong: rgba(18, 25, 35, 0.98); - --surface-muted: rgba(135, 154, 180, 0.05); - --surface-elevated: rgba(23, 31, 43, 0.96); - --border: rgba(174, 191, 214, 0.13); - --border-strong: rgba(201, 214, 233, 0.2); - --text-primary: #eef2f7; - --text-muted: #a7b2c1; - --text-soft: #7f8b9b; + --page-bg: #0c1015; + --page-bg-secondary: rgba(14, 108, 119, 0.05); + --page-bg-accent: rgba(186, 89, 55, 0.03); + --surface: #131920; + --surface-strong: #0f141a; + --surface-muted: rgba(99, 150, 200, 0.06); + --surface-elevated: #1a2230; + --border: rgba(130, 160, 200, 0.1); + --border-strong: rgba(130, 160, 200, 0.18); + --text-primary: #e2e8f0; + --text-muted: #8b99ad; + --text-soft: #5e6b7e; --accent: #de7f58; --accent-strong: #f0a27f; - --accent-secondary: #72c4cf; - --success: #78c5a8; - --warning: #e6b263; - --danger: #ef9c90; - --shadow-lg: 0 28px 72px rgba(0, 0, 0, 0.5); - --shadow-md: 0 14px 34px rgba(0, 0, 0, 0.34); - --surface-code: rgba(8, 13, 20, 0.98); - --surface-code-raised: rgba(15, 21, 30, 0.95); - --surface-code-text: #eef3f7; + --accent-secondary: #5cc7d4; + --success: #6dbfa0; + --warning: #d9a65a; + --danger: #d47a6e; + --shadow-lg: 0 2px 8px rgba(0, 0, 0, 0.4); + --shadow-md: 0 1px 3px rgba(0, 0, 0, 0.3); + --surface-code: #0a0f14; + --surface-code-raised: #111820; + --surface-code-text: #e2e8f0; + --code-gutter-text: rgba(226, 232, 240, 0.4); + --code-gutter-border: rgba(226, 232, 240, 0.06); + --code-active-line: rgba(255, 255, 255, 0.04); + --code-selection: rgba(92, 199, 212, 0.18); + --code-string: #7fd7a4; + --code-number: #f29c8f; + --code-keyword: #efb360; + --code-boolean: #69d1dd; + --code-null: #9eb3ff; + --code-property: #efb360; + --code-operator: #f08d5e; + --code-comment: #5e6b7e; } * { @@ -76,27 +102,9 @@ body { color: var(--text-primary); font-family: var(--font-sans), sans-serif; background: - radial-gradient(circle at top left, var(--page-bg-secondary), transparent 36%), - radial-gradient(circle at top right, var(--page-bg-accent), transparent 28%), - linear-gradient(180deg, color-mix(in srgb, var(--page-bg) 92%, white 8%), var(--page-bg)); -} - -.dark body { - background: - linear-gradient(180deg, color-mix(in srgb, var(--page-bg) 94%, #0e141d 6%), var(--page-bg)); -} - -body::before { - content: ""; - position: fixed; - inset: 0; - pointer-events: none; - background: - linear-gradient(90deg, transparent 0, transparent calc(100% - 1px), color-mix(in srgb, var(--border) 80%, transparent) calc(100% - 1px), transparent 100%), - linear-gradient(0deg, transparent 0, transparent calc(100% - 1px), color-mix(in srgb, var(--border) 60%, transparent) calc(100% - 1px), transparent 100%); - background-size: 72px 72px; - mask-image: linear-gradient(180deg, rgba(0, 0, 0, 0.65), transparent 85%); - opacity: 0.35; + radial-gradient(ellipse at top left, var(--page-bg-secondary), transparent 50%), + radial-gradient(ellipse at bottom right, var(--page-bg-accent), transparent 50%), + var(--page-bg); } a { @@ -130,38 +138,15 @@ select { border-radius: var(--radius-xl); background: var(--surface); box-shadow: var(--shadow-md); - backdrop-filter: blur(14px); overflow: hidden; } -.panel::after { - content: ""; - position: absolute; - inset: 0; - border-radius: inherit; - pointer-events: none; - background: linear-gradient(180deg, rgba(255, 255, 255, 0.16), transparent 32%); - opacity: 0.68; -} - -.dark .panel::after { - background: linear-gradient(180deg, rgba(255, 255, 255, 0.04), transparent 30%); - opacity: 0.48; -} - .panel-strong { background: var(--surface-strong); box-shadow: var(--shadow-lg); } .panel-hero { - background: - linear-gradient(135deg, color-mix(in srgb, var(--surface-strong) 82%, transparent), color-mix(in srgb, var(--surface) 96%, transparent)), - radial-gradient(circle at 85% 15%, color-mix(in srgb, var(--accent-secondary) 18%, transparent), transparent 32%), - radial-gradient(circle at 20% 90%, color-mix(in srgb, var(--accent) 12%, transparent), transparent 28%); -} - -.dark .panel-hero { background: var(--surface-strong); } @@ -177,23 +162,21 @@ select { .mono-pill { display: inline-flex; align-items: center; - gap: 0.45rem; - padding: 0.46rem 0.78rem; + gap: 0.35rem; + padding: 0.25rem 0.5rem; border: 1px solid var(--border); border-radius: var(--radius-sm); - background: color-mix(in srgb, var(--surface-elevated) 86%, transparent); + background: var(--surface-elevated); color: var(--text-muted); font-family: var(--font-mono), monospace; font-size: 0.75rem; - font-weight: 600; + font-weight: 500; letter-spacing: 0.01em; } .shell-pill { - min-height: 2.25rem; - border-color: var(--border-strong); + min-height: 2rem; color: var(--text-primary); - background: color-mix(in srgb, var(--surface-strong) 88%, transparent); } .metric-grid { @@ -203,17 +186,64 @@ select { } .metric-card { - border: 1px solid var(--border); - border-radius: var(--radius-lg); - background: color-mix(in srgb, var(--surface-elevated) 90%, transparent); + border-radius: var(--radius-md); + background: var(--surface-muted); padding: 0.9rem 0.95rem; } .empty-state-layout { display: grid; + gap: 2.5rem; +} + +.home-intro { + max-width: 42rem; +} + +.fragment-error { + padding: 1rem 1.25rem; + border-left: 3px solid var(--danger); + border-radius: 0 var(--radius-md) var(--radius-md) 0; + background: color-mix(in srgb, var(--danger) 5%, var(--surface-muted)); +} + +.fragment-error-head { + display: flex; + align-items: center; + justify-content: space-between; gap: 1rem; } +.home-lower { + display: grid; + gap: 2.5rem; +} + +.home-meta { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(120px, 1fr)); + gap: 0.25rem 1.5rem; +} + +.home-meta div { + display: flex; + justify-content: space-between; + gap: 1rem; + padding: 0.5rem 0; + border-bottom: 1px solid var(--border); +} + +.home-meta dt { + color: var(--text-soft); + font-size: 0.82rem; +} + +.home-meta dd { + font-family: var(--font-mono), monospace; + font-size: 0.82rem; + font-weight: 500; +} + .hero-metric-stack, .hero-callout-row, .empty-state-lower-grid, @@ -230,28 +260,17 @@ select { gap: 0.6rem; min-height: 100%; padding: 1rem 1.05rem; - border: 1px solid var(--border); - border-radius: var(--radius-lg); - background: - linear-gradient(180deg, color-mix(in srgb, var(--surface-elevated) 90%, transparent), color-mix(in srgb, var(--surface) 96%, transparent)), - radial-gradient(circle at top right, color-mix(in srgb, var(--accent-secondary) 12%, transparent), transparent 48%); - transition: - transform var(--duration-base) ease, - border-color var(--duration-base) ease, - background-color var(--duration-base) ease, - box-shadow var(--duration-base) ease; + border-radius: var(--radius-md); + background: var(--surface-muted); + transition: background-color var(--duration-base) ease; } .hero-link-card:hover { - transform: translateY(-2px); - border-color: color-mix(in srgb, var(--accent-secondary) 44%, var(--border)); - box-shadow: 0 18px 36px color-mix(in srgb, var(--accent-secondary) 10%, transparent); + background: color-mix(in srgb, var(--accent-secondary) 8%, var(--surface-muted)); } .hero-link-card.is-static:hover { - transform: none; - border-color: var(--border); - box-shadow: none; + background: var(--surface-muted); } .hero-link-card:focus-visible { @@ -312,7 +331,7 @@ select { .budget-fill { height: 100%; border-radius: inherit; - background: linear-gradient(90deg, var(--accent-secondary), var(--accent)); + background: var(--accent-secondary); transition: width var(--duration-base) ease; } @@ -322,26 +341,47 @@ select { justify-content: space-between; gap: 1rem; min-height: 100%; - border: 1px solid var(--border); - border-radius: var(--radius-lg); + border-radius: var(--radius-md); padding: 1rem; - background: color-mix(in srgb, var(--surface-elevated) 88%, transparent); + background: var(--surface-muted); transition: - transform var(--duration-base) ease, border-color var(--duration-base) ease, - background-color var(--duration-base) ease, - box-shadow var(--duration-base) ease; + background-color var(--duration-base) ease; } .sample-link:hover { - transform: translateY(-2px); - border-color: color-mix(in srgb, var(--accent) 42%, var(--border)); - box-shadow: 0 18px 36px color-mix(in srgb, var(--accent) 10%, transparent); + background: color-mix(in srgb, var(--accent) 8%, var(--surface-muted)); } .sample-link.is-active { - border-color: color-mix(in srgb, var(--accent) 64%, var(--border)); - background: color-mix(in srgb, var(--accent) 8%, var(--surface-strong)); + background: color-mix(in srgb, var(--accent) 12%, var(--surface-muted)); +} + +.sample-list { + display: flex; + flex-direction: column; +} + +.sample-row { + display: flex; + align-items: center; + gap: 0.75rem; + padding: 0.6rem 0; + border-bottom: 1px solid var(--border); + color: var(--text-primary); + transition: color var(--duration-base) ease; +} + +.sample-row:last-child { + border-bottom: none; +} + +.sample-row:hover { + color: var(--accent); +} + +.sample-row.is-active { + color: var(--accent-secondary); } .home-generator-grid, @@ -373,24 +413,18 @@ select { align-items: center; gap: 0.7rem; padding: 0.82rem 0.88rem; - border: 1px solid var(--border); - border-radius: var(--radius-lg); - background: color-mix(in srgb, var(--surface-elevated) 90%, transparent); + border-radius: var(--radius-md); + background: var(--surface-muted); color: var(--text-primary); - transition: - border-color var(--duration-base) ease, - background-color var(--duration-base) ease, - box-shadow var(--duration-base) ease; + transition: background-color var(--duration-base) ease; } .creator-kind-card:hover { - border-color: color-mix(in srgb, var(--accent-secondary) 42%, var(--border)); - box-shadow: 0 14px 26px color-mix(in srgb, var(--accent-secondary) 8%, transparent); + background: color-mix(in srgb, var(--accent-secondary) 8%, var(--surface-muted)); } .creator-kind-card.is-active { - border-color: color-mix(in srgb, var(--accent-secondary) 58%, var(--border)); - background: color-mix(in srgb, var(--accent-secondary) 10%, var(--surface-strong)); + background: color-mix(in srgb, var(--accent-secondary) 14%, var(--surface-muted)); } .creator-kind-card:focus-visible, @@ -501,11 +535,8 @@ select { display: grid; gap: 0.85rem; height: 100%; - border: 1px solid var(--border); - border-radius: calc(var(--radius-xl) - 2px); - background: - linear-gradient(180deg, color-mix(in srgb, var(--surface-elevated) 90%, transparent), color-mix(in srgb, var(--surface-strong) 98%, transparent)), - radial-gradient(circle at top right, color-mix(in srgb, var(--accent-secondary) 12%, transparent), transparent 42%); + border-radius: var(--radius-lg); + background: var(--surface-muted); padding: 1rem; } @@ -521,9 +552,8 @@ select { .creator-result-note, .creator-empty-state, .creator-error-state { - border: 1px solid var(--border); - border-radius: var(--radius-lg); - background: color-mix(in srgb, var(--surface-strong) 92%, transparent); + border-radius: var(--radius-md); + background: var(--surface-muted); padding: 0.95rem 1rem; } @@ -597,22 +627,15 @@ select { gap: 0.85rem; flex: 0 0 min(18rem, calc(100vw - 5rem)); min-height: 4.5rem; - border: 1px solid var(--border); - border-radius: var(--radius-lg); + border-radius: var(--radius-md); padding: 0.85rem 0.95rem; - background: color-mix(in srgb, var(--surface-elevated) 88%, transparent); + background: var(--surface-muted); scroll-snap-align: start; - transition: - transform var(--duration-base) ease, - border-color var(--duration-base) ease, - background-color var(--duration-base) ease, - box-shadow var(--duration-base) ease; + transition: background-color var(--duration-base) ease; } .artifact-switcher:hover { - transform: translateY(-1px); - border-color: color-mix(in srgb, var(--accent-secondary) 46%, var(--border)); - box-shadow: 0 14px 30px color-mix(in srgb, var(--accent-secondary) 14%, transparent); + background: color-mix(in srgb, var(--accent-secondary) 8%, var(--surface-muted)); } .artifact-switcher-content { @@ -620,9 +643,7 @@ select { } .artifact-switcher.is-active { - border-color: color-mix(in srgb, var(--accent-secondary) 56%, var(--border)); - background: color-mix(in srgb, var(--accent-secondary) 10%, var(--surface-strong)); - box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--accent-secondary) 18%, transparent); + background: color-mix(in srgb, var(--accent-secondary) 14%, var(--surface-muted)); } .artifact-switcher:focus-visible { @@ -648,17 +669,11 @@ select { } .viewer-frame { - border: 1px solid var(--border); - border-radius: var(--radius-xl); - background: color-mix(in srgb, var(--surface-strong) 97%, transparent); + border-radius: var(--radius-lg); + background: var(--surface-muted); overflow: hidden; } -.dark .viewer-frame { - background: color-mix(in srgb, var(--surface-strong) 96%, #0b1016 4%); - box-shadow: inset 0 0 0 1px rgba(255, 255, 255, 0.02); -} - .viewer-frame-primary { min-height: 0; } @@ -684,7 +699,7 @@ select { align-items: center; justify-content: space-between; gap: 1rem; - padding: 1rem 1.2rem; + padding: 0.65rem 1rem; border-bottom: 1px solid var(--border); } @@ -738,27 +753,24 @@ select { align-items: center; justify-content: center; gap: 0.45rem; - min-height: 2.7rem; - padding: 0.58rem 0.82rem; + min-height: 2.25rem; + padding: 0.4rem 0.7rem; border: 1px solid var(--border); border-radius: var(--radius-sm); - background: color-mix(in srgb, var(--surface-elevated) 88%, transparent); + background: var(--surface-elevated); color: var(--text-primary); font-family: var(--font-mono), monospace; font-size: 0.74rem; font-weight: 600; letter-spacing: 0.02em; transition: - transform var(--duration-base) ease, border-color var(--duration-base) ease, - background-color var(--duration-base) ease, - box-shadow var(--duration-base) ease; + background-color var(--duration-base) ease; } .artifact-action:hover { - transform: translateY(-1px); border-color: color-mix(in srgb, var(--accent-secondary) 46%, var(--border)); - box-shadow: 0 14px 30px color-mix(in srgb, var(--accent-secondary) 14%, transparent); + background: color-mix(in srgb, var(--accent-secondary) 6%, var(--surface-elevated)); } .artifact-action:focus-visible { @@ -804,10 +816,10 @@ select { .markdown-article h6 { margin-top: 1.5em; color: var(--text-primary); - font-family: var(--font-display), serif; + font-family: var(--font-sans), sans-serif; font-weight: 600; - line-height: 1.08; - letter-spacing: -0.04em; + line-height: 1.15; + letter-spacing: -0.02em; } .markdown-article h1 { @@ -872,7 +884,7 @@ select { padding: 1rem 1.2rem; border-left: 3px solid var(--accent-secondary); border-radius: 0 var(--radius-lg) var(--radius-lg) 0; - background: linear-gradient(135deg, color-mix(in srgb, var(--accent-secondary) 9%, transparent), color-mix(in srgb, var(--surface-strong) 92%, transparent)); + background: color-mix(in srgb, var(--accent-secondary) 6%, var(--surface-strong)); color: var(--text-primary); } @@ -894,7 +906,7 @@ select { overflow: hidden; border: 1px solid color-mix(in srgb, var(--border-strong) 88%, transparent); border-radius: var(--radius-lg); - background: linear-gradient(180deg, color-mix(in srgb, var(--surface-code-raised) 84%, transparent), color-mix(in srgb, var(--surface-code) 100%, transparent)); + background: var(--surface-code); box-shadow: var(--shadow-md); } @@ -905,7 +917,7 @@ select { gap: 0.75rem; padding: 0.8rem 1rem; border-bottom: 1px solid color-mix(in srgb, var(--border) 85%, transparent); - background: linear-gradient(90deg, color-mix(in srgb, var(--accent-secondary) 14%, transparent), transparent 58%); + background: var(--surface-code-raised); } .markdown-code-chip, @@ -955,9 +967,7 @@ select { gap: 0.75rem; padding: 0.9rem 1rem; border-bottom: 1px solid rgba(239, 243, 247, 0.08); - background: - linear-gradient(180deg, rgba(255, 255, 255, 0.03), transparent), - var(--surface-code-raised); + background: var(--surface-code-raised); } .code-renderer-meta { @@ -1187,19 +1197,19 @@ select { } .json-string { - color: #7fd7a4; + color: var(--code-string); } .json-number { - color: #f29c8f; + color: var(--code-number); } .json-boolean { - color: #69d1dd; + color: var(--code-boolean); } .json-null { - color: #9eb3ff; + color: var(--code-null); } .json-children { @@ -1221,14 +1231,14 @@ select { .markdown-code .hljs-title, .markdown-code .hljs-section, .markdown-code .hljs-link { - color: #efb360; + color: var(--code-keyword); } .markdown-code .hljs-string, .markdown-code .hljs-attr, .markdown-code .hljs-template-tag, .markdown-code .hljs-template-variable { - color: #7fd7a4; + color: var(--code-string); } .markdown-code .hljs-number, @@ -1236,25 +1246,25 @@ select { .markdown-code .hljs-bullet, .markdown-code .hljs-variable, .markdown-code .hljs-variable.constant_ { - color: #f29c8f; + color: var(--code-number); } .markdown-code .hljs-comment, .markdown-code .hljs-quote { - color: rgba(239, 243, 247, 0.48); + color: var(--code-comment); } .markdown-code .hljs-function, .markdown-code .hljs-title.function_, .markdown-code .hljs-title.class_ { - color: #9eb3ff; + color: var(--code-null); } .markdown-code .hljs-property, .markdown-code .hljs-attribute, .markdown-code .hljs-built_in, .markdown-code .hljs-type { - color: #69d1dd; + color: var(--code-boolean); } .markdown-table-wrap { @@ -1327,9 +1337,8 @@ select { } .artifact-meta-card { - border: 1px solid var(--border); - border-radius: var(--radius-lg); - background: color-mix(in srgb, var(--surface-elevated) 86%, transparent); + border-radius: var(--radius-md); + background: var(--surface-muted); padding: 0.9rem 1rem; } @@ -1414,24 +1423,7 @@ select { margin-bottom: 0; } -.fade-up { - opacity: 0; - transform: translateY(24px); - animation: fade-up 720ms cubic-bezier(0.2, 1, 0.22, 1) forwards; -} - -@keyframes fade-up { - to { - opacity: 1; - transform: translateY(0); - } -} - @media (max-width: 640px) { - body::before { - background-size: 48px 48px; - } - .app-shell { padding-inline: 0.7rem; padding-top: 0.6rem; @@ -1445,12 +1437,7 @@ select { .panel, .viewer-frame, .artifact-disclosure { - border-radius: calc(var(--radius-md) + 2px); - box-shadow: 0 12px 28px rgba(21, 27, 39, 0.08); - } - - .panel::after { - opacity: 0.52; + border-radius: var(--radius-lg); } .mono-pill { @@ -1950,10 +1937,6 @@ select { color: var(--print-ink) !important; } - body::before { - display: none; - } - body[data-print-mode="markdown"] .print-hide-on-markdown { display: none !important; } @@ -1964,14 +1947,9 @@ select { border: none !important; background: transparent !important; box-shadow: none !important; - backdrop-filter: none !important; overflow: visible !important; } - body[data-print-mode="markdown"] .panel::after { - display: none !important; - } - body[data-print-mode="markdown"] .app-shell { padding: 0 !important; } @@ -1995,10 +1973,10 @@ select { body[data-print-mode="markdown"] .markdown-print-heading h1 { margin: 0.45rem 0 0; color: var(--print-ink); - font-family: var(--font-display), serif; + font-family: var(--font-sans), sans-serif; font-size: 1.9rem; line-height: 1.1; - letter-spacing: -0.04em; + letter-spacing: -0.02em; } body[data-print-mode="markdown"] .markdown-print-heading p:last-child { diff --git a/src/app/layout.tsx b/src/app/layout.tsx index 4abd831..86f0576 100644 --- a/src/app/layout.tsx +++ b/src/app/layout.tsx @@ -7,7 +7,7 @@ import "./globals.css"; const display = Fraunces({ subsets: ["latin"], variable: "--font-display", - weight: ["500", "600", "700"], + weight: ["600"], }); const sans = IBM_Plex_Sans({ diff --git a/src/components/home/link-creator.tsx b/src/components/home/link-creator.tsx index a8964b7..62a3ff6 100644 --- a/src/components/home/link-creator.tsx +++ b/src/components/home/link-creator.tsx @@ -107,18 +107,18 @@ export function LinkCreator({ onPreviewHash }: LinkCreatorProps) { }; return ( -
+
-

Try it now

-

- Make a shareable artifact link from pasted content. +

Create a link

+

+ Paste content, get a link.

-

- Pick a format, paste the artifact, and generate a real `agent-render` URL in the browser. No backend, no upload step, no extra route. +

+ Choose a format and paste your artifact. The link is generated entirely in the browser.

@@ -243,8 +243,8 @@ export function LinkCreator({ onPreviewHash }: LinkCreatorProps) {
-

Generated link

-

Ready to copy, preview, or open

+

Result

+

Your link

@@ -296,7 +296,7 @@ export function LinkCreator({ onPreviewHash }: LinkCreatorProps) {

{generatedLink.envelope.title}

{isGeneratedLinkStale ? (

- The draft changed after the last generation. Generate again to refresh the link. + Draft changed since last generation. Hit Generate again.

) : null}
@@ -304,7 +304,7 @@ export function LinkCreator({ onPreviewHash }: LinkCreatorProps) { ) : (

- Generate a link to get a shareable URL that opens this artifact directly in the viewer. + Fill out the form and hit Generate to get a shareable URL.

)} diff --git a/src/components/renderers/code-renderer.tsx b/src/components/renderers/code-renderer.tsx index 6d0d663..77d45c3 100644 --- a/src/components/renderers/code-renderer.tsx +++ b/src/components/renderers/code-renderer.tsx @@ -48,8 +48,8 @@ const editorTheme = EditorView.theme({ }, ".cm-gutters": { backgroundColor: "var(--surface-code-raised)", - color: "rgba(239, 243, 247, 0.5)", - borderRight: "1px solid rgba(239, 243, 247, 0.08)", + color: "var(--code-gutter-text)", + borderRight: "1px solid var(--code-gutter-border)", minWidth: "3.3rem", }, ".cm-gutterElement": { @@ -57,13 +57,13 @@ const editorTheme = EditorView.theme({ textAlign: "right", }, ".cm-activeLine": { - backgroundColor: "rgba(255, 255, 255, 0.045)", + backgroundColor: "var(--code-active-line)", }, ".cm-activeLineGutter": { - backgroundColor: "rgba(255, 255, 255, 0.05)", + backgroundColor: "var(--code-active-line)", }, ".cm-selectionBackground": { - backgroundColor: "rgba(105, 209, 221, 0.18) !important", + backgroundColor: "var(--code-selection) !important", }, ".cm-rainbow-bracket": { fontWeight: "700", diff --git a/src/components/theme-toggle.tsx b/src/components/theme-toggle.tsx index f6e2c21..32f6f20 100644 --- a/src/components/theme-toggle.tsx +++ b/src/components/theme-toggle.tsx @@ -29,7 +29,7 @@ export function ThemeToggle({ className }: ThemeToggleProps) { type="button" onClick={() => mounted && setTheme(isDark ? "light" : "dark")} className={cn( - "mono-pill shell-pill min-w-[8.5rem] justify-center transition-transform duration-300 hover:-translate-y-0.5", + "mono-pill shell-pill min-w-[8.5rem] justify-center", className, )} aria-label={mounted ? `Switch to ${isDark ? "light" : "dark"} theme` : "Theme toggle loading"} diff --git a/src/components/viewer-shell.tsx b/src/components/viewer-shell.tsx index 9e37028..48a55e0 100644 --- a/src/components/viewer-shell.tsx +++ b/src/components/viewer-shell.tsx @@ -3,7 +3,6 @@ import dynamic from "next/dynamic"; import Image from "next/image"; import { useCallback, useEffect, useMemo, useRef, useState } from "react"; -import type { CSSProperties } from "react"; import type { LucideIcon } from "lucide-react"; import { ArrowUpRight, @@ -15,10 +14,7 @@ import { FileJson2, FileSpreadsheet, FileText, - FolderKanban, Printer, - ShieldCheck, - Sparkles, } from "lucide-react"; import { sampleEnvelopes, sampleLinks } from "@/lib/payload/examples"; import { decodeFragment, decodeFragmentAsync, encodeEnvelope, encodeEnvelopeAsync } from "@/lib/payload/fragment"; @@ -26,7 +22,6 @@ import { loadArxDictionary } from "@/lib/payload/arx-codec"; import { MAX_FRAGMENT_LENGTH, PAYLOAD_FRAGMENT_KEY, - artifactKinds, type ArtifactKind, type ArtifactPayload, type CodeArtifact, @@ -61,26 +56,6 @@ const sampleCards = sampleLinks.map((link, index) => ({ const iconPath = `${process.env.NEXT_PUBLIC_BASE_PATH ?? ""}/icon.svg`; -const ecosystemLinks = [ - { - href: "https://github.com/baanish/agent-render", - kicker: "Source", - title: "Browse the GitHub repo", - description: "Inspect the payload format, renderer shell, and deployment notes behind the static viewer.", - }, - { - href: "https://clawdhub.com/skills/agent-render-linking", - kicker: "Ecosystem", - title: "Use the ClawdHub skill", - description: "Help OpenClaw agents emit `agent-render` links intentionally across chat surfaces and workflows.", - }, -] as const; - -const emptyStateSteps = [ - "Pick a sample fragment to update the hash in place.", - "The shell decodes the envelope client-side and activates the chosen artifact.", - "Artifact-specific renderers take over this stage without sending the payload to a server.", -] as const; const MarkdownRenderer = dynamic( () => import("@/components/renderers/markdown-renderer").then((module) => module.MarkdownRenderer), @@ -227,9 +202,6 @@ function getStatusTone(parsed: ReturnType) { }; } -function getAnimationStyle(delay: number): CSSProperties { - return { animationDelay: `${delay}ms` }; -} /** * Render the main viewer shell for decoding and displaying artifact fragments from the URL hash. @@ -286,7 +258,6 @@ export function ViewerShell() { const csvArtifact: CsvArtifact | null = activeArtifact?.kind === "csv" ? activeArtifact : null; const jsonArtifact: JsonArtifact | null = activeArtifact?.kind === "json" ? activeArtifact : null; const hasKnownRenderer = Boolean(markdownArtifact || codeArtifact || diffArtifact || csvArtifact || jsonArtifact); - const budgetRatio = Math.min(fragmentLength / MAX_FRAGMENT_LENGTH, 1); const statusTone = getStatusTone(parsed); const viewerState = activeArtifact && envelope ? "artifact" : parsed.ok ? "decoded-no-artifact" : parsed.code === "empty" ? "empty" : "error"; @@ -437,7 +408,7 @@ export function ViewerShell() { data-renderer-ready={rendererReady ? "true" : "false"} >
-
+
{ @@ -453,29 +424,19 @@ export function ViewerShell() {

agent-render

-
- Zero Data Retention by design - -
+
{activeArtifact && envelope ? (
-
+
-
-

Artifact bundle

-
-

{envelope.title ?? "Untitled bundle"}

- {envelope.artifacts.length} item{envelope.artifacts.length === 1 ? "" : "s"} -
-

- Selecting an artifact updates the active fragment target while keeping the rendered payload front and center. -

+
+

{envelope.title ?? "Untitled bundle"}

- + {statusTone.label} {numberFormatter.format(fragmentLength)} chars @@ -492,15 +453,14 @@ export function ViewerShell() { />
-
+
-

{getArtifactSubtitle(activeArtifact)}

-

+

{getArtifactHeading(activeArtifact)}

-

- {getArtifactSupportingLabel(activeArtifact)} +

+ {getArtifactSubtitle(activeArtifact)}

@@ -546,7 +506,7 @@ export function ViewerShell() {
-
+
{getArtifactDetailRows(activeArtifact).map((row) => (
@@ -568,262 +528,63 @@ export function ViewerShell() {
) : (
-
-
-
-

Artifact viewer

-

- Share artifacts in the URL, keep the server out of the payload. -

-

- agent-render opens markdown, code, diff, CSV, and JSON artifacts from a single static link, so someone can understand the payload without uploading it anywhere. -

-
- - - fully static export - - - - product-minded shell - - - - renderer slots ready - -
-
- -
-
-

Protocol shape

-

- #{PAYLOAD_FRAGMENT_KEY}=v1.<codec>.<payload> -

-
-
-

Why it exists

-

- Agent outputs get flattened in chat. agent-render keeps them readable, portable, and static-host friendly. -

-
-
+
+

+ Artifacts in the URL. No server required. +

+

+ Paste markdown, code, diffs, CSV, or JSON. Get a static link that renders it — nothing uploaded, nothing stored. +

+
+ + GitHub + + + + ClawdHub skill + + + + #{PAYLOAD_FRAGMENT_KEY}=v1.<codec>.<payload> +
+
-
- {ecosystemLinks.map((link) => ( - - {link.kicker} - - {link.title} - - -

{link.description}

-
- ))} - -
- How to try it - Load any sample fragment below -

Pick a sample, update the hash, and the viewer opens without sending the artifact body to the host.

+ {!parsed.ok && parsed.code !== "empty" ? ( +
+
+ {statusTone.label} + {numberFormatter.format(fragmentLength)} / {numberFormatter.format(MAX_FRAGMENT_LENGTH)} chars
+

{statusTone.message}

+
+                  {getHashPreview(hash)}
+                
-
+ ) : null} -
-
-
-
-

Example fragments

-

Load a sample envelope

-
- {sampleCards.length} presets -
-

- Each preset uses the same fragment transport as the live product, so you can try the viewer shell with realistic payload sizes and renderer combinations in one click. -

- -
- -
-
-

Fragment inspector

-
-

Current URL state

- - {statusTone.label} - -
-

{statusTone.message}

-
- -
-
-

Fragment budget

-

{numberFormatter.format(fragmentLength)} / {numberFormatter.format(MAX_FRAGMENT_LENGTH)}

-
-
-

Codec

-

{parsed.ok ? parsed.envelope.codec : "plain"}

-
-
-

Artifacts

-

{parsed.ok ? numberFormatter.format(parsed.envelope.artifacts.length) : "0"}

-
-
- -
-
- Size budget - {Math.round(budgetRatio * 100)}% -
-
-
-
-
- -
-
-

Hash preview

-
-                      {getHashPreview(hash)}
-                    
-
- -
-

What happens next

-

- Once a fragment is present, the shell decodes the envelope in-browser, selects the active artifact, and swaps this empty state for the renderer-specific stage without changing the route model. -

-
-
+
- -
-
-
-

Viewer shell

-

- {activeArtifact?.title ?? envelope?.title ?? "Renderer staging area"} -

-

- {activeArtifact - ? `${getArtifactSubtitle(activeArtifact)} selected from the decoded fragment. Multiple artifact-specific viewers now render directly in-frame while the shell stays ready for future polish.` - : "This stage becomes the live artifact surface once a fragment is active. The payload stays in the URL hash, the route stays export-friendly, and each artifact kind can render inside the same shell."} -

-
- -
- {artifactKinds.map((kind) => { - const Icon = kindIcons[kind]; - const isCurrent = activeArtifact?.kind === kind; - - return ( - - - {kind} - - ); - })} -
-
- -
-
-
-
-

Ready for fragment decode

-

Choose a sample payload to populate this shell

-
- public-safe -
-
-
-
-

First-run flow

-

- The live renderer stage appears here as soon as a fragment is selected. -

-

- {parsed.ok - ? "A decoded fragment is already present, so the active artifact can take over this frame immediately." - : parsed.message} -

-
- -
- {emptyStateSteps.map((step, index) => ( -
-

Step {index + 1}

-

{step}

-
- ))} -
-
-
-
- -
-
-

Security posture

-

- Payloads stay in the hash, renderers stay client-side, and artifact-specific viewers can land without changing fragment transport semantics. -

-
-
-

Route model

-

- The app remains a single export-friendly route for Cloudflare Pages and other static hosts, even as richer artifact viewers plug into the shell. -

-
-
-

Supported artifacts

-

- Markdown, code, diff, CSV, and JSON all share the same shell contract, so the homepage can explain the product before the first payload ever lands. -

-
-
-
-
)}