diff --git a/CHANGELOG.md b/CHANGELOG.md index 15c5fe63..123ddb4a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,11 +1,14 @@ # Changelog -## 0.0.11-beta.3 — 2026-06-08 +## 0.0.11-beta.3 — 2026-06-09 ### Dependencies - Swap the Vitest DOM environment from `happy-dom` to `jsdom` (`vitest.config.mts`, `package.json`, 15 `docs/*/testing.mdx`). happy-dom is single-maintainer and had a 2024 critical CVE; jsdom has 6 maintainers, ~7× the weekly downloads, and a perfect Snyk maintenance score. Test suite (1691 tests across 82 files) stays green on jsdom (#419). ### Features +- Make the dashboard chrome scale to fill ultrawide monitors. `.report` in `app/globals.css` swaps the fixed `max-width: 1380px; padding: 0 40px` for `max-width: clamp(720px, 96vw, 1840px); padding: 0 clamp(20px, 3vw, 56px)` — on a 2400px viewport the content shell now occupies ~1840px (~530px of empty side margin → ~280px) without letting tabular line measure get unreadably long on 4K. The audit-page override in `app/audit/audit-styles.css` matches, narrower: `clamp(720px, 92vw, 1480px)` so the archetype hero stays composed. `.archetype-frame` itself gets a `max-width: 1320px` cap with `margin: 0 auto` so the giant Bitcount headline + pink-shadowed border don't stretch past their compositional break-point on huge screens. Prose blocks (`.arch-desc`, `.arch-tagline` at 580px max-width) and side gutters keep readability tight at every step of the clamp. +- Subtle polish pass across all 5 pages — same brutalist pixel-craft aesthetic, more carefully made. `app/globals.css` gets a global `::selection` pink wash, an opt-in `:focus-visible` ring system that fires keyboard-only on every interactive element (`a`, `button`, `input`, `.btn`, `.tab`, `.btn-press`), and the existing `.btn` / `.btn-press` / `.tab` / `.panel` transitions are repointed at the `cubic-bezier(0.22, 1, 0.36, 1)` curve already used by `.audit-bar-fill` (away from `transition: all 120ms ease`, which was animating layout properties unintentionally). New: a `.btn:active` press-down, a `.btn-press:active` collapse, a `.tab::after` underline that emerges from center on hover for inactive tabs, and an opt-in `.panel.is-interactive` hover that grows the pink corner brackets from 10px → 16px. `app/projects/page.tsx` gains a section-mast row with the `━━ projects` glyph + folder counter, swaps "Projects" for the lowercased "your agent footprint." headline, and the empty state now renders a 6×6 pixel-grid "no projects" sigil with a 4px hard-offset pink shadow. `app/projects/loading.tsx` staggers its 8 skeleton rows with the same `audit-row-enter` keyframe used by the audit findings table and adds an `aria-busy` "loading…" pip in the mast. `app/project/[name]/page.tsx` migrates from the old shadcn-style `container mx-auto bg-card rounded-lg` chrome to the unified `.report` + `.section` + `.panel` shell — `` becomes a `.btn` with the same `━━` glyph, the path / modified pair is a tight green-eyebrow `
` grid, and the sessions block gets its own section-label mast. `components/navbar.tsx` tightens the slipping-through badge aria (pluralised, `role="status"`, native title tooltip). All new motion respects `prefers-reduced-motion: reduce` — `globals.css` and `audit-styles.css` each got a guard that stills the new tab underline, panel corner growth, button micro-motion, the audit terminal cursor blink, spinner step, marquee shine, and identity dot pulse for vestibular-sensitive users. + - Drop the standalone pixel icon from the top navbar (`components/navbar.tsx`) — the brand cluster is now wordmark-only. Logo resolution is also reworked: a new `useBrandLogo` hook attempts a runtime `fetch` of the remote brand URL on mount, blob-wraps the response into an object URL on success, and falls back to the bundled `/logo.svg` (served from `public/`, mirrored at `assets/logos/company/logo.svg`) on any error/non-OK status. The local fallback is also rendered as the initial state so SSR + pre-fetch frames show the brand without a flash. - Swap the display font across the app from `Architype Stedelijk` to `Bitcount Prop Single` (the wordmark treatment used on befailproof.ai). Replaces both font binaries — `public/audit/fonts/architype-stedelijk.{woff2,ttf}` and `assets/audit/assets/fonts/architype-stedelijk.{woff2,ttf}` — with the single self-hosted static instance `bitcount-prop-single.woff2` (wght 417 + ELSH 55 baked in, so no `font-variation-settings` plumbing is needed). Both `@font-face` blocks (`app/globals.css`, `assets/audit/styles.css`) and both `--font-display` declarations are updated; the CSS variable name and fallback stack (`"VT323", "JetBrains Mono", monospace`) stay unchanged so every consumer of `var(--font-display)` picks up the new face with no further edits. Stale Architype references in the comment headers of `components/navbar.tsx`, `app/audit/_components/empty-state.tsx`, and `app/audit/_components/show-off-cta.tsx` are renamed to match. - Pin the `/audit` report footer to the viewport bottom on the empty / running states. `ReportFooter` (`app/audit/_components/report-footer.tsx`) gains an optional `fixed` prop that adds a `report-footer--fixed` class (`position: fixed; left/right/bottom: 0; padding: 16px 32px; z-index: 10`, defined in `app/audit/audit-styles.css`); `ShellEmpty` in `audit-dashboard.tsx` passes it so the pre-run `EmptyState` and the `RunProgress` view — both short pages where the in-flow footer was floating mid-viewport and scrolling with the page — get a sticky status-bar style footer. The post-run dashboard mount is left unchanged because its long content places the footer at the document end naturally. diff --git a/app/audit/audit-styles.css b/app/audit/audit-styles.css index 7041c893..9534734c 100644 --- a/app/audit/audit-styles.css +++ b/app/audit/audit-styles.css @@ -192,10 +192,18 @@ /* ───────────────────────── audit page shell ───────────────────────── */ +/* the audit page intentionally runs narrower than the global .report — the + archetype hero, findings cards, and score-share card are tuned for a + ~1400px-max layout where the headline line measure stays "billboard" + tight. On a 2400px+ monitor we still want to break out of the 1180px + feel that used to leave too much black on both flanks; clamp up to + 1480px and let `.archetype-frame` cap itself further so the hero doesn't + stretch past its compositional break-point. */ .report { - max-width: 1180px; + width: 100%; + max-width: clamp(720px, 92vw, 1480px); margin: 0 auto; - padding: 0 32px; + padding: 0 clamp(20px, 3vw, 48px); } .section { @@ -242,6 +250,11 @@ .archetype-frame { position: relative; + /* hero composition was tuned for ~1180px max — let it grow a little but + don't let the giant Bitcount name turn into a billboard on 4K */ + max-width: 1320px; + margin-left: auto; + margin-right: auto; border: 1px solid var(--line-2); background: repeating-linear-gradient(0deg, rgba(228,88,125,0.025) 0 1px, transparent 1px 16px), @@ -295,6 +308,9 @@ 0%, 100% { opacity: 1; transform: scale(1); } 50% { opacity: 0.4; transform: scale(0.85); } } +@media (prefers-reduced-motion: reduce) { + .arch-target .dot-live { animation: none; } +} .arch-counter { font-family: var(--font-mono); font-size: 11px; letter-spacing: 0.18em; text-transform: uppercase; color: var(--dim); @@ -1724,3 +1740,17 @@ .strength-metric { grid-column: 2; text-align: left; margin-top: 6px; } .return-hook { padding: 28px 24px; } } + +/* reduced-motion: silence the ambient terminal motion (blinking cursor, + spinner, marquee shine, status pulse) so vestibular-sensitive users get a + still UI. Functional progress (.running-bar-fill width) still updates; + only the decorative loops are stopped. */ +@media (prefers-reduced-motion: reduce) { + .running-cursor, + .running-stage-spin, + .running-bar-fill::after, + .auth-status-pill .dot { + animation: none; + } + .running-bar-fill { transition: none; } +} diff --git a/app/globals.css b/app/globals.css index ba5b9139..feced63a 100644 --- a/app/globals.css +++ b/app/globals.css @@ -155,6 +155,35 @@ select { color-scheme: dark; } select option { background-color: var(--bg-2); color: var(--ink); } input[type="date"] { color-scheme: dark; } +/* selection — pink wash ties prose snippets and code-like fragments to the + accent without redesigning anything; used app-wide. */ +::selection { + background: var(--accent-pink-bg); + color: var(--accent-light); +} +::-moz-selection { + background: var(--accent-pink-bg); + color: var(--accent-light); +} + +/* focus-visible — keyboard users get a clear pink ring on every interactive + element. Pointer users see nothing different (`:focus-visible` only fires + on keyboard-driven focus). Bracketed outline + outline-offset matches the + pixel-craft chrome (no rounded glow). */ +:where(a, button, [role="button"], input, textarea, select, summary, [tabindex]):focus-visible { + outline: 1px solid var(--accent-pink); + outline-offset: 2px; +} +.btn:focus-visible, +.tab:focus-visible { + outline: 1px solid var(--accent-pink); + outline-offset: 3px; +} +.btn-press:focus-visible { + outline: 1px solid var(--accent-pink); + outline-offset: 4px; +} + @theme inline { --color-background: var(--background); --color-foreground: var(--foreground); @@ -250,16 +279,32 @@ input[type="date"] { color-scheme: dark; } padding: 7px 12px; font-family: var(--font-mono); font-size: 12px; letter-spacing: 0.04em; border: 1px solid var(--line-2); background: transparent; color: var(--ink); - transition: all 120ms ease; white-space: nowrap; + /* limit transitioned props so width/height/font don't animate, and use the + same cubic-bezier curve as .audit-bar-fill for a coherent motion feel */ + transition: + color 140ms cubic-bezier(0.22, 1, 0.36, 1), + background-color 140ms cubic-bezier(0.22, 1, 0.36, 1), + border-color 140ms cubic-bezier(0.22, 1, 0.36, 1), + box-shadow 140ms cubic-bezier(0.22, 1, 0.36, 1), + transform 140ms cubic-bezier(0.22, 1, 0.36, 1); + white-space: nowrap; } .btn:hover { border-color: var(--ink); background: rgba(255,255,255,0.03); } +.btn:active { transform: translateY(1px); } +.btn[aria-disabled="true"], .btn:disabled { opacity: 0.5; cursor: not-allowed; } +.btn[aria-disabled="true"]:hover, .btn:disabled:hover { border-color: var(--line-2); background: transparent; } .btn-primary { border-color: var(--accent-pink); color: var(--accent-pink); } .btn-primary:hover { background: var(--accent-pink); color: var(--bg); } .btn-press { box-shadow: 4px 4px 0 0 var(--accent-pink-shadow); - transition: box-shadow 120ms, transform 120ms; + transition: + box-shadow 140ms cubic-bezier(0.22, 1, 0.36, 1), + transform 140ms cubic-bezier(0.22, 1, 0.36, 1), + background-color 140ms cubic-bezier(0.22, 1, 0.36, 1), + color 140ms cubic-bezier(0.22, 1, 0.36, 1); } .btn-press:hover { box-shadow: 2px 2px 0 0 var(--accent-pink-shadow); transform: translate(2px, 2px); } +.btn-press:active { box-shadow: 0 0 0 0 var(--accent-pink-shadow); transform: translate(4px, 4px); } /* primary tab strip — used by both the audit page and the policies/projects nav rows */ .tabs { @@ -269,26 +314,49 @@ input[type="date"] { color-scheme: dark; } } .tabs::-webkit-scrollbar { display: none; } .tab { + position: relative; display: inline-flex; align-items: center; gap: 8px; padding: 12px 16px; font-family: var(--font-mono); font-size: 12px; letter-spacing: 0.04em; color: var(--ink-2); border-bottom: 1px solid transparent; margin-bottom: -1px; - transition: color 120ms, border-color 120ms; white-space: nowrap; + transition: + color 160ms cubic-bezier(0.22, 1, 0.36, 1), + border-color 160ms cubic-bezier(0.22, 1, 0.36, 1); + white-space: nowrap; background: transparent; } .tab:hover { color: var(--ink); } +/* faint underline preview on hover — emerges from center on inactive tabs; + active state keeps the existing sharp full-width pink line. */ +.tab::after { + content: ""; + position: absolute; + left: 16px; right: 16px; bottom: -1px; + height: 1px; + background: var(--accent-pink); + transform: scaleX(0); + transform-origin: center; + opacity: 0.4; + transition: transform 220ms cubic-bezier(0.22, 1, 0.36, 1); +} +.tab:hover::after { transform: scaleX(1); } .tab.is-active { color: var(--accent-pink); border-bottom-color: var(--accent-pink); } +.tab.is-active::after { display: none; } /* ───────────────────────── canonical page chrome ───────────────────────── */ +/* the canonical content shell. Scales to fill ultrawide monitors without + collapsing on mobile or letting prose lines get unreadable. The cap at + 1840px keeps the line measure for tabular pages (policies activity, + sessions list) comfortably below "scan back to start of next row" length + on a 2560px display. Side gutter is clamped 20→56px so the chrome breathes + on big screens without crowding small ones. */ .report { - max-width: 1380px; + width: 100%; + max-width: clamp(720px, 96vw, 1840px); margin: 0 auto; - padding: 0 40px; -} -@media (max-width: 720px) { - .report { padding: 0 20px; } + padding: 0 clamp(20px, 3vw, 56px); } .section { @@ -347,6 +415,15 @@ input[type="date"] { color-scheme: dark; } border: 1px solid var(--line-2); background: var(--bg-2); padding: 28px; + transition: + border-color 200ms cubic-bezier(0.22, 1, 0.36, 1), + box-shadow 200ms cubic-bezier(0.22, 1, 0.36, 1); +} +.panel::before, +.panel::after { + transition: width 200ms cubic-bezier(0.22, 1, 0.36, 1), + height 200ms cubic-bezier(0.22, 1, 0.36, 1), + border-color 200ms cubic-bezier(0.22, 1, 0.36, 1); } .panel::before { content: ""; position: absolute; top: -1px; left: -1px; @@ -360,6 +437,13 @@ input[type="date"] { color-scheme: dark; } border-bottom: 1px solid var(--accent-pink); border-right: 1px solid var(--accent-pink); } +/* opt-in interactive flavour: hovering grows the corner brackets and softens + the panel border. Apply to clickable card-like panels (e.g. project rows). */ +.panel.is-interactive { cursor: pointer; } +.panel.is-interactive:hover { border-color: var(--line); } +.panel.is-interactive:hover::before, +.panel.is-interactive:hover::after { width: 16px; height: 16px; } +.panel.is-interactive:focus-within { border-color: var(--accent-pink-soft); } /* ───────────────────────── animations (preserved from previous globals) ───────────────────────── */ @@ -407,4 +491,15 @@ input[type="date"] { color-scheme: dark; } animation: none; opacity: 1; } + .section-h-dot { + animation: none; + } + .tab::after, + .panel, + .panel::before, + .panel::after, + .btn, + .btn-press { + transition: none; + } } diff --git a/app/project/[name]/page.tsx b/app/project/[name]/page.tsx index 0a1aa092..918c7fbd 100644 --- a/app/project/[name]/page.tsx +++ b/app/project/[name]/page.tsx @@ -13,7 +13,6 @@ import { notFound } from "next/navigation"; import { existsSync } from "fs"; import { stat } from "fs/promises"; import Link from "next/link"; -import { ArrowLeft } from "lucide-react"; import { formatDate } from "@/lib/format-date"; import SessionsList from "@/app/components/sessions-list"; @@ -115,50 +114,101 @@ export default async function ProjectPage({ params }: ProjectPageProps) { const displayPath = claudeExists && claudeProjectPath ? claudeProjectPath : canonicalRoot; return ( -
-
- - - Back to Projects - - -
-

- {canonicalRoot} -

-
-

- Path: {displayPath} -

- {lastModifiedFormatted && ( -

- Modified: {lastModifiedFormatted} -

- )} +
+
+
+ + ━━ + back to projects + +
+ {sessionFiles.length} session{sessionFiles.length === 1 ? "" : "s"}
- {/* Sessions Section */} -
-

Sessions

+

+ {canonicalRoot} +

- {sessionFiles.length === 0 ? ( -
-

- No .jsonl files found in this project. -

-

- Session files will appear here once they are created. -

-
- ) : ( - +
+
+ path +
+
{displayPath}
+ {lastModifiedFormatted && ( + <> +
+ modified +
+
{lastModifiedFormatted}
+ )} +
+ +
+
+ ━━ sessions +
-
+ + {sessionFiles.length === 0 ? ( +
+

+ no .jsonl files found in this project. +

+

+ session files will appear here once they are created. +

+
+ ) : ( +
+ +
+ )} +
); } diff --git a/app/projects/loading.tsx b/app/projects/loading.tsx index a8804495..4fea943b 100644 --- a/app/projects/loading.tsx +++ b/app/projects/loading.tsx @@ -1,18 +1,35 @@ /** Skeleton loading UI for the projects page — audit-styled to match - * the dashed `.panel` chrome of the loaded state. */ + * the dashed `.panel` chrome of the loaded state. Staggered fade-in on the + * rows gives the skeleton its own rhythm rather than every bar blinking in + * lockstep. */ export default function ProjectsLoading() { return (
+
+
+ ━━ projects +
+
+ loading… +
+

- Projects - + your agent footprint.

{Array.from({ length: 8 }).map((_, i) => ( -
+
))}
diff --git a/app/projects/page.tsx b/app/projects/page.tsx index 4e547ff2..ed46ea48 100644 --- a/app/projects/page.tsx +++ b/app/projects/page.tsx @@ -26,18 +26,53 @@ export default async function ProjectsPage() { return (
+
+
+ ━━ projects +
+
+ 0 ? "g" : "p"}>●{" "} + {count} {count === 1 ? "folder" : "folders"} +
+

- Projects - {/* */} + your agent footprint.

{count === 0 ? (
+

- no projects found in the .claude/projects directory. + no projects found in the .claude/projects directory.

- make sure the directory exists and contains project folders. + run an agent in any folder and it will show up here.

) : ( diff --git a/components/navbar.tsx b/components/navbar.tsx index 3d406af6..51332a85 100644 --- a/components/navbar.tsx +++ b/components/navbar.tsx @@ -114,6 +114,7 @@ export const Navbar: React.FC<{ {label} {showAuditBadge && ( {auditSlippingCount}