-
Notifications
You must be signed in to change notification settings - Fork 0
fix(theme): dark mode goes back to the stock neutral ground and text #239
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,57 @@ | ||
| import { describe, expect, test } from "bun:test" | ||
| import { shouldRenderRail } from "./thought-rail" | ||
|
|
||
| // The rail's grammar, stated as tests. A step is "running" only when it is the | ||
| // TAIL of a turn that is still working; everything above it has by definition | ||
| // been succeeded. That is what guarantees a hollow dot can never sit above a | ||
| // filled one, however the underlying tools complete. | ||
| const railState = (row: { previousAssistantPart: boolean; lastAssistantPart: boolean; turnRunning: boolean }) => ({ | ||
| render: shouldRenderRail(row), | ||
| first: !row.previousAssistantPart, | ||
| last: row.lastAssistantPart, | ||
| running: row.lastAssistantPart && row.turnRunning, | ||
| }) | ||
|
|
||
| /** Build the rows a turn of `n` steps produces, mirroring rows.ts. */ | ||
| const turn = (n: number, running: boolean) => | ||
| Array.from({ length: n }, (_, i) => | ||
| railState({ previousAssistantPart: i > 0, lastAssistantPart: i === n - 1, turnRunning: running }), | ||
| ) | ||
|
|
||
| describe("thought rail", () => { | ||
| test("a single-step turn draws no rail — one dot is decoration, not a sequence", () => { | ||
| expect(turn(1, false)[0].render).toBe(false) | ||
| expect(turn(1, true)[0].render).toBe(false) | ||
| }) | ||
|
|
||
| test("a multi-step turn draws a rail on every step", () => { | ||
| expect(turn(4, false).every((s) => s.render)).toBe(true) | ||
| }) | ||
|
|
||
| test("exactly one dot is running, and it is the tail", () => { | ||
| const steps = turn(5, true) | ||
| const running = steps.filter((s) => s.running) | ||
| expect(running).toHaveLength(1) | ||
| expect(steps[steps.length - 1].running).toBe(true) | ||
| }) | ||
|
|
||
| test("no dot is running once the turn finishes — the tail fills too", () => { | ||
| expect(turn(5, false).some((s) => s.running)).toBe(false) | ||
| }) | ||
|
|
||
| test("a hollow dot never sits above a filled one, at any length", () => { | ||
| for (const n of [2, 3, 7, 20]) { | ||
| const steps = turn(n, true) | ||
| const firstRunning = steps.findIndex((s) => s.running) | ||
| // everything after the running step must not exist; it is the tail | ||
| expect(firstRunning).toBe(n - 1) | ||
| expect(steps.slice(0, firstRunning).some((s) => s.running)).toBe(false) | ||
| } | ||
| }) | ||
|
|
||
| test("first and last are flagged so the line does not overshoot either end", () => { | ||
| const steps = turn(3, false) | ||
| expect(steps.map((s) => s.first)).toEqual([true, false, false]) | ||
| expect(steps.map((s) => s.last)).toEqual([false, false, true]) | ||
| }) | ||
| }) | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,88 @@ | ||
| // AMICODE: the thought rail — a vertical spine down a turn's assistant steps, | ||
| // ported from the website's /amicode animation (harmoniqs-ai | ||
| // app/components/demo/parts.jsx, `Step`). | ||
| // | ||
| // TWO THINGS THAT LOOK LIKE MISTAKES AND ARE NOT: | ||
| // | ||
| // 1. The rail is drawn as a PER-ROW SEGMENT, not as a border-left on a shared | ||
| // container. It has to be. The timeline is virtualised (@tanstack/solid-virtual): | ||
| // every row is an absolutely-positioned box and consecutive rows share no | ||
| // ancestor except the total-height spacer, so a container spine is structurally | ||
| // impossible here. The site independently arrived at the same per-row approach, | ||
| // which is why the port is cheap. Segments meet because each row already owns a | ||
| // 12px `pt-3` gap that the segment spans. | ||
| // | ||
| // 2. "done" is decided by ADJACENCY — a step is finished once a successor exists — | ||
| // not by that step's own tool lifecycle. Tools can complete out of order or run | ||
| // in parallel, so asking each row "are you finished?" would let a filled dot sit | ||
| // above a hollow one and destroy the rail's grammar. Adjacency makes the | ||
| // sequence monotonic by construction. It is also exactly what the website does: | ||
| // a step flips filled no later than the moment the next one appears. | ||
| // | ||
| // Hollow therefore means RUNNING (the tail of a turn still in flight), never | ||
| // "planned". The website does not preview future steps either — its scenes gate | ||
| // every entry on `t >= from`, so an unstarted step is never in the DOM. Showing | ||
| // the path ahead needs a real plan source (score stages), which is a later step. | ||
|
|
||
| import { Show } from "solid-js" | ||
|
|
||
| const NODE = 7 // dot diameter, px — matches the site's Step | ||
| const DOT_TOP = 7.5 // px from the row's top edge to the dot's top | ||
|
|
||
| export function ThoughtRail(props: { | ||
| /** first step of the turn — the line must not run above the dot */ | ||
| first: boolean | ||
| /** last step of the turn — the line must not run below the dot */ | ||
| last: boolean | ||
| /** the turn is still working, so this tail step is in flight */ | ||
| running: boolean | ||
| }) { | ||
| // Only the tail of a still-running turn is hollow. Everything above it has, | ||
| // by definition, been succeeded. | ||
| const isRunning = () => props.last && props.running | ||
| return ( | ||
| <> | ||
| <span | ||
| aria-hidden="true" | ||
| data-slot="thought-rail-line" | ||
| class="pointer-events-none absolute left-[3px] w-px bg-v2-border-border-base" | ||
| style={ | ||
| props.last | ||
| ? // the tail: draw only down to the dot, never past it | ||
| { top: "0px", height: props.first ? "0px" : `${DOT_TOP + NODE / 2}px` } | ||
| : // mid-run: span the row, starting below the dot on the very first step | ||
| { top: props.first ? `${DOT_TOP + NODE / 2}px` : "0px", bottom: "0px" } | ||
| } | ||
|
Comment on lines
+49
to
+55
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win Extend non-first rail segments through the row gap. Line 54 starts each non-first segment at the nested Include the preceding row gap in non-first segment geometry, including the tail height, or position the rail in the padded outer frame. 🤖 Prompt for AI Agents |
||
| /> | ||
| <span | ||
| aria-hidden="true" | ||
| data-slot="thought-rail-dot" | ||
| data-state={isRunning() ? "running" : "done"} | ||
| classList={{ | ||
| "pointer-events-none absolute left-0 rounded-full": true, | ||
| // hollow + pulsing while in flight, solid once succeeded | ||
| "thought-rail-dot--running": isRunning(), | ||
| }} | ||
| style={{ | ||
| top: `${DOT_TOP}px`, | ||
| width: `${NODE}px`, | ||
| height: `${NODE}px`, | ||
| border: "1px solid var(--v2-border-border-strong)", | ||
| background: isRunning() ? "var(--v2-background-bg-base)" : "var(--v2-border-border-strong)", | ||
| }} | ||
| /> | ||
| </> | ||
| ) | ||
| } | ||
|
|
||
| /** The gutter a rail occupies, so content clears it. */ | ||
| export const THOUGHT_RAIL_INSET = "pl-4" | ||
|
|
||
| /** | ||
| * A lone step is not a sequence: one dot on its own reads as decoration rather | ||
| * than as a rail, so a single-part turn gets nothing. | ||
| */ | ||
| export function shouldRenderRail(input: { previousAssistantPart: boolean; lastAssistantPart: boolean }) { | ||
| const isOnlyStep = !input.previousAssistantPart && input.lastAssistantPart | ||
| return !isOnlyStep | ||
| } | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
📐 Maintainability & Code Quality | 🟠 Major | ⚡ Quick win
Test
constructMessageRowsinstead of a duplicate row model.railStateandturnreimplement the state derivation fromrows.tsandThoughtRail. A defect in the production derivation can leave these tests passing.Build assistant-message fixtures, call
Timeline.constructMessageRows, and assert the producedAssistantPartflags. Keep directshouldRenderRailtests only for its own predicate behavior.As per coding guidelines,
**/*.{test,spec}.{ts,tsx}: Test actual implementation, do not duplicate logic into tests.🤖 Prompt for AI Agents
Source: Coding guidelines