diff --git a/.factory/settings.json b/.factory/settings.json new file mode 100644 index 0000000..565f14a --- /dev/null +++ b/.factory/settings.json @@ -0,0 +1,5 @@ +{ + "enabledPlugins": { + "core@factory-plugins": true + } +} \ No newline at end of file diff --git a/.gitignore b/.gitignore index 6dec77b..5e5848d 100644 --- a/.gitignore +++ b/.gitignore @@ -140,3 +140,13 @@ test-results/ .pnp.* .cursor/* /.junie + +# macOS +.DS_Store + +# Design references & screenshots +Stitch/ +final-light*.png + +# Playwright MCP snapshots +.playwright-mcp/ diff --git a/.impeccable.md b/.impeccable.md new file mode 100644 index 0000000..1399aec --- /dev/null +++ b/.impeccable.md @@ -0,0 +1,72 @@ +# agent-render Design Context + +## Users +Developers and AI agents sharing structured artifacts (markdown, code, diffs, CSV, JSON) via static links. Users arrive from chat surfaces, CI pipelines, or direct links. They need to immediately understand a payload without uploading it anywhere. Context is technical but the experience should feel curated, not utilitarian. + +## Brand Personality +**Precise. Editorial. Atmospheric.** + +The interface should feel like opening a well-typeset technical document in a reading room — not like loading another SaaS dashboard. It takes technical content seriously enough to present it beautifully. + +## Aesthetic Direction +- **Visual tone**: Brutalist precision meets editorial warmth. Sharp architecture, generous whitespace, atmospheric color. +- **Light mode**: Warm ivory canvas (`#faf8f4`), sunset orange accent (`#cc6633`), muted lavender secondary (`#8b5ca8`). Ghost borders. Fraunces serif for display, IBM Plex Sans for body, IBM Plex Mono for machine labels. +- **Dark mode**: Deep dusk canvas (`#0e0b14`) — purple-black, not gray-black. Ember orange accent (`#e08c50`), soft lavender secondary (`#b08ed0`). Zero border-radius. Visible 40px terminal grid overlay. +- **References**: Brass Hands (operational language, brutalist minimalism), Factory.ai (dark-first, orange accents, generous space, premium micro-interactions) +- **Anti-references**: Generic SaaS dashboards, Shadcn default look, anything with large border-radius pills or blue link colors + +## Design Principles + +### 1. Ghost borders, not lines +Borders are a failure of hierarchy. Separation comes from tonal background shifts and whitespace. When a border must exist, it's a ghost: warm taupe at 14% opacity (light) or twilight violet at 16% (dark). Never a solid 1px line. + +### 2. Orange is a laser, not a paint bucket +The accent color is reserved for critical actions, active states, and status indicators. It should feel like a precise highlight on a technical document, not a brand color flooding the interface. + +### 3. Sharp corners are architectural +Light mode: 2px radius. Dark mode: 0px radius. No pills, no large rounded corners. The interface is built, not inflated. + +### 4. Typography has three voices +Display (Fraunces serif) speaks with editorial authority. Body (IBM Plex Sans) is the clear, readable workhorse. Mono (IBM Plex Mono) is the machine — metadata, timestamps, protocol labels. Each has a distinct role; mixing them creates the tension between human curation and technical precision. + +### 5. Shadows are atmospheric, never mechanical +Shadows are tinted with the surface color and heavily diffused. Light mode: 3-7% opacity, barely there. Dark mode: 25-50% opacity, creating depth in the void. Never use pure black drop shadows. + +### 6. Full-bleed sections, not floating cards +The landing page uses full-width editorial sections separated by ghost border-bottoms and generous vertical padding (80-128px). The bento grid uses 1px gaps filled with border color for tonal separation. Content breathes. + +### 7. Active states are tactile +All buttons use `active:scale-95` for press feedback. Hover transitions are slow and deliberate (500ms for background, 280ms for transforms). The interface responds to touch like a physical object. + +## Color Tokens + +### Light +| Token | Value | Role | +|-------|-------|------| +| `--page-bg` | `#faf8f4` | Canvas | +| `--text-primary` | `#2a2028` | Headings, body | +| `--text-muted` | `#6b5e68` | Secondary text | +| `--text-soft` | `#8a7e86` | Tertiary, labels | +| `--accent` | `#cc6633` | Primary actions, highlights | +| `--accent-secondary` | `#8b5ca8` | Links, blockquote accents | +| `--border` | `rgba(160,140,130,0.14)` | Ghost borders | +| `--surface-code` | `#1e1a22` | Code block backgrounds | + +### Dark +| Token | Value | Role | +|-------|-------|------| +| `--page-bg` | `#0e0b14` | Canvas (deep dusk) | +| `--text-primary` | `#ece6f0` | Headings, body | +| `--text-muted` | `#b8afc2` | Secondary text | +| `--text-soft` | `#8a7e96` | Tertiary, labels | +| `--accent` | `#e08c50` | Primary actions, highlights | +| `--accent-secondary` | `#b08ed0` | Links, blockquote accents | +| `--border` | `rgba(100,80,120,0.16)` | Ghost borders | +| `--surface-code` | `rgba(10,8,16,0.98)` | Code block backgrounds | + +## Tech Stack +- Next.js 15 (static export), React 19, TypeScript +- Tailwind CSS v4 with CSS custom properties (no tailwind.config) +- `next-themes` for class-based light/dark switching +- Fraunces + IBM Plex Sans + IBM Plex Mono (Google Fonts via next/font) +- CodeMirror 6 for code rendering, git-diff-view for diffs diff --git a/AGENTS.md b/AGENTS.md index fb7fd50..2c3733f 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -85,7 +85,7 @@ Current rules: - fragment key: `agent-render` - format: `v1..` for `plain|lz|deflate`, and `v1.arx..` for `arx` - codecs: `plain`, `lz`, `deflate`, and `arx` -- fragment size budget: `8000` characters +- fragment size budget: `8192` characters - decoded payload budget: `200000` characters - packed wire transport (`p: 1`) is allowed and must decode back to the standard envelope - bundles must contain at least one artifact diff --git a/docs/payload-format.md b/docs/payload-format.md index 9160a63..4ce4125 100644 --- a/docs/payload-format.md +++ b/docs/payload-format.md @@ -77,7 +77,7 @@ Packed key map: ## Limits -- Supported fragment budget: 8,000 characters +- Supported fragment budget: 8,192 characters - Supported decoded payload budget: 200,000 characters - Larger payloads should fail with a clear error before rendering - Compression is selected automatically by shortest fragment across packed/non-packed candidates @@ -102,7 +102,7 @@ Running `npm run codec:poc` (single markdown artifact containing `AGENTS.md`) cu Result: `arx` with baseBMP encoding achieves ~69% smaller fragments than `deflate` on this payload (~6.1x compression ratio). The improvement comes from brotli compression (~20% better than deflate), baseBMP encoding (~15.92 bits/char using ~62k safe BMP code points), and domain dictionary substitution. **base64url** is an ASCII-only option that can beat base76 on surfaces that percent-encode Unicode (chat apps, some shorteners). Base1k, baseBMP, and base76 remain available; auto-selection compares estimated transport length. -Timing (AGENTS.md 8000 chars, avg of 10 runs): +Timing (AGENTS.md 8192 chars, avg of 10 runs): - `deflate+base64url`: ~0.1ms - `arx+base76`: ~13.8ms - `arx+base64url`: ~8.1ms diff --git a/skills/agent-render-linking/SKILL.md b/skills/agent-render-linking/SKILL.md index 03eae58..352849e 100644 --- a/skills/agent-render-linking/SKILL.md +++ b/skills/agent-render-linking/SKILL.md @@ -201,7 +201,7 @@ If the dictionary fetch fails, fall back to `deflate` codec. ## Practical limits Respect these limits: -- target fragment budget: about 8,000 characters +- target fragment budget: about 8,192 characters - target decoded payload budget: about 200,000 characters - strict Discord practical budget for linked text workflows: about 1,500 characters diff --git a/src/app/globals.css b/src/app/globals.css index 06a7d20..67f429d 100644 --- a/src/app/globals.css +++ b/src/app/globals.css @@ -2,64 +2,81 @@ @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); - --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; + /* ── Surface hierarchy — warm ivory with sunset undertone ── */ + --page-bg: #faf8f4; + --surface: rgba(250, 248, 244, 0.88); + --surface-strong: rgba(255, 255, 255, 0.96); + --surface-muted: rgba(40, 30, 50, 0.03); + --surface-elevated: rgba(245, 241, 235, 0.86); + /* ── Ghost borders — warm taupe ── */ + --border: rgba(160, 140, 130, 0.14); + --border-strong: rgba(160, 140, 130, 0.24); + /* ── Text — warm charcoal ── */ + --text-primary: #2a2028; + --text-muted: #6b5e68; + --text-soft: #8a7e86; + /* ── Accent — sunset orange ── */ + --accent: #cc6633; + --accent-strong: #a8502a; + --accent-secondary: #8b5ca8; + /* ── Semantic ── */ + --success: #4a8c6f; + --warning: #b8863a; + --danger: #c44b3f; + /* ── Ambient shadows — warm-tinted ── */ + --shadow-lg: 0 26px 72px rgba(42, 32, 40, 0.07); + --shadow-md: 0 20px 40px rgba(42, 32, 40, 0.035); + /* ── Radius — sharp, architectural ── */ + --radius-xl: 2px; + --radius-lg: 2px; + --radius-md: 2px; + --radius-sm: 2px; --duration-base: 280ms; - --surface-code: rgba(18, 27, 40, 0.95); - --surface-code-raised: rgba(28, 38, 54, 0.82); - --surface-code-text: #eff3f7; + /* ── Code surfaces ── */ + --surface-code: #1e1a22; + --surface-code-raised: rgba(30, 26, 34, 0.92); + --surface-code-text: #ede8f2; + /* ── Print ── */ --print-paper: #ffffff; - --print-ink: #172033; - --print-muted: #566073; - --print-border: rgba(23, 32, 51, 0.14); + --print-ink: #2a2028; + --print-muted: #6b5e68; + --print-border: rgba(160, 140, 130, 0.14); } .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; - --accent: #de7f58; - --accent-strong: #f0a27f; - --accent-secondary: #72c4cf; + /* ── Surface hierarchy — deep dusk ── */ + --page-bg: #0e0b14; + --surface: rgba(18, 14, 24, 0.92); + --surface-strong: rgba(14, 11, 20, 0.98); + --surface-muted: rgba(180, 160, 200, 0.05); + --surface-elevated: rgba(24, 20, 32, 0.96); + /* ── Ghost borders — twilight violet ── */ + --border: rgba(100, 80, 120, 0.16); + --border-strong: rgba(100, 80, 120, 0.28); + /* ── Text — warm pearl ── */ + --text-primary: #ece6f0; + --text-muted: #b8afc2; + --text-soft: #8a7e96; + /* ── Accent — sunset ember ── */ + --accent: #e08c50; + --accent-strong: #c87a42; + --accent-secondary: #b08ed0; + /* ── Semantic ── */ --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; + --warning: #deb06a; + --danger: #e8a098; + /* ── Shadows — purple-black ── */ + --shadow-lg: 0 28px 72px rgba(14, 11, 20, 0.5); + --shadow-md: 0 20px 40px rgba(14, 11, 20, 0.25); + /* ── Radius — zero, architectural ── */ + --radius-xl: 0px; + --radius-lg: 0px; + --radius-md: 0px; + --radius-sm: 0px; + /* ── Code ── */ + --surface-code: rgba(10, 8, 16, 0.98); + --surface-code-raised: rgba(18, 14, 26, 0.95); + --surface-code-text: #ece6f0; } * { @@ -76,28 +93,20 @@ 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)); + radial-gradient(ellipse 60% 45% at 0% 0%, #f1e0d2, transparent 70%), + radial-gradient(ellipse 45% 45% at 100% 5%, #e6dee8, transparent 70%), + radial-gradient(ellipse 40% 35% at 65% 100%, #ede2d6, transparent 70%), + var(--page-bg); } .dark body { background: - linear-gradient(180deg, color-mix(in srgb, var(--page-bg) 94%, #0e141d 6%), var(--page-bg)); + radial-gradient(ellipse 70% 50% at 0% 0%, #2a1a10, transparent 70%), + radial-gradient(ellipse 60% 60% at 100% 20%, #1a142a, transparent 70%), + radial-gradient(ellipse 50% 40% at 50% 100%, #201018, transparent 70%), + 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; -} a { color: inherit; @@ -112,10 +121,15 @@ select { } ::selection { - background: color-mix(in srgb, var(--accent) 28%, transparent); + background: color-mix(in srgb, var(--accent) 36%, transparent); color: var(--text-primary); } +.dark ::selection { + background: rgba(224, 140, 80, 0.30); + color: #ece6f0; +} + .font-display { font-family: var(--font-display), serif; } @@ -124,54 +138,145 @@ select { font-family: var(--font-mono), monospace; } -.panel { - position: relative; +/* ── Custom scrollbars ── */ +::-webkit-scrollbar { + width: 4px; + height: 4px; +} + +::-webkit-scrollbar-track { + background: transparent; +} + +::-webkit-scrollbar-thumb { + background: var(--surface-elevated); + border-radius: 2px; +} + +::-webkit-scrollbar-thumb:hover { + background: var(--accent); +} + +/* ── Nav bar — flat, full-width, not a card ── */ +.nav-bar { + background: var(--page-bg); + border-bottom: 1px solid var(--border); +} + +/* ── Full-bleed editorial sections ── */ +.home-hero-section { + padding-top: 2rem; + padding-bottom: 2rem; + border-bottom: 1px solid var(--border); +} + +@media (min-width: 768px) { + .home-hero-section { + padding-top: 3rem; + padding-bottom: 3rem; + } +} + +@media (min-width: 1280px) { + .home-hero-section { + padding-top: 4rem; + padding-bottom: 4rem; + } +} + +.section-header { + display: flex; + flex-wrap: wrap; + align-items: flex-start; + justify-content: space-between; + gap: 1rem; +} + +.home-samples-section, +.home-inspector-section, +.home-stage-section { + padding-top: 2.5rem; + padding-bottom: 2.5rem; + border-bottom: 1px solid var(--border); +} + +@media (min-width: 768px) { + .home-samples-section, + .home-inspector-section, + .home-stage-section { + padding-top: 4rem; + padding-bottom: 4rem; + } +} + +/* ── Bento grid — tonal separation via 1px gaps ── */ +.bento-grid { + display: grid; + grid-template-columns: 1fr; + gap: 1px; + background: var(--border); border: 1px solid var(--border); 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; +@media (min-width: 768px) { + .bento-grid { + grid-template-columns: repeat(2, 1fr); + } +} + +@media (min-width: 1280px) { + .bento-grid { + grid-template-columns: repeat(3, 1fr); + } + + .bento-wide { + grid-column: span 2; + } +} + +.bento-grid-compact { + grid-template-columns: repeat(2, 1fr); } -.dark .panel::after { - background: linear-gradient(180deg, rgba(255, 255, 255, 0.04), transparent 30%); - opacity: 0.48; +.bento-card { + background: var(--page-bg); } -.panel-strong { - background: var(--surface-strong); - box-shadow: var(--shadow-lg); +.dark .bento-card { + background: rgba(14, 11, 20, 0.98); } -.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%); +.bento-link { + display: grid; + gap: 0.5rem; + align-content: start; + transition: background-color 500ms ease; } -.dark .panel-hero { +.bento-link:hover { + background: color-mix(in srgb, var(--accent) 3%, var(--page-bg)); +} + +.dark .bento-link:hover { + background: rgba(224, 140, 80, 0.04); +} + +.panel { + position: relative; + border: 1px solid var(--border); + border-radius: var(--radius-xl); background: var(--surface-strong); + overflow: hidden; } .section-kicker { color: var(--text-soft); font-family: var(--font-mono), monospace; - font-size: 0.72rem; - font-weight: 600; - letter-spacing: 0.12em; - text-transform: uppercase; + font-size: 0.75rem; + font-weight: 500; + letter-spacing: 0.02em; } .mono-pill { @@ -196,6 +301,12 @@ select { background: color-mix(in srgb, var(--surface-strong) 88%, transparent); } +/* Dark data chips */ +.dark .mono-pill { + background: rgba(24, 20, 32, 0.8); + border-color: rgba(100, 80, 120, 0.22); +} + .metric-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(150px, 1fr)); @@ -211,16 +322,10 @@ select { .empty-state-layout { display: grid; - gap: 1rem; + gap: 0; } -.hero-metric-stack, -.hero-callout-row, -.empty-state-lower-grid, -.sample-link-grid, -.viewer-shell-empty-grid, -.viewer-shell-side-grid, -.viewer-step-grid { +.sample-link-grid { display: grid; gap: 0.75rem; } @@ -232,20 +337,15 @@ 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: color-mix(in srgb, var(--surface-elevated) 80%, transparent); 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-color 150ms ease, + background-color 150ms 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); + border-color: color-mix(in srgb, var(--accent) 30%, var(--border)); + background: color-mix(in srgb, var(--accent) 3%, var(--surface-elevated)); } .hero-link-card.is-static:hover { @@ -255,17 +355,16 @@ select { } .hero-link-card:focus-visible { - outline: 2px solid color-mix(in srgb, var(--accent-secondary) 48%, transparent); + outline: 2px solid color-mix(in srgb, var(--accent) 48%, transparent); outline-offset: 2px; } .hero-link-eyebrow { - color: var(--accent-secondary); + color: var(--accent); font-family: var(--font-mono), monospace; - font-size: 0.72rem; - font-weight: 600; - letter-spacing: 0.12em; - text-transform: uppercase; + font-size: 0.75rem; + font-weight: 500; + letter-spacing: 0.02em; } .hero-link-title { @@ -303,16 +402,16 @@ select { } .budget-track { - height: 0.7rem; - border-radius: var(--radius-sm); + height: 3px; + border-radius: 0; background: color-mix(in srgb, var(--surface-muted) 78%, transparent); overflow: hidden; } .budget-fill { height: 100%; - border-radius: inherit; - background: linear-gradient(90deg, var(--accent-secondary), var(--accent)); + border-radius: 0; + background: linear-gradient(90deg, var(--accent), var(--accent-strong)); transition: width var(--duration-base) ease; } @@ -324,7 +423,7 @@ select { min-height: 100%; border: 1px solid var(--border); border-radius: var(--radius-lg); - padding: 1rem; + padding: 1.25rem 1.5rem; background: color-mix(in srgb, var(--surface-elevated) 88%, transparent); transition: transform var(--duration-base) ease, @@ -334,14 +433,26 @@ select { } .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); + border-color: color-mix(in srgb, var(--accent) 30%, var(--border)); + background: color-mix(in srgb, var(--accent) 3%, var(--surface-elevated)); } .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)); + border-color: color-mix(in srgb, var(--accent) 50%, var(--border)); + background: color-mix(in srgb, var(--accent) 5%, var(--surface-strong)); +} + +.home-generator-section { + padding-top: 2.5rem; + padding-bottom: 2.5rem; + border-bottom: 1px solid var(--border); +} + +@media (min-width: 768px) { + .home-generator-section { + padding-top: 4rem; + padding-bottom: 4rem; + } } .home-generator-grid, @@ -353,7 +464,7 @@ select { } .home-generator-grid { - gap: 1rem; + gap: 1.5rem; } .home-generator-heading { @@ -378,25 +489,24 @@ select { background: color-mix(in srgb, var(--surface-elevated) 90%, transparent); color: var(--text-primary); transition: - border-color var(--duration-base) ease, - background-color var(--duration-base) ease, - box-shadow var(--duration-base) ease; + border-color 150ms ease, + background-color 150ms 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); + border-color: color-mix(in srgb, var(--accent) 30%, var(--border)); + background: color-mix(in srgb, var(--accent) 3%, var(--surface-elevated)); } .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)); + border-color: color-mix(in srgb, var(--accent) 45%, var(--border)); + background: color-mix(in srgb, var(--accent) 6%, var(--surface-strong)); } .creator-kind-card:focus-visible, .creator-input:focus-visible, .creator-textarea:focus-visible { - outline: 2px solid color-mix(in srgb, var(--accent-secondary) 44%, transparent); + outline: 2px solid color-mix(in srgb, var(--accent) 44%, transparent); outline-offset: 2px; } @@ -408,7 +518,7 @@ select { border: 1px solid var(--border); border-radius: var(--radius-md); background: color-mix(in srgb, var(--surface-muted) 90%, transparent); - color: var(--accent-secondary); + color: var(--accent); } .creator-kind-label { @@ -449,10 +559,17 @@ select { .creator-textarea, .creator-link-output { width: 100%; - border: 1px solid var(--border); + border: none; + border-bottom: 2px solid transparent; border-radius: var(--radius-lg); background: color-mix(in srgb, var(--surface-strong) 94%, transparent); color: var(--text-primary); + transition: border-color var(--duration-base) ease; +} + +.creator-input:focus-visible, +.creator-textarea:focus-visible { + border-bottom-color: var(--accent); } .creator-input, @@ -499,14 +616,12 @@ select { .creator-result-card { display: grid; - gap: 0.85rem; + gap: 1rem; 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%); - padding: 1rem; + border-radius: var(--radius-xl); + background: color-mix(in srgb, var(--surface-elevated) 80%, transparent); + padding: 1.5rem; } .creator-result-head { @@ -560,35 +675,58 @@ select { .artifact-first-layout { display: grid; - gap: 1rem; + gap: 0; } -.artifact-bundle-header { +/* ── Compact toolbar ribbon ── */ +.artifact-toolbar-bar { display: flex; flex-wrap: wrap; - align-items: flex-start; + align-items: center; justify-content: space-between; - gap: 1rem; + gap: 0.75rem; + padding: 0.75rem 0; + border-bottom: 1px solid var(--border); +} + +.artifact-toolbar-left { + display: flex; + flex-wrap: wrap; + align-items: center; + gap: 0.75rem 1.5rem; +} + +/* ── Editorial content area ── */ +.artifact-content-section { + padding-top: 2rem; + padding-bottom: 2rem; +} + +@media (min-width: 768px) { + .artifact-content-section { + padding-top: 3rem; + padding-bottom: 3rem; + } } .artifact-selector-row { display: flex; gap: 0.75rem; - margin-top: 1rem; - padding-top: 1rem; + padding-top: 0.5rem; + padding-bottom: 0.5rem; scroll-padding-inline: 1rem; overflow-x: auto; - border-top: 1px solid var(--border); + border-bottom: 1px solid var(--border); scroll-snap-type: x proximity; } .artifact-selector-row::-webkit-scrollbar { - height: 0.55rem; + height: 4px; } .artifact-selector-row::-webkit-scrollbar-thumb { - border-radius: var(--radius-sm); - background: color-mix(in srgb, var(--accent-secondary) 22%, var(--border)); + border-radius: 2px; + background: color-mix(in srgb, var(--accent) 22%, var(--border)); } .artifact-switcher { @@ -610,9 +748,8 @@ select { } .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); + border-color: color-mix(in srgb, var(--accent) 30%, var(--border)); + background: color-mix(in srgb, var(--accent) 3%, var(--surface-elevated)); } .artifact-switcher-content { @@ -620,13 +757,17 @@ 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); + border-color: color-mix(in srgb, var(--accent) 40%, var(--border)); + background: color-mix(in srgb, var(--accent) 6%, var(--surface-strong)); + box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--accent) 12%, transparent); +} + +.dark .artifact-switcher.is-active { + box-shadow: inset 3px 0 0 var(--accent); } .artifact-switcher:focus-visible { - outline: 2px solid color-mix(in srgb, var(--accent-secondary) 48%, transparent); + outline: 2px solid color-mix(in srgb, var(--accent) 48%, transparent); outline-offset: 2px; } @@ -639,12 +780,12 @@ select { border: 1px solid var(--border); border-radius: var(--radius-md); background: color-mix(in srgb, var(--surface-muted) 90%, transparent); - color: var(--accent-secondary); + color: var(--accent); } .artifact-switcher.is-active .artifact-switcher-icon { - border-color: color-mix(in srgb, var(--accent-secondary) 46%, var(--border)); - background: color-mix(in srgb, var(--accent-secondary) 12%, var(--surface-strong)); + border-color: color-mix(in srgb, var(--accent) 40%, var(--border)); + background: color-mix(in srgb, var(--accent) 8%, var(--surface-strong)); } .viewer-frame { @@ -663,37 +804,18 @@ select { min-height: 0; } -.viewer-frame-home { - height: 100%; -} - -.viewer-frame-hero { - margin-top: 1rem; -} - -.artifact-stage-head { - display: flex; - flex-wrap: wrap; - align-items: flex-start; - justify-content: space-between; - gap: 1rem; -} - -.viewer-head { - display: flex; - align-items: center; - justify-content: space-between; - gap: 1rem; - padding: 1rem 1.2rem; - border-bottom: 1px solid var(--border); -} - .artifact-preview { min-height: 0; - padding: 1rem; + padding: 1.5rem; background: color-mix(in srgb, var(--surface-strong) 96%, transparent); } +@media (min-width: 768px) { + .artifact-preview { + padding: 2rem 2.5rem; + } +} + .dark .artifact-preview { background: color-mix(in srgb, var(--surface-elevated) 96%, transparent); } @@ -712,19 +834,6 @@ select { padding: 0.95rem; } -.viewer-empty-preview { - min-height: clamp(18rem, 34vw, 24rem); - display: flex; - align-items: stretch; -} - -.viewer-empty-content { - display: grid; - gap: 1rem; - width: 100%; - align-items: start; -} - .viewer-toolbar { display: flex; flex-wrap: wrap; @@ -746,30 +855,36 @@ select { color: var(--text-primary); font-family: var(--font-mono), monospace; font-size: 0.74rem; - font-weight: 600; - letter-spacing: 0.02em; + font-weight: 500; + letter-spacing: 0.01em; 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-color 150ms ease, + background-color 150ms 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); + border-color: color-mix(in srgb, var(--accent) 40%, var(--border)); + background: color-mix(in srgb, var(--accent) 5%, var(--surface-elevated)); } .artifact-action:focus-visible { - outline: 2px solid color-mix(in srgb, var(--accent-secondary) 48%, transparent); + outline: 2px solid color-mix(in srgb, var(--accent) 48%, transparent); outline-offset: 2px; } .artifact-action.is-primary { - border-color: color-mix(in srgb, var(--accent-secondary) 45%, var(--border)); - background: color-mix(in srgb, var(--accent-secondary) 12%, var(--surface-strong)); - color: var(--accent-secondary); + border-color: transparent; + background: linear-gradient(135deg, var(--accent), var(--accent-strong)); + color: #ffffff; +} + +.dark .artifact-action.is-primary { + background: var(--accent); + color: #1a0e08; +} + +.artifact-action.is-primary:hover { + box-shadow: 0 14px 30px color-mix(in srgb, var(--accent) 20%, transparent); } .artifact-action.is-code { @@ -839,11 +954,16 @@ select { } .markdown-link { - color: var(--accent-secondary); + color: var(--accent); text-decoration: underline; - text-decoration-color: color-mix(in srgb, var(--accent-secondary) 36%, transparent); + text-decoration-color: color-mix(in srgb, var(--accent) 36%, transparent); text-decoration-thickness: 1.5px; text-underline-offset: 0.18em; + transition: text-decoration-color var(--duration-base) ease; +} + +.markdown-link:hover { + text-decoration-color: var(--accent); } .markdown-article ul, @@ -869,11 +989,13 @@ select { .markdown-article blockquote { margin-inline: 0; - padding: 1rem 1.2rem; + padding: 1.2rem 1.4rem; 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)); + border-radius: 0; + background: color-mix(in srgb, var(--surface-elevated) 60%, transparent); color: var(--text-primary); + font-family: var(--font-display), serif; + font-style: italic; } .markdown-article blockquote > * { @@ -883,7 +1005,7 @@ select { .markdown-inline-code { padding: 0.18rem 0.45rem; border: 1px solid var(--border); - border-radius: 0.55rem; + border-radius: var(--radius-md); background: color-mix(in srgb, var(--surface-muted) 82%, transparent); color: var(--text-primary); font-family: var(--font-mono), monospace; @@ -898,6 +1020,10 @@ select { box-shadow: var(--shadow-md); } +.dark .markdown-code-frame { + border-color: rgba(100, 80, 120, 0.18); +} + .markdown-code-head { display: flex; align-items: center; @@ -905,7 +1031,11 @@ 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: linear-gradient(90deg, color-mix(in srgb, var(--accent) 8%, transparent), transparent 58%); +} + +.dark .markdown-code-head { + background: linear-gradient(90deg, rgba(224, 140, 80, 0.05), transparent 58%); } .markdown-code-chip, @@ -941,7 +1071,7 @@ select { } .code-renderer-shell { - border-radius: calc(var(--radius-xl) - 6px); + border-radius: var(--radius-lg); overflow: hidden; background: var(--surface-code); box-shadow: inset 0 0 0 1px rgba(239, 243, 247, 0.05); @@ -1039,8 +1169,8 @@ select { } .patch-bundle-link.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)); + border-color: color-mix(in srgb, var(--accent) 40%, var(--border)); + background: color-mix(in srgb, var(--accent) 6%, var(--surface-strong)); } .patch-bundle-files { @@ -1300,6 +1430,7 @@ select { height: 1px; border: 0; background: var(--border); + margin: 2rem 0; } .markdown-article img { @@ -1316,22 +1447,11 @@ select { } .artifact-list-item.is-active { - border-color: color-mix(in srgb, var(--accent-secondary) 52%, var(--border)); - background: color-mix(in srgb, var(--accent-secondary) 10%, var(--surface-strong)); + border-color: color-mix(in srgb, var(--accent) 40%, var(--border)); + background: color-mix(in srgb, var(--accent) 6%, var(--surface-strong)); } -.artifact-meta-grid { - display: grid; - grid-template-columns: repeat(auto-fit, minmax(150px, 1fr)); - gap: 0.75rem; -} -.artifact-meta-card { - border: 1px solid var(--border); - border-radius: var(--radius-lg); - background: color-mix(in srgb, var(--surface-elevated) 86%, transparent); - padding: 0.9rem 1rem; -} .artifact-meta-value { margin-top: 0.45rem; @@ -1374,7 +1494,7 @@ select { } .artifact-disclosure-summary:focus-visible { - outline: 2px solid color-mix(in srgb, var(--accent-secondary) 40%, transparent); + outline: 2px solid color-mix(in srgb, var(--accent) 40%, transparent); outline-offset: -2px; } @@ -1428,29 +1548,15 @@ select { } @media (max-width: 640px) { - body::before { - background-size: 48px 48px; - } - - .app-shell { - padding-inline: 0.7rem; - padding-top: 0.6rem; - padding-bottom: 1.2rem; - } - .app-shell > div { - gap: 0.85rem; + gap: 1.5rem; } .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-md); + box-shadow: 0 12px 28px rgba(28, 28, 24, 0.06); } .mono-pill { @@ -1459,31 +1565,15 @@ select { font-size: 0.69rem; } - .home-hero-panel, - .home-generator-panel, - .home-samples-panel, - .home-inspector-panel, - .home-stage-panel { - padding: 0.95rem; - } - .metric-card, - .artifact-meta-card, .artifact-hash-preview, .artifact-empty-state { padding: 0.72rem 0.8rem; } .metric-grid, - .artifact-meta-grid, .artifact-disclosure-grid, - .hero-metric-stack, - .hero-callout-row, - .empty-state-lower-grid, .sample-link-grid, - .viewer-shell-empty-grid, - .viewer-shell-side-grid, - .viewer-step-grid, .artifact-first-layout { gap: 0.6rem; } @@ -1557,68 +1647,10 @@ select { .hero-link-body, .artifact-disclosure-status, - .viewer-empty-content p, .sample-link p { line-height: 1.48; } - .home-hero-panel > div:first-child, - .home-empty-lower-grid { - gap: 0.85rem; - } - - .home-hero-panel h2 { - font-size: clamp(1.9rem, 9vw, 2.35rem); - line-height: 0.95; - } - - .home-hero-panel .metric-card p, - .home-inspector-panel .metric-card p, - .home-stage-panel .metric-card p { - line-height: 1.45; - } - - .home-hero-callouts { - margin-top: 0.85rem; - } - - .home-hero-callouts .hero-link-card.is-static { - order: -1; - } - - .home-samples-panel .sample-link-grid, - .home-inspector-panel .metric-grid, - .home-stage-panel .viewer-shell-empty-grid { - margin-top: 0.85rem; - } - - .home-generator-panel .creator-form-grid, - .home-generator-panel .creator-kind-grid { - margin-top: 0.85rem; - } - - .home-inspector-panel .metric-grid { - grid-template-columns: repeat(2, minmax(0, 1fr)); - } - - .home-inspector-panel .metric-grid .metric-card:last-child { - grid-column: 1 / -1; - } - - .home-inspector-panel .budget-track { - height: 0.58rem; - } - - .home-inspector-panel pre { - margin-top: 0.65rem; - line-height: 1.45; - } - - .artifact-bundle-header, - .artifact-stage-head { - gap: 0.72rem; - } - .artifact-first-layout { gap: 0.72rem; } @@ -1803,22 +1835,12 @@ select { border-bottom: 1px solid var(--border); } - .viewer-head { - align-items: flex-start; - gap: 0.68rem; - padding: 0.76rem 0.8rem; - } - .viewer-toolbar { width: 100%; justify-content: flex-start; gap: 0.38rem; } - .artifact-stage-head { - gap: 0.72rem; - } - .artifact-action { flex: 1 1 calc(50% - 0.25rem); min-height: 2.55rem; @@ -1830,12 +1852,10 @@ select { flex-basis: 100%; } - .viewer-frame-primary, - .viewer-empty-preview { + .viewer-frame-primary { min-height: 0; } - .artifact-meta-grid, .artifact-disclosure-grid { grid-template-columns: repeat(2, minmax(0, 1fr)); } @@ -1886,16 +1906,11 @@ select { @media (max-width: 360px) { .creator-kind-grid, - .home-inspector-panel .metric-grid, - .artifact-meta-grid, + .bento-grid-compact, .artifact-disclosure-grid { grid-template-columns: 1fr; } - .home-inspector-panel .metric-grid .metric-card:last-child { - grid-column: auto; - } - .artifact-action { flex-basis: 100%; } @@ -1911,34 +1926,11 @@ select { grid-template-columns: repeat(2, minmax(0, 1fr)); } - .hero-callout-row, - .sample-link-grid, - .viewer-shell-side-grid, - .viewer-step-grid { + .sample-link-grid { grid-template-columns: repeat(2, minmax(0, 1fr)); } - - .viewer-empty-content { - grid-template-columns: minmax(0, 1.06fr) minmax(17rem, 0.94fr); - } } -@media (min-width: 1280px) { - .hero-callout-row { - grid-template-columns: minmax(0, 1fr) minmax(0, 1fr) minmax(0, 0.92fr); - } - - .empty-state-lower-grid, - .viewer-shell-empty-grid { - grid-template-columns: minmax(0, 1.05fr) minmax(0, 0.95fr); - align-items: stretch; - } - - .viewer-shell-side-grid { - grid-template-columns: 1fr; - align-content: stretch; - } -} @media print { @page { @@ -1950,10 +1942,6 @@ select { color: var(--print-ink) !important; } - body::before { - display: none; - } - body[data-print-mode="markdown"] .print-hide-on-markdown { display: none !important; } @@ -1968,10 +1956,6 @@ select { overflow: visible !important; } - body[data-print-mode="markdown"] .panel::after { - display: none !important; - } - body[data-print-mode="markdown"] .app-shell { padding: 0 !important; } diff --git a/src/components/home/link-creator.tsx b/src/components/home/link-creator.tsx index a8964b7..abf67a5 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

-

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

+ Paste content, pick a format, and get a shareable URL. Everything encodes client-side.

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

Generated link

-

Ready to copy, preview, or open

+

Output

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

) : 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 in the form and hit generate.

)} diff --git a/src/components/theme-toggle.tsx b/src/components/theme-toggle.tsx index f6e2c21..06b9508 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 transition-colors duration-150", 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..31d50d6 100644 --- a/src/components/viewer-shell.tsx +++ b/src/components/viewer-shell.tsx @@ -65,21 +65,21 @@ 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.", + title: "GitHub", + description: "Source code, payload format, and deployment docs.", }, { 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.", + title: "ClawdHub skill", + description: "Let OpenClaw agents generate agent-render links in chat.", }, ] 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.", + "Pick a sample fragment below.", + "The payload decodes client-side from the URL hash.", + "The renderer displays the artifact without contacting a server.", ] as const; const MarkdownRenderer = dynamic( @@ -114,26 +114,26 @@ function getArtifactBody(artifact: ArtifactPayload): string { function getArtifactSubtitle(artifact: ArtifactPayload): string { if (artifact.kind === "markdown") { - return "GFM markdown artifact"; + return "Markdown"; } if (artifact.kind === "code") { - return artifact.language ? `${artifact.language} source artifact` : "Source artifact"; + return artifact.language ?? "Code"; } if (artifact.kind === "diff") { - return artifact.view ? `${artifact.view} diff artifact` : "Patch artifact"; + return artifact.view ? `${artifact.view} diff` : "Diff"; } if (artifact.kind === "json") { - return "Structured data artifact"; + return "JSON"; } if (artifact.kind === "csv") { - return "Tabular data artifact"; + return "CSV"; } - return "Document artifact"; + return (artifact as ArtifactPayload).kind; } function getArtifactHeading(artifact: ArtifactPayload): string { @@ -208,20 +208,20 @@ function getStatusTone(parsed: ReturnType) { return { label: "Decoded", color: "var(--success)", - message: "Envelope is valid and ready for viewer routing.", + message: "Fragment decoded successfully.", }; } if (parsed.code === "empty") { return { - label: "Awaiting fragment", + label: "Empty", color: "var(--accent-secondary)", message: parsed.message, }; } return { - label: "Needs correction", + label: "Error", color: "var(--danger)", message: parsed.message, }; @@ -429,105 +429,91 @@ export function ViewerShell() { return (
-
-
- { - e.preventDefault(); - handleGoHome(); - }} - className="flex items-center gap-2.5 sm:gap-3 focus:outline-none focus-visible:ring-2 focus-visible:ring-[color:var(--accent-secondary)] focus-visible:ring-offset-2 rounded-[var(--radius-lg)] -m-1 p-1" - aria-label="Go to homepage" - > -
- -
-

agent-render

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

-
- -
- - {statusTone.label} - - {numberFormatter.format(fragmentLength)} chars -
+ {/* ── Compact file-info toolbar ── */} +
+
+ + {statusTone.label} + + {getArtifactSupportingLabel(activeArtifact)} + {numberFormatter.format(fragmentLength)} chars
- - -
- -
-
-
-

{getArtifactSubtitle(activeArtifact)}

-

- {getArtifactHeading(activeArtifact)} -

-

- {getArtifactSupportingLabel(activeArtifact)} -

-
- -
- - + {markdownArtifact ? ( + - {markdownArtifact ? ( - - ) : null} - {activeArtifact.kind} -
+ ) : null} +
+
-
+ {/* ── Artifact selector (when multi-artifact) ── */} + {envelope.artifacts.length > 1 ? ( +
+ +
+ ) : null} + + {/* ── Editorial artifact heading + content ── */} +
+
+

{getArtifactSubtitle(activeArtifact)}

+

+ {getArtifactHeading(activeArtifact)} +

+
+ +
{markdownArtifact ? ( @@ -546,10 +532,11 @@ export function ViewerShell() {
+ {/* ── Metadata bento ── */}
-
+
{getArtifactDetailRows(activeArtifact).map((row) => ( -
+

{row.label}

{row.value}

@@ -568,176 +555,154 @@ 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. -

-
-
+ {/* ── Editorial hero ── */} +
+

Artifact viewer

+

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

+

+ View markdown, code, diffs, CSV, and JSON from a single static link. Nothing leaves the browser. +

+
+ + + static export + + + + 5 renderers + + + + zero retention +
+
-
- {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.

-
+ {/* ── Bento feature grid ── */} +
+
+

Protocol shape

+

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

+
+
+

Why

+

+ Agent outputs get flattened in chat. This keeps them readable and portable. +

+
+ {ecosystemLinks.map((link) => ( + + {link.kicker} + + {link.title} + + +

{link.description}

+
+ ))} +
+ Try it + Load a sample below +

Click any sample to populate the viewer from the URL hash.

+ {/* ── Link creator ── */} -
-
-
-
-

Example fragments

-

Load a sample envelope

-
- {sampleCards.length} presets + {/* ── Samples + Inspector — full-bleed sections ── */} +
+
+
+

Example fragments

+

Load a sample envelope

-

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

- -
+

{sample.title}

+

+ {sample.description ?? `${sample.envelope.artifacts.length} artifact${sample.envelope.artifacts.length === 1 ? "" : "s"}`} +

+
+ + + ); + })} +
+
-
+
+

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"}

-
+

Current URL state

+ + {statusTone.label} + +
+

{statusTone.message}

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

Fragment budget

+

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

+
- -
-
-

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

-
+
+

Codec

+

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

-
-
+
+

Artifacts

+

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

+
+
+

Hash preview

+
+                    {getHashPreview(hash)}
+                  
+
+
+
-
-
+ {/* ── Initialize section ── */} +
+

Viewer shell

-

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

+ {activeArtifact?.title ?? envelope?.title ?? "Initialize your Artifact"}

-

+

{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."} + ? `${getArtifactSubtitle(activeArtifact)} selected.` + : "Select a fragment above to render it here. Everything stays in the URL."}

@@ -753,8 +718,8 @@ export function ViewerShell() { style={ isCurrent ? { - borderColor: "var(--accent-secondary)", - color: "var(--accent-secondary)", + borderColor: "var(--accent)", + color: "var(--accent)", } : undefined } @@ -767,60 +732,35 @@ export function ViewerShell() {
-
-
-
-
-

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}

-
- ))} -
-
-
+
+
+

Getting started

+

+ Pick a sample or paste your own content above. +

+

+ {parsed.ok + ? "Fragment decoded — select an artifact to render it." + : "No fragment in the URL yet."} +

- -
-
-

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

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

Step {index + 1}

+

{step}

+ ))} +
+

Security

+

+ The payload never leaves the URL hash. Rendering is entirely client-side. +

+
+
+

Hosting

+

+ Single static route. Works on any static host. +

diff --git a/src/components/viewer/fragment-details-disclosure.tsx b/src/components/viewer/fragment-details-disclosure.tsx index 9ee8e33..8f22326 100644 --- a/src/components/viewer/fragment-details-disclosure.tsx +++ b/src/components/viewer/fragment-details-disclosure.tsx @@ -28,7 +28,7 @@ export function FragmentDetailsDisclosure({ Fragment details - Codec, transport, budget, and hash preview + Codec, budget, and hash preview diff --git a/src/lib/payload/examples.ts b/src/lib/payload/examples.ts index 24ada12..6271245 100644 --- a/src/lib/payload/examples.ts +++ b/src/lib/payload/examples.ts @@ -114,7 +114,7 @@ export const sampleEnvelopes: PayloadEnvelope[] = [ title: "Artifact manifest", filename: "manifest.json", content: - '{\n "name": "agent-render",\n "version": "2.0.0",\n "description": "Fragment-powered artifact viewer with arx compression",\n "transport": {\n "method": "url-fragment",\n "prefix": "agent-render=v1",\n "codecs": ["plain", "lz", "deflate", "arx"],\n "preferred": "arx",\n "maxFragmentChars": 8000,\n "maxDecodedChars": 200000\n },\n "arx": {\n "pipeline": ["dictionary-substitution", "brotli-q11", "binary-to-text"],\n "encodings": {\n "base76": { "charset": "ascii", "bitsPerChar": 6.27, "maxCompat": true },\n "base1k": { "charset": "U+00A1-U+07FF", "bitsPerChar": 10.79 },\n "baseBMP": { "charset": "U+00A1-U+FFEF", "bitsPerChar": 15.92, "smallest": true }\n },\n "dictionary": {\n "version": 1,\n "entries": 128,\n "targets": ["json-structure", "common-words", "html-entities", "code-patterns"]\n }\n },\n "artifacts": {\n "kinds": ["markdown", "code", "diff", "csv", "json"],\n "thisBundle": [\n { "id": "release-notes", "kind": "markdown", "title": "v2.0 release notes" },\n { "id": "codec-src", "kind": "code", "title": "arx-codec.ts (excerpt)" },\n { "id": "migration-diff", "kind": "diff", "title": "v1 → v2 migration" },\n { "id": "metrics", "kind": "csv", "title": "Bundle metrics" },\n { "id": "manifest", "kind": "json", "title": "Artifact manifest" }\n ]\n },\n "features": {\n "zeroRetention": true,\n "serverNeverSeesPayload": true,\n "clientSideOnly": true,\n "selfHostable": true,\n "openSource": true\n }\n}', + '{\n "name": "agent-render",\n "version": "2.0.0",\n "description": "Fragment-powered artifact viewer with arx compression",\n "transport": {\n "method": "url-fragment",\n "prefix": "agent-render=v1",\n "codecs": ["plain", "lz", "deflate", "arx"],\n "preferred": "arx",\n "maxFragmentChars": 8192,\n "maxDecodedChars": 200000\n },\n "arx": {\n "pipeline": ["dictionary-substitution", "brotli-q11", "binary-to-text"],\n "encodings": {\n "base76": { "charset": "ascii", "bitsPerChar": 6.27, "maxCompat": true },\n "base1k": { "charset": "U+00A1-U+07FF", "bitsPerChar": 10.79 },\n "baseBMP": { "charset": "U+00A1-U+FFEF", "bitsPerChar": 15.92, "smallest": true }\n },\n "dictionary": {\n "version": 1,\n "entries": 128,\n "targets": ["json-structure", "common-words", "html-entities", "code-patterns"]\n }\n },\n "artifacts": {\n "kinds": ["markdown", "code", "diff", "csv", "json"],\n "thisBundle": [\n { "id": "release-notes", "kind": "markdown", "title": "v2.0 release notes" },\n { "id": "codec-src", "kind": "code", "title": "arx-codec.ts (excerpt)" },\n { "id": "migration-diff", "kind": "diff", "title": "v1 → v2 migration" },\n { "id": "metrics", "kind": "csv", "title": "Bundle metrics" },\n { "id": "manifest", "kind": "json", "title": "Artifact manifest" }\n ]\n },\n "features": {\n "zeroRetention": true,\n "serverNeverSeesPayload": true,\n "clientSideOnly": true,\n "selfHostable": true,\n "openSource": true\n }\n}', }, ], }, diff --git a/src/lib/payload/schema.ts b/src/lib/payload/schema.ts index 45d5219..66bd88f 100644 --- a/src/lib/payload/schema.ts +++ b/src/lib/payload/schema.ts @@ -1,4 +1,4 @@ -export const MAX_FRAGMENT_LENGTH = 8000; +export const MAX_FRAGMENT_LENGTH = 8192; export const MAX_DECODED_PAYLOAD_LENGTH = 200000; export const PAYLOAD_FRAGMENT_KEY = "agent-render"; diff --git a/tests/components/fragment-details-disclosure.test.tsx b/tests/components/fragment-details-disclosure.test.tsx index 6d6fd75..de28524 100644 --- a/tests/components/fragment-details-disclosure.test.tsx +++ b/tests/components/fragment-details-disclosure.test.tsx @@ -3,6 +3,7 @@ import { render, screen } from "@testing-library/react"; import userEvent from "@testing-library/user-event"; import { describe, expect, it } from "vitest"; import { FragmentDetailsDisclosure } from "@/components/viewer/fragment-details-disclosure"; +import { MAX_FRAGMENT_LENGTH } from "@/lib/payload/schema"; describe("FragmentDetailsDisclosure", () => { it("reveals metadata when expanded", async () => { @@ -11,13 +12,13 @@ describe("FragmentDetailsDisclosure", () => { codec="lz" fragmentLength="120" hashPreview="#agent-render=v1.lz.abc" - maxLength="8000" + maxLength={String(MAX_FRAGMENT_LENGTH)} statusLabel="Decoded" - statusMessage="Envelope is valid and ready for viewer routing." + statusMessage="Fragment decoded successfully." />, ); - const summary = screen.getByText(/Codec, transport, budget, and hash preview/i); + const summary = screen.getByText(/Codec, budget, and hash preview/i); await userEvent.click(summary); expect(screen.getByText("Decoded")).toBeVisible(); diff --git a/tests/e2e/helpers.ts b/tests/e2e/helpers.ts index eafdc8a..a569c79 100644 --- a/tests/e2e/helpers.ts +++ b/tests/e2e/helpers.ts @@ -23,7 +23,7 @@ export async function stabilizePage(page: Page) { } export async function waitForViewerState(page: Page, state: string) { - await expect(page.locator('[data-testid="viewer-shell"]')).toHaveAttribute("data-viewer-state", state); + await expect(page.locator('[data-testid="viewer-shell"]')).toHaveAttribute("data-viewer-state", state, { timeout: 15000 }); } export async function waitForRendererReady(page: Page, kind: "markdown" | "code" | "diff" | "csv" | "json") { diff --git a/tests/e2e/viewer.spec.ts b/tests/e2e/viewer.spec.ts index cbe2ecc..51139dc 100644 --- a/tests/e2e/viewer.spec.ts +++ b/tests/e2e/viewer.spec.ts @@ -67,7 +67,7 @@ test("renders markdown payloads and triggers print", async ({ page }) => { }; }); - await page.getByRole("button", { name: "Print / PDF" }).click(); + await page.getByRole("button", { name: "Print" }).click(); await expect.poll(() => page.evaluate(() => Boolean(window.__printCalled))).toBe(true); }); @@ -108,25 +108,17 @@ test.describe("mobile UX", () => { await expect(page.getByRole("button", { name: "Back to unified" })).toBeVisible(); }); - test("surfaces the try-it action before supporting links on phones", async ({ page }) => { + test("surfaces samples before inspector on phones", async ({ page }) => { await waitForViewerState(page, "empty"); - const tryItBox = await page.locator(".home-hero-callouts .hero-link-card.is-static").boundingBox(); - const sourceBox = await page.getByRole("link", { name: /Browse the GitHub repo/i }).boundingBox(); - const samplesBox = await page.locator(".home-samples-panel").boundingBox(); - const inspectorBox = await page.locator(".home-inspector-panel").boundingBox(); + const samplesBox = await page.locator(".home-samples-section").boundingBox(); + const inspectorBox = await page.locator(".home-inspector-section").boundingBox(); - expect(tryItBox?.y ?? Number.POSITIVE_INFINITY).toBeLessThan(sourceBox?.y ?? 0); expect(samplesBox?.y ?? Number.POSITIVE_INFINITY).toBeLessThan(inspectorBox?.y ?? 0); await expect(page.getByRole("link", { name: /Maintainer kickoff/i })).toBeVisible(); }); - test("keeps homepage and artifact metadata in compact two-column grids", async ({ page }) => { - await waitForViewerState(page, "empty"); - - const homeMetrics = page.locator(".home-inspector-panel .metric-grid"); - await expect.poll(() => homeMetrics.evaluate((element) => window.getComputedStyle(element).gridTemplateColumns.split(" ").length)).toBe(2); - + test("keeps artifact metadata in compact two-column grids", async ({ page }) => { await goToHash(page, getFragmentHash("arx showcase")); await waitForViewerState(page, "artifact"); @@ -138,7 +130,7 @@ test.describe("mobile UX", () => { test("renders compact CSV payloads without giant whitespace", async ({ page }) => { await goToHash(page, getFragmentHash("Data export preview")); await waitForViewerState(page, "artifact"); - const frame = await page.locator(".viewer-frame-hero").boundingBox(); + const frame = await page.locator(".viewer-frame-primary").boundingBox(); expect(frame?.height ?? 0).toBeLessThan(900); await expect(page.locator("table.csv-table")).toBeVisible(); }); @@ -183,7 +175,7 @@ test("download action emits a file", async ({ page }) => { await waitForViewerState(page, "artifact"); const [download] = await Promise.all([ page.waitForEvent("download"), - page.getByRole("button", { name: "Download" }).click(), + page.getByRole("button", { name: "Download" }).first().click(), ]); await expect(download.suggestedFilename()).toContain("viewer-shell.tsx"); });