From 01d0d6275e5bfabe51451b42da7896475752fea1 Mon Sep 17 00:00:00 2001 From: Aanish Bhirud Date: Fri, 27 Mar 2026 14:43:06 -0400 Subject: [PATCH 1/6] =?UTF-8?q?style:=20de-slop=20UI=20=E2=80=94=20strip?= =?UTF-8?q?=20AI-generated=20visual=20patterns?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Replace oversized border radii (28px→10px), remove glassmorphism (backdrop-blur, shine overlays, stacked gradients), tighten shadows (72px→3px), desaturate warm palette, restrict Fraunces to wordmark, remove page-load cascade animations and translateY hover effects, delete hero icon-pill badges. Achieves Linear/Raycast-clean aesthetic while preserving warm personality. Co-Authored-By: Claude Opus 4.6 (1M context) --- .../specs/2026-03-27-ui-de-slop-design.md | 126 +++++++++ src/app/globals.css | 257 ++++++------------ src/app/layout.tsx | 2 +- src/components/home/link-creator.tsx | 4 +- src/components/theme-toggle.tsx | 2 +- src/components/viewer-shell.tsx | 47 +--- 6 files changed, 223 insertions(+), 215 deletions(-) create mode 100644 docs/superpowers/specs/2026-03-27-ui-de-slop-design.md 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..ac2732f 100644 --- a/src/app/globals.css +++ b/src/app/globals.css @@ -2,63 +2,63 @@ @import "@git-diff-view/react/styles/diff-view-pure.css"; :root { - --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); + --page-bg: #f7f5f2; + --page-bg-secondary: rgba(92, 138, 142, 0.06); + --page-bg-accent: rgba(156, 107, 82, 0.07); + --surface: #ffffff; + --surface-strong: #fafaf9; --surface-muted: rgba(19, 28, 41, 0.035); - --surface-elevated: rgba(255, 255, 255, 0.82); - --border: rgba(23, 32, 51, 0.11); - --border-strong: rgba(23, 32, 51, 0.18); - --text-primary: #172033; - --text-muted: #5b6678; - --text-soft: #7a8495; - --accent: #ba5937; - --accent-strong: #964526; - --accent-secondary: #0e6c77; - --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-elevated: #ffffff; + --border: rgba(0, 0, 0, 0.08); + --border-strong: rgba(0, 0, 0, 0.14); + --text-primary: #1a1d23; + --text-muted: #6b7280; + --text-soft: #9ca3af; + --accent: #9c6b52; + --accent-strong: #7d5640; + --accent-secondary: #5c8a8e; + --success: #4a7a64; + --warning: #a07a4f; + --danger: #8c4a3e; + --shadow-lg: 0 1px 3px rgba(0, 0, 0, 0.08), 0 0 0 1px rgba(0, 0, 0, 0.04); + --shadow-md: 0 1px 2px rgba(0, 0, 0, 0.06); + --radius-xl: 10px; + --radius-lg: 8px; + --radius-md: 6px; + --radius-sm: 4px; + --duration-base: 150ms; + --surface-code: #0f1419; + --surface-code-raised: #1a2029; --surface-code-text: #eff3f7; --print-paper: #ffffff; - --print-ink: #172033; + --print-ink: #1a1d23; --print-muted: #566073; - --print-border: rgba(23, 32, 51, 0.14); + --print-border: rgba(0, 0, 0, 0.1); } .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); + --page-bg: #111113; + --page-bg-secondary: rgba(138, 181, 185, 0.04); + --page-bg-accent: rgba(192, 148, 120, 0.03); + --surface: #1a1a1c; + --surface-strong: #161618; --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); + --surface-elevated: #1e1e20; + --border: rgba(255, 255, 255, 0.08); + --border-strong: rgba(255, 255, 255, 0.14); --text-primary: #eef2f7; --text-muted: #a7b2c1; --text-soft: #7f8b9b; - --accent: #de7f58; - --accent-strong: #f0a27f; - --accent-secondary: #72c4cf; + --accent: #c09478; + --accent-strong: #d4a88e; + --accent-secondary: #8ab5b9; --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); + --shadow-lg: 0 1px 3px rgba(0, 0, 0, 0.24), 0 0 0 1px rgba(255, 255, 255, 0.04); + --shadow-md: 0 1px 2px rgba(0, 0, 0, 0.18); + --surface-code: #0a0e14; + --surface-code-raised: #111620; --surface-code-text: #eef3f7; } @@ -75,28 +75,7 @@ body { margin: 0; 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; + background: var(--page-bg); } a { @@ -130,38 +109,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 +133,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 { @@ -232,26 +186,20 @@ select { 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%); + background: var(--surface-elevated); 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; } .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) 4%, var(--surface-elevated)); } .hero-link-card.is-static:hover { - transform: none; border-color: var(--border); - box-shadow: none; + background: var(--surface-elevated); } .hero-link-card:focus-visible { @@ -312,7 +260,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; } @@ -325,18 +273,15 @@ select { border: 1px solid var(--border); border-radius: var(--radius-lg); padding: 1rem; - background: color-mix(in srgb, var(--surface-elevated) 88%, transparent); + background: var(--surface-elevated); 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) 4%, var(--surface-elevated)); } .sample-link.is-active { @@ -375,17 +320,16 @@ select { 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); + background: var(--surface-elevated); color: var(--text-primary); transition: border-color var(--duration-base) ease, - background-color var(--duration-base) ease, - box-shadow var(--duration-base) ease; + 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) 4%, var(--surface-elevated)); } .creator-kind-card.is-active { @@ -503,9 +447,7 @@ select { 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%); + background: var(--surface-elevated); padding: 1rem; } @@ -600,19 +542,16 @@ select { border: 1px solid var(--border); border-radius: var(--radius-lg); padding: 0.85rem 0.95rem; - background: color-mix(in srgb, var(--surface-elevated) 88%, transparent); + background: var(--surface-elevated); 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; + 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) 4%, var(--surface-elevated)); } .artifact-switcher-content { @@ -684,7 +623,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 +677,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 +740,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 +808,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 +830,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 +841,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 +891,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 { @@ -1414,24 +1348,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 +1362,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 +1862,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 +1872,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 +1898,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..628f320 100644 --- a/src/components/home/link-creator.tsx +++ b/src/components/home/link-creator.tsx @@ -107,13 +107,13 @@ export function LinkCreator({ onPreviewHash }: LinkCreatorProps) { }; return ( -
+

Try it now

-

+

Make a shareable artifact link from pasted content.

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..3c0b9c8 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"; @@ -227,9 +223,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. @@ -437,7 +430,7 @@ export function ViewerShell() { data-renderer-ready={rendererReady ? "true" : "false"} >
-
+
{ @@ -461,7 +454,7 @@ export function ViewerShell() { {activeArtifact && envelope ? (
-
+

Artifact bundle

@@ -492,11 +485,11 @@ export function ViewerShell() { />
-
+

{getArtifactSubtitle(activeArtifact)}

-

+

{getArtifactHeading(activeArtifact)}

@@ -546,7 +539,7 @@ export function ViewerShell() {

-
+
{getArtifactDetailRows(activeArtifact).map((row) => (
@@ -568,30 +561,16 @@ 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 - -
@@ -633,7 +612,7 @@ export function ViewerShell() {
-
+

Example fragments

@@ -672,7 +651,7 @@ export function ViewerShell() {
-
+

Fragment inspector

@@ -727,11 +706,11 @@ export function ViewerShell() {
-
+

Viewer shell

-

+

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

@@ -780,7 +759,7 @@ export function ViewerShell() {

First-run flow

-

+

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

From 174d199512905aeeb466178da9eb996f6f6879ad Mon Sep 17 00:00:00 2001 From: Aanish Bhirud Date: Fri, 27 Mar 2026 14:54:56 -0400 Subject: [PATCH 2/6] style: restore original palette, warm dark mode, flatten card nesting MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Restore original light mode colors (#f2ecdf, #ba5937, #0e6c77) — the desaturated palette was soulless - New warm dark mode: espresso/walnut tones (#17120e bg, #1e1814 surface) instead of generic blue-black or neutral gray — distinctive - Flatten card-within-card nesting: remove borders from metric-card, artifact-meta-card, viewer-frame, creator-result-card, sample-link, hero-link-card, creator-kind-card, artifact-switcher when nested inside panels. Use background tints + spacing for hierarchy instead. - Code surfaces now use warm dark (#1a1210) matching the warm dark mode - Subtle body gradient restored (two radials, not the old 3-layer stack) Co-Authored-By: Claude Opus 4.6 (1M context) --- src/app/globals.css | 190 ++++++++++++++------------------ src/components/viewer-shell.tsx | 2 +- 2 files changed, 84 insertions(+), 108 deletions(-) diff --git a/src/app/globals.css b/src/app/globals.css index ac2732f..3d5e821 100644 --- a/src/app/globals.css +++ b/src/app/globals.css @@ -2,64 +2,66 @@ @import "@git-diff-view/react/styles/diff-view-pure.css"; :root { - --page-bg: #f7f5f2; - --page-bg-secondary: rgba(92, 138, 142, 0.06); - --page-bg-accent: rgba(156, 107, 82, 0.07); - --surface: #ffffff; - --surface-strong: #fafaf9; + /* 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: #f8f4ea; + --surface-strong: #f5f0e4; --surface-muted: rgba(19, 28, 41, 0.035); - --surface-elevated: #ffffff; - --border: rgba(0, 0, 0, 0.08); - --border-strong: rgba(0, 0, 0, 0.14); - --text-primary: #1a1d23; - --text-muted: #6b7280; - --text-soft: #9ca3af; - --accent: #9c6b52; - --accent-strong: #7d5640; - --accent-secondary: #5c8a8e; - --success: #4a7a64; - --warning: #a07a4f; - --danger: #8c4a3e; - --shadow-lg: 0 1px 3px rgba(0, 0, 0, 0.08), 0 0 0 1px rgba(0, 0, 0, 0.04); - --shadow-md: 0 1px 2px rgba(0, 0, 0, 0.06); - --radius-xl: 10px; + --surface-elevated: #faf7ef; + --border: rgba(23, 32, 51, 0.11); + --border-strong: rgba(23, 32, 51, 0.18); + --text-primary: #172033; + --text-muted: #5b6678; + --text-soft: #7a8495; + --accent: #ba5937; + --accent-strong: #964526; + --accent-secondary: #0e6c77; + --success: #287357; + --warning: #ad6b1f; + --danger: #a33b2b; + --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: #0f1419; - --surface-code-raised: #1a2029; - --surface-code-text: #eff3f7; + --surface-code: #1a1210; + --surface-code-raised: #261c18; + --surface-code-text: #f0ebe4; --print-paper: #ffffff; - --print-ink: #1a1d23; + --print-ink: #172033; --print-muted: #566073; - --print-border: rgba(0, 0, 0, 0.1); + --print-border: rgba(23, 32, 51, 0.14); } +/* Warm dark mode — espresso & walnut */ .dark { - --page-bg: #111113; - --page-bg-secondary: rgba(138, 181, 185, 0.04); - --page-bg-accent: rgba(192, 148, 120, 0.03); - --surface: #1a1a1c; - --surface-strong: #161618; - --surface-muted: rgba(135, 154, 180, 0.05); - --surface-elevated: #1e1e20; - --border: rgba(255, 255, 255, 0.08); - --border-strong: rgba(255, 255, 255, 0.14); - --text-primary: #eef2f7; - --text-muted: #a7b2c1; - --text-soft: #7f8b9b; - --accent: #c09478; - --accent-strong: #d4a88e; - --accent-secondary: #8ab5b9; - --success: #78c5a8; - --warning: #e6b263; - --danger: #ef9c90; - --shadow-lg: 0 1px 3px rgba(0, 0, 0, 0.24), 0 0 0 1px rgba(255, 255, 255, 0.04); - --shadow-md: 0 1px 2px rgba(0, 0, 0, 0.18); - --surface-code: #0a0e14; - --surface-code-raised: #111620; - --surface-code-text: #eef3f7; + --page-bg: #17120e; + --page-bg-secondary: rgba(14, 108, 119, 0.06); + --page-bg-accent: rgba(186, 89, 55, 0.05); + --surface: #1e1814; + --surface-strong: #1a1410; + --surface-muted: rgba(186, 160, 130, 0.06); + --surface-elevated: #241d18; + --border: rgba(186, 160, 130, 0.12); + --border-strong: rgba(186, 160, 130, 0.2); + --text-primary: #f0ebe4; + --text-muted: #b8a99a; + --text-soft: #8a7d72; + --accent: #d88a62; + --accent-strong: #eaa07c; + --accent-secondary: #5dbfcc; + --success: #6dbfa0; + --warning: #d9a65a; + --danger: #d47a6e; + --shadow-lg: 0 2px 8px rgba(0, 0, 0, 0.3); + --shadow-md: 0 1px 3px rgba(0, 0, 0, 0.2); + --surface-code: #120e0a; + --surface-code-raised: #1e1612; + --surface-code-text: #f0ebe4; } * { @@ -75,7 +77,10 @@ body { margin: 0; color: var(--text-primary); font-family: var(--font-sans), sans-serif; - background: var(--page-bg); + background: + 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 { @@ -157,9 +162,8 @@ 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; } @@ -184,22 +188,17 @@ select { gap: 0.6rem; min-height: 100%; padding: 1rem 1.05rem; - border: 1px solid var(--border); - border-radius: var(--radius-lg); - background: var(--surface-elevated); - transition: - border-color var(--duration-base) ease, - background-color var(--duration-base) ease; + border-radius: var(--radius-md); + background: var(--surface-muted); + transition: background-color var(--duration-base) ease; } .hero-link-card:hover { - border-color: color-mix(in srgb, var(--accent-secondary) 44%, var(--border)); - background: color-mix(in srgb, var(--accent-secondary) 4%, var(--surface-elevated)); + background: color-mix(in srgb, var(--accent-secondary) 8%, var(--surface-muted)); } .hero-link-card.is-static:hover { - border-color: var(--border); - background: var(--surface-elevated); + background: var(--surface-muted); } .hero-link-card:focus-visible { @@ -270,23 +269,20 @@ 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: var(--surface-elevated); + background: var(--surface-muted); transition: border-color var(--duration-base) ease, background-color var(--duration-base) ease; } .sample-link:hover { - border-color: color-mix(in srgb, var(--accent) 42%, var(--border)); - background: color-mix(in srgb, var(--accent) 4%, var(--surface-elevated)); + 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)); } .home-generator-grid, @@ -318,23 +314,18 @@ select { align-items: center; gap: 0.7rem; padding: 0.82rem 0.88rem; - border: 1px solid var(--border); - border-radius: var(--radius-lg); - background: var(--surface-elevated); + 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; + transition: background-color var(--duration-base) ease; } .creator-kind-card:hover { - border-color: color-mix(in srgb, var(--accent-secondary) 42%, var(--border)); - background: color-mix(in srgb, var(--accent-secondary) 4%, var(--surface-elevated)); + 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, @@ -445,9 +436,8 @@ select { display: grid; gap: 0.85rem; height: 100%; - border: 1px solid var(--border); - border-radius: calc(var(--radius-xl) - 2px); - background: var(--surface-elevated); + border-radius: var(--radius-lg); + background: var(--surface-muted); padding: 1rem; } @@ -463,9 +453,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; } @@ -539,19 +528,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: var(--surface-elevated); + background: var(--surface-muted); scroll-snap-align: start; - transition: - border-color var(--duration-base) ease, - background-color var(--duration-base) ease; + transition: background-color var(--duration-base) ease; } .artifact-switcher:hover { - border-color: color-mix(in srgb, var(--accent-secondary) 46%, var(--border)); - background: color-mix(in srgb, var(--accent-secondary) 4%, var(--surface-elevated)); + background: color-mix(in srgb, var(--accent-secondary) 8%, var(--surface-muted)); } .artifact-switcher-content { @@ -559,9 +544,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 { @@ -587,17 +570,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; } @@ -1261,9 +1238,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; } diff --git a/src/components/viewer-shell.tsx b/src/components/viewer-shell.tsx index 3c0b9c8..52fd500 100644 --- a/src/components/viewer-shell.tsx +++ b/src/components/viewer-shell.tsx @@ -689,7 +689,7 @@ export function ViewerShell() {

-
+

Hash preview

                       {getHashPreview(hash)}

From a8142c598633ba4ca5a6320c54d06433e7a3f720 Mon Sep 17 00:00:00 2001
From: Aanish Bhirud 
Date: Fri, 27 Mar 2026 15:33:58 -0400
Subject: [PATCH 3/6] style: blue dark mode, gut home page layout
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

Dark mode: blue-charcoal (#0c1015 bg, #131920 surface) with warm
accents — warm orange and teal pop against cool blue base.

Layout sledgehammer — deleted ~300 lines from home page:
- Killed hero panel (heading, metric cards, ecosystem link cards)
- Killed inspector panel (budget bar, codec/artifact metrics, hash
  preview, "what happens next" card)
- Killed stage panel (fake viewer, step cards, 3 side-grid info cards)
- Killed "Zero Data Retention" header pill
- Replaced sample card grid with simple row-based list
- Simplified viewer state: killed verbose descriptions, tightened
  artifact headings

What remains: header → link creator → sample list. The tool IS the
page. No marketing copy, no documentation-as-UI, no cards-in-cards.

Co-Authored-By: Claude Opus 4.6 (1M context) 
---
 src/app/globals.css             |  69 +++++---
 src/components/viewer-shell.tsx | 304 +++-----------------------------
 2 files changed, 71 insertions(+), 302 deletions(-)

diff --git a/src/app/globals.css b/src/app/globals.css
index 3d5e821..f8df0e5 100644
--- a/src/app/globals.css
+++ b/src/app/globals.css
@@ -37,31 +37,31 @@
   --print-border: rgba(23, 32, 51, 0.14);
 }
 
-/* Warm dark mode — espresso & walnut */
+/* Cool blue-charcoal dark mode — warm accents on cool base */
 .dark {
-  --page-bg: #17120e;
-  --page-bg-secondary: rgba(14, 108, 119, 0.06);
-  --page-bg-accent: rgba(186, 89, 55, 0.05);
-  --surface: #1e1814;
-  --surface-strong: #1a1410;
-  --surface-muted: rgba(186, 160, 130, 0.06);
-  --surface-elevated: #241d18;
-  --border: rgba(186, 160, 130, 0.12);
-  --border-strong: rgba(186, 160, 130, 0.2);
-  --text-primary: #f0ebe4;
-  --text-muted: #b8a99a;
-  --text-soft: #8a7d72;
-  --accent: #d88a62;
-  --accent-strong: #eaa07c;
-  --accent-secondary: #5dbfcc;
+  --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: #5cc7d4;
   --success: #6dbfa0;
   --warning: #d9a65a;
   --danger: #d47a6e;
-  --shadow-lg: 0 2px 8px rgba(0, 0, 0, 0.3);
-  --shadow-md: 0 1px 3px rgba(0, 0, 0, 0.2);
-  --surface-code: #120e0a;
-  --surface-code-raised: #1e1612;
-  --surface-code-text: #f0ebe4;
+  --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;
 }
 
 * {
@@ -285,6 +285,33 @@ select {
   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,
 .creator-form-grid,
 .creator-result-metrics,
diff --git a/src/components/viewer-shell.tsx b/src/components/viewer-shell.tsx
index 52fd500..e74b9cb 100644
--- a/src/components/viewer-shell.tsx
+++ b/src/components/viewer-shell.tsx
@@ -5,7 +5,6 @@ import Image from "next/image";
 import { useCallback, useEffect, useMemo, useRef, useState } from "react";
 import type { LucideIcon } from "lucide-react";
 import {
-  ArrowUpRight,
   Check,
   Copy,
   Download,
@@ -22,7 +21,6 @@ import { loadArxDictionary } from "@/lib/payload/arx-codec";
 import {
   MAX_FRAGMENT_LENGTH,
   PAYLOAD_FRAGMENT_KEY,
-  artifactKinds,
   type ArtifactKind,
   type ArtifactPayload,
   type CodeArtifact,
@@ -57,26 +55,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),
@@ -279,7 +257,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";
 
@@ -446,29 +423,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 @@ -488,12 +455,11 @@ export function ViewerShell() {
-

{getArtifactSubtitle(activeArtifact)}

-

+

{getArtifactHeading(activeArtifact)}

-

- {getArtifactSupportingLabel(activeArtifact)} +

+ {getArtifactSubtitle(activeArtifact)}

@@ -561,248 +527,24 @@ 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. -

-
- -
-
-

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. -

-
-
-
- -
-
- -
-
-
-
-

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. -

-
-
-
-
+
)}
From 4ce576593701e78c3be4390f81d0f7270178ed85 Mon Sep 17 00:00:00 2001 From: Aanish Bhirud Date: Fri, 27 Mar 2026 15:55:31 -0400 Subject: [PATCH 4/6] style: light code surfaces, restore home content, theme-aware syntax MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Code surfaces: - Light mode now uses warm cream code background (#ede7da) with dark syntax colors — no more dark blocks clashing with warm page - All syntax colors (string, number, keyword, boolean, etc.) are now CSS variables that switch between light and dark mode - CodeMirror gutter, active line, and selection colors use CSS vars - JSON tree view colors use the same variables Home page content restored with new layout: - Intro section: heading + description + protocol shape (no panel) - Link creator (unchanged) - Sample fragments: simple row-based list - Fragment state: definition list with key-value pairs, no cards - Links: simple text links to GitHub and ClawdHub - All content present, no cards-within-cards Co-Authored-By: Claude Opus 4.6 (1M context) --- src/app/globals.css | 84 +++++++++++++++++---- src/components/renderers/code-renderer.tsx | 10 +-- src/components/viewer-shell.tsx | 85 ++++++++++++++++++---- 3 files changed, 146 insertions(+), 33 deletions(-) diff --git a/src/app/globals.css b/src/app/globals.css index f8df0e5..b9bbaa4 100644 --- a/src/app/globals.css +++ b/src/app/globals.css @@ -28,9 +28,21 @@ --radius-md: 6px; --radius-sm: 4px; --duration-base: 150ms; - --surface-code: #1a1210; - --surface-code-raised: #261c18; - --surface-code-text: #f0ebe4; + --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; @@ -62,6 +74,18 @@ --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; } * { @@ -169,7 +193,41 @@ select { .empty-state-layout { display: grid; + gap: 2.5rem; +} + +.home-intro { + max-width: 42rem; +} + +.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, @@ -1125,19 +1183,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 { @@ -1159,14 +1217,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, @@ -1174,25 +1232,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 { 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/viewer-shell.tsx b/src/components/viewer-shell.tsx index e74b9cb..f21b73c 100644 --- a/src/components/viewer-shell.tsx +++ b/src/components/viewer-shell.tsx @@ -527,24 +527,79 @@ export function ViewerShell() {
) : (
+
+

+ 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. +

+

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

+
+ - +
+
+
)} From b4452df10969ae5bcde28bf12a0aac0b1dc52f4c Mon Sep 17 00:00:00 2001 From: Aanish Bhirud Date: Fri, 27 Mar 2026 16:02:19 -0400 Subject: [PATCH 5/6] copy: rewrite all UI text, promote repo + skill links MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Copy pass: - "Share artifacts in the URL, keep the server out of the payload" → "Artifacts in the URL. No server required." - "agent-render opens markdown, code, diff..." (43 words) → "Paste markdown, code, diffs, CSV, or JSON. Get a static link that renders it — nothing uploaded, nothing stored." (20 words) - "Make a shareable artifact link from pasted content" → "Paste content, get a link." - "Pick a format, paste the artifact, and generate a real..." (22 words) → "Choose a format and paste your artifact. The link is generated entirely in the browser." (14 words) - "Ready to copy, preview, or open" → "Your link" - "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." Links promoted: - GitHub + ClawdHub skill links moved from buried bottom section to intro area, inline with protocol shape, with arrow icons - Removed separate "Links" section Co-Authored-By: Claude Opus 4.6 (1M context) --- src/components/home/link-creator.tsx | 16 ++++++------- src/components/viewer-shell.tsx | 34 ++++++++++++++-------------- 2 files changed, 25 insertions(+), 25 deletions(-) diff --git a/src/components/home/link-creator.tsx b/src/components/home/link-creator.tsx index 628f320..62a3ff6 100644 --- a/src/components/home/link-creator.tsx +++ b/src/components/home/link-creator.tsx @@ -112,13 +112,13 @@ export function LinkCreator({ onPreviewHash }: LinkCreatorProps) {
-

Try it now

+

Create a link

- Make a shareable artifact link from pasted content. + 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/viewer-shell.tsx b/src/components/viewer-shell.tsx index f21b73c..c29344b 100644 --- a/src/components/viewer-shell.tsx +++ b/src/components/viewer-shell.tsx @@ -5,6 +5,7 @@ import Image from "next/image"; import { useCallback, useEffect, useMemo, useRef, useState } from "react"; import type { LucideIcon } from "lucide-react"; import { + ArrowUpRight, Check, Copy, Download, @@ -529,14 +530,24 @@ export function ViewerShell() {

- Share artifacts in the URL, keep the server out of the payload. + Artifacts in the URL. No server required.

-

- 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. -

-

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

+ 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> + +
@@ -588,17 +599,6 @@ export function ViewerShell() { ) : null}
-
-

Links

- -
)} From 931ac608dffea97a482c2536bd08085911ae0c46 Mon Sep 17 00:00:00 2001 From: Aanish Bhirud Date: Fri, 27 Mar 2026 18:25:56 -0400 Subject: [PATCH 6/6] fix: show fragment diagnostics only on error, promote to top MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Fragment state section was always visible — now it only appears when the URL contains a broken fragment (parsed.code !== "empty" and !parsed.ok). When it does appear, it renders as an error banner with a danger-colored left border right below the intro, before the link creator — so it's the first thing you see when something is wrong. Removed the always-visible "Fragment state" section with its Status/Budget/Codec/Artifacts metadata table. Co-Authored-By: Claude Opus 4.6 (1M context) --- src/app/globals.css | 14 +++++++++++ src/components/viewer-shell.tsx | 41 +++++++++++---------------------- 2 files changed, 27 insertions(+), 28 deletions(-) diff --git a/src/app/globals.css b/src/app/globals.css index b9bbaa4..1212700 100644 --- a/src/app/globals.css +++ b/src/app/globals.css @@ -200,6 +200,20 @@ select { 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; diff --git a/src/components/viewer-shell.tsx b/src/components/viewer-shell.tsx index c29344b..48a55e0 100644 --- a/src/components/viewer-shell.tsx +++ b/src/components/viewer-shell.tsx @@ -550,6 +550,19 @@ export function ViewerShell() {
+ {!parsed.ok && parsed.code !== "empty" ? ( +
+
+ {statusTone.label} + {numberFormatter.format(fragmentLength)} / {numberFormatter.format(MAX_FRAGMENT_LENGTH)} chars +
+

{statusTone.message}

+
+                  {getHashPreview(hash)}
+                
+
+ ) : null} +
@@ -571,34 +584,6 @@ export function ViewerShell() { })} - -
-

Fragment state

-
-
-
Status
-
{statusTone.label}
-
-
-
Budget
-
{numberFormatter.format(fragmentLength)} / {numberFormatter.format(MAX_FRAGMENT_LENGTH)}
-
-
-
Codec
-
{parsed.ok ? parsed.envelope.codec : "—"}
-
-
-
Artifacts
-
{parsed.ok ? parsed.envelope.artifacts.length : "0"}
-
-
- {hash ? ( -
-                    {getHashPreview(hash)}
-                  
- ) : null} -
-
)}