diff --git a/CHANGELOG.md b/CHANGELOG.md index 1f8e422..922d339 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,11 +8,21 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/). ### Added +- Web UI redesign: a "survey sheet" visual language with a basalt/chalk/iron palette, Bricolage Grotesque display type with IBM Plex body and mono, square corners and hairline rules +- Strata column: every wizard step shows the project as layered rock, one layer per feature with thickness proportional to its share of files, updating live as features are toggled + - Machine-readable error codes on every ProblemDetails response: `ErrorMessages` entries are now `Error` records (stable snake_case `code` + message), `Result.Failure()` takes an `Error`, and `ProblemFactory` writes the code to the `code` extension (framework-generated bodies get `validation_failed` or a snake_case reason phrase such as `not_found`). New `ProblemDetailsSchemaTransformer` documents the extension in OpenAPI, and `ErrorMessagesTests` enforce code naming and uniqueness - Hangfire-backed email delivery: with both `email` and `jobs` enabled, transactional emails are queued as `EmailDeliveryJob` via `BackgroundEmailService` and retried automatically (5 attempts, 30s to 1h backoff) instead of being lost on SMTP failure. Without `jobs`, `SmtpEmailService` still sends inline - New API tests (`ProblemFactoryTests`, `ProblemDetailsCodeTests`, `ProblemDetailsAssert` fixture) and component tests for the email job pipeline - Frontend: `getErrorCode()` and `getErrorMessage(error, fallback, messagesByCode)` translate by backend code; login form and OAuth callback page map on codes instead of English `detail` text +### Changed + +- Header is a title block with a strata mark and revision number; the wizard progress is a numbered sheet index (01 Name, 02 Stack, 03 Download) +- Landing page leads with a headline and the project name field beside the strata column; FAQ, Why, and Changelog pages restyled to match +- Dependency graph, presets, architecture selector, feature cards, review stats, and file tree restyled; glow filters, traveling particles, and the typewriter effect removed +- Changelog page renders inline code in entries + ### Fixed - Frontend API proxy buffers request bodies so backend 401 responses pass through instead of surfacing as 502 on Node 24 (undici streaming issue) diff --git a/CLAUDE.md b/CLAUDE.md index 44609e5..d46a50f 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -91,12 +91,15 @@ All changes to `packages/web` must follow these rules. Do not ask - just enforce ### Visual Standards -- Dark theme only - all colors from `app.css` theme variables +- Visual language is a geological survey sheet: basalt background, chalk text, one warm iron accent, square corners (no `rounded-*`), hairline borders, no glow/blur/gradient blobs +- Dark theme only - all colors from `app.css` theme variables; strata tones (`--color-strata-*`) are reserved for the `StrataColumn` legend and architecture diagrams +- Type: `.display` / `.display-md` (Bricolage Grotesque) for headlines and big numbers, IBM Plex Sans for body, `font-mono` (IBM Plex Mono) and `.label` for labels, stats, and technical text +- The `StrataColumn` (project layers by file share) is the signature element - it appears on every wizard step and must stay in sync with the resolved features - No white flash - inline `background` on `` for instant dark paint -- No browser default focus rings - use custom `focus-visible` styles with accent color -- Monospace font (`font-mono`) for labels, stats, and technical text +- No browser default focus rings - global `focus-visible` outline in accent color - Touch-friendly spacing on mobile, compact on desktop - Tooltips only on hover-capable devices (`@media (hover: hover)`) +- Respect `prefers-reduced-motion` (global rule in `app.css`) ### Anime.js diff --git a/packages/web/src/app.css b/packages/web/src/app.css index 97e33d3..49d2924 100644 --- a/packages/web/src/app.css +++ b/packages/web/src/app.css @@ -1,28 +1,41 @@ @import 'tailwindcss'; @theme { - --font-sans: 'Inter', system-ui, sans-serif; - --font-mono: 'JetBrains Mono', 'Fira Code', ui-monospace, monospace; + --font-sans: 'IBM Plex Sans', system-ui, sans-serif; + --font-mono: 'IBM Plex Mono', ui-monospace, monospace; + --font-display: 'Bricolage Grotesque', 'IBM Plex Sans', system-ui, sans-serif; - --color-bg: #08080d; - --color-surface: #111118; - --color-surface-raised: #191922; - --color-border-subtle: #222233; - --color-border-active: #334155; + /* Basalt, shale, chalk - a survey sheet read at night */ + --color-bg: #121416; + --color-surface: #191c1f; + --color-surface-raised: #212529; + --color-border-subtle: #2a2f35; + --color-border-active: #454c55; - --color-text-primary: #ececf1; - --color-text-secondary: #8e8ea0; - --color-text-muted: #555566; + --color-text-primary: #ebe6da; + --color-text-secondary: #a39d90; + --color-text-muted: #6d6860; - --color-accent: #06b6d4; - --color-accent-light: #22d3ee; - --color-accent-dim: #06b6d415; + /* Iron oxide - the one warm accent */ + --color-accent: #e08a3c; + --color-accent-light: #f0a55c; + --color-accent-dim: #e08a3c1f; - --color-emerald: #10b981; - --color-emerald-dim: #10b98115; + /* Lichen - success */ + --color-emerald: #8fb573; + --color-emerald-dim: #8fb5731a; - --color-amber: #f59e0b; - --color-amber-dim: #f59e0b14; + /* Sulfur - caution */ + --color-amber: #d9b44a; + --color-amber-dim: #d9b44a17; + + /* Strata tones - one per feature group, used only in the column and legend */ + --color-strata-core: #7d8590; + --color-strata-auth: #c39a5b; + --color-strata-infra: #6f8ea3; + --color-strata-addon: #b3746a; + --color-strata-tooling: #8fa37a; + --color-strata-frontend: #d8d1c2; } html { @@ -38,38 +51,102 @@ body { } ::selection { - background-color: #06b6d459; + background-color: #e08a3c66; } -/* Typewriter animation */ -@keyframes typewriter { - from { - max-width: 0; +@layer components { + /* Display type: condensed, heavy, tight */ + .display { + font-family: var(--font-display); + font-variation-settings: + 'opsz' 96, + 'wdth' 85; + font-weight: 800; + letter-spacing: -0.02em; + line-height: 0.95; + } + + .display-md { + font-family: var(--font-display); + font-variation-settings: + 'opsz' 48, + 'wdth' 90; + font-weight: 700; + letter-spacing: -0.015em; + line-height: 1.05; } - to { - max-width: 8ch; + + /* Survey-sheet label: small caps mono */ + .label { + font-family: var(--font-mono); + font-size: 10px; + letter-spacing: 0.14em; + text-transform: uppercase; + color: var(--color-text-muted); } } -@keyframes blink { - 0%, - 100% { - opacity: 1; +/* Hatch fills for strata layers */ +.hatch-solid { + background-color: var(--strata); +} +.hatch-diag { + background-color: color-mix(in srgb, var(--strata) 55%, var(--color-bg)); + background-image: repeating-linear-gradient(135deg, var(--strata) 0 1.5px, transparent 1.5px 7px); +} +.hatch-cross { + background-color: color-mix(in srgb, var(--strata) 45%, var(--color-bg)); + background-image: + repeating-linear-gradient(45deg, var(--strata) 0 1px, transparent 1px 8px), + repeating-linear-gradient(135deg, var(--strata) 0 1px, transparent 1px 8px); +} +.hatch-dots { + background-color: color-mix(in srgb, var(--strata) 40%, var(--color-bg)); + background-image: radial-gradient(var(--strata) 1.1px, transparent 1.2px); + background-size: 7px 7px; +} +.hatch-brick { + background-color: color-mix(in srgb, var(--strata) 50%, var(--color-bg)); + background-image: + repeating-linear-gradient(0deg, var(--strata) 0 1px, transparent 1px 8px), + repeating-linear-gradient(90deg, var(--strata) 0 1px, transparent 1px 16px); +} +.hatch-wave { + background-color: color-mix(in srgb, var(--strata) 60%, var(--color-bg)); + background-image: repeating-linear-gradient(0deg, var(--strata) 0 1px, transparent 1px 5px); +} + +@layer components { + .btn-primary { + display: inline-flex; + min-height: 44px; + align-items: center; + gap: 0.5rem; + background: var(--color-accent); + color: var(--color-bg); + padding: 0.625rem 1.5rem; + font-family: var(--font-mono); + font-size: 0.875rem; + font-weight: 500; + transition: + background-color 0.15s, + opacity 0.15s; + } + .btn-primary:hover:not(:disabled) { + background: var(--color-accent-light); } - 50% { - opacity: 0; + .btn-primary:disabled { + opacity: 0.4; } } -.typewriter { - display: inline-flex; - overflow: hidden; - white-space: nowrap; - animation: typewriter 1s steps(7) 0.3s both; +/* Focus ring - iron, offset, no browser default */ +:where(button, a, summary, [tabindex]):focus-visible { + outline: 2px solid var(--color-accent); + outline-offset: 2px; } - -.cursor-blink { - animation: blink 1s step-end infinite; +:where(button, a, summary, [tabindex]):focus:not(:focus-visible) { + outline: none; } /* Hide scrollbar for horizontal nav strips */ @@ -92,3 +169,13 @@ body { background: var(--color-border-subtle); border-radius: 3px; } + +@media (prefers-reduced-motion: reduce) { + *, + *::before, + *::after { + animation-duration: 0.01ms !important; + animation-iteration-count: 1 !important; + transition-duration: 0.01ms !important; + } +} diff --git a/packages/web/src/app.html b/packages/web/src/app.html index 67d7717..d19df5e 100644 --- a/packages/web/src/app.html +++ b/packages/web/src/app.html @@ -1,5 +1,5 @@ - + @@ -7,7 +7,7 @@ name="description" content="Generate .NET 10 API projects with auth, email, jobs, file storage, and more. Pick features, download a complete solution that builds and tests pass." /> - + diff --git a/packages/web/src/lib/components/DependencyGraph.svelte b/packages/web/src/lib/components/DependencyGraph.svelte index fb4357b..2fafe85 100644 --- a/packages/web/src/lib/components/DependencyGraph.svelte +++ b/packages/web/src/lib/components/DependencyGraph.svelte @@ -6,7 +6,6 @@ const W = 88; const H = 30; - const R = 6; const HIT_PAD = 16; interface GNode { @@ -192,11 +191,9 @@ }); -
+
- - {generator.isFrontendEnabled ? 'Full stack' : 'API only'} - backend dependencies - + Backend dependencies click nodes to toggle
- + - - - - - - - - - - + - + required - + features - + extensions @@ -241,19 +229,11 @@ class="gedge" d={d} fill="none" - stroke={active ? '#06b6d4' : '#222233'} + stroke={active ? 'var(--color-accent)' : 'var(--color-border-active)'} stroke-width={active ? 1.2 : 0.6} - opacity={active ? 0.7 : 0.25} + opacity={active ? 0.8 : 0.4} style="transition: stroke 0.3s, stroke-width 0.3s, opacity 0.3s;" /> - {#if active} - - - - - - - {/if} {/each} @@ -277,7 +257,6 @@ y={node.cy - H / 2 - HIT_PAD} width={W + HIT_PAD * 2} height={H + HIT_PAD * 2} - rx={R + 2} fill="transparent" /> {/if} @@ -287,11 +266,10 @@ y={node.cy - H / 2} width={W} height={H} - rx={R} - fill={on ? '#06b6d415' : '#191922'} - stroke={on ? '#06b6d4' : '#222233'} - stroke-width={on ? 1.5 : 0.8} - filter={on ? 'url(#glow)' : 'none'} + rx="1" + fill={on ? 'var(--color-accent-dim)' : 'var(--color-surface)'} + stroke={on ? 'var(--color-accent)' : 'var(--color-border-subtle)'} + stroke-width={on ? 1.2 : 0.8} style="transition: fill 0.3s, stroke 0.3s, stroke-width 0.3s;" /> {#if isRequired} - - - + {/if} {/each} @@ -325,10 +296,10 @@ {#if hoveredNode && tips[hoveredNode]}
- + {nodes.find((n) => n.id === hoveredNode)?.label} - @@ -338,7 +309,7 @@