Skip to content

feat(web): Lazurio shell takes T3's sidebar colours - #38

Merged
agentrozjedemeai merged 1 commit into
mainfrom
agent/DEV-6639-shell-colour-roles
Oct 4, 2026
Merged

agentrozjedemeai merged 1 commit into
mainfrom
agent/DEV-6639-shell-colour-roles

Conversation

@immakermatty

@immakermatty immakermatty commented Oct 4, 2026 •

Copy link
Copy Markdown

Why

Root decision 0187 (2026-10-04, being written): the Lazurio shell's elements, <lazurio-rail>, <lazurio-column-head> and the Environment list, take the colours of the app they sit in through colour roles. These are CSS custom properties that the host sets on its document root, and they inherit into the shell's shadow roots. In Chat, that app is T3 Code. The rail and T3's sidebar beside it must read as one surface, with no line between them. For now each app keeps its own theme picker and palettes; a later mechanism will unify themes across apps and is not part of this PR. The Platform shell library is learning the roles in parallel (Lazurio/LazurioPlatform, branch agent/DEV-6639-shell-colour-roles). Until a Launchpad with that library is out, the roles are set but nothing reads them.

What changes

apps/web/index.html (already in allowed_upstream_changes):

  • Twelve roles from T3's own tokens. The roles are only references, with no copied colours, so they follow every T3 theme, light and dark, and a theme switch at runtime. Foregrounds and the border use the --contrast-* variants because those are what T3 actually paints with (text-sidebar-foreground → --contrast-sidebar-foreground), and they respect Settings → Appearance contrast.

    Role T3 token
    --lazurio-surface --sidebar
    --lazurio-ink --contrast-sidebar-foreground
    --lazurio-ink-muted --contrast-sidebar-muted-foreground
    --lazurio-line --contrast-sidebar-border
    --lazurio-line-strong color-mix(in oklab, var(--contrast-sidebar-foreground) 24%, var(--sidebar))
    --lazurio-hover --sidebar-row-hover
    --lazurio-selected --sidebar-row-selected
    --lazurio-control --sidebar-control-surface
    --lazurio-raised --sidebar-row-active
    --lazurio-overlay --popover
    --lazurio-overlay-ink --contrast-popover-foreground
    --lazurio-focus --ring

    These are the wireframe's mapping (HumanAndMachine-ai/prototypes-lazurio#13) with the fork's real token names. The --app-theme-* values feed these tokens when a theme is selected.

  • Declared on :root, [data-app-sidebar], and the rail carries data-app-sidebar. T3 gives its sidebar its own palette under [data-app-sidebar] and recomputes contrast there (index.css: "Recompute contrast wherever a subtree owns its own semantic color palette"). In the default dark theme, T3's sidebar is #000 while the document's --sidebar is color-mix(neutral-950 97%, white), about #111. Roles on :root alone would put a lighter rail beside a black sidebar. Declaring them in both scopes makes the column head resolve against the sidebar's values. Putting the rail in that scope does the same for the rail.

  • The rail wears T3's surface grain (--surface-grain, the same utility T3 uses on the sidebar and body). This is an outer-document rule on the host, so it overrides only background-image of the shell's :host background and keeps its colour. Without the grain the rail sits 1.2–3.3 of 255 brightness levels off the sidebar, exactly at the boundary column (measured below).

scripts/lazurio-release-contract.test.mjs: a new test checks:

  • the twelve mappings, and that there are exactly twelve;
  • the rail attribute and the grain rule;
  • that every token the roles name still exists in index.css, along with the [data-app-sidebar] palette block, its contrast recomputation and data-app-sidebar="" in AppSidebarLayout.tsx;
  • that no .ts/.tsx under apps/web/src other than AppSidebarLayout.tsx mentions data-app-sidebar. With the rail in that scope, a future upstream querySelector("[data-app-sidebar]") would find the rail first. My own browser probe hit exactly this, so a rebuild that introduces such a lookup should fail and get reviewed.

I checked that the test fails when a mapping is changed and when a source file starts querying the attribute. The existing slot test now expects <lazurio-rail data-app-sidebar>.

lazurio-fork-ci.yml: one comment line on the existing allowlist reason. No new allowlist entries.

docs/operations/lazurio-fork-release.md: the "Lazurio shell" section describes the colours, why the rail shares the sidebar scope, and the grain.

What does not change

  • T3 itself. No T3 colours, tokens, themes or branding change. T3's own border between its sidebar and the main area stays. The sidebar container has a border on its right side only; neither the sidebar's left edge nor #root has one.
  • Upstream look without the shell. The roles are inert custom properties, and the undefined <lazurio-rail> has no box: width 0, #root padding 0, sidebar at x=0. A pixel diff of the client without /.lazurio/shell.js, between this branch and main's index.html, found 0 differing pixels in default light and default dark (1280×800).
  • No TypeScript, no router or state changes, no new files under apps/.
  • The overlay table. It gets no new row. This commit belongs with feat(web): Lazurio shell slot and folds into it at the next rebuild on a new upstream tag. Say so if you'd rather list it separately.

Verification

CI steps run locally on this head (Node 24.16, pnpm 11.10):

  • vp fmt --check: clean (4182 files).
  • vp run --filter @t3tools/web typecheck and vp run --filter t3 typecheck: exit 0.
  • apps/web: vp test run src/lazurio: 10/10 pass.
  • node --test scripts/lazurio-release-contract.test.mjs: 14/14 pass.
  • The allowlist guard against v0.0.45 (6c8fed35): no unexpected upstream changes, no merge commits.
  • vp lint scripts/lazurio-release-contract.test.mjs: clean.
  • vp build of @t3tools/web: the built index.html keeps <lazurio-rail data-app-sidebar> and the minified role and grain rules.
  • apps/server: the config/auth/label tests pass (117/117). src/server.test.ts is flaky on this machine regardless of this change: on unmodified 1e4cf23d it went 1 pass and 2 fails (1–2 tests) over three runs; with this change, 1 pass and 2 fails (2–5 tests). The failures are a test HTTP client getting a plain ok from /api/auth/browser-session, most likely port contention with other local servers. This PR touches no server code, and main's CI is green. CI decides.
  • Not run locally: the Docker image build and the cli-archives job.

In a real browser (Chrome via Playwright): this branch's dev server (vp run dev, worktree-local state), with /.lazurio/shell.js answered by a stub. The stub's rail and column head paint themselves only with the roles, with a magenta fallback for any role that fails to resolve. Light and dark come from T3's own preference keys, set before each reload.

  • In each of default light, default dark, Grove light, Grove dark and T3 Chat dark, all 12 roles resolve inside the rail and inside the column head to exactly the same colour as the matching T3 token in the sidebar's scope. The rail's painted background equals the sidebar's (oklch(0.985 0 none), rgb(0, 0, 0), oklch(0.936464 0.014601 163.554), oklch(0.309925 0.032827 160.944), oklch(0.185778 0.019368 322.159)).
  • On documentElement the roles equal T3's tokens, too. The only difference is the default dark --lazurio-surface on the document, color(srgb 0.068 …) (the document's --sidebar), against the sidebar's #000. That gap is the reason for the shared scope.
  • Live switch without a reload. In T3's Settings → Appearance, "Use Grove theme" then "Use dark mode" (from default light) turns the rail and column head roles to Grove dark immediately. All 12 match the sidebar.
  • Seam. I measured the mean colour of every pixel column from x=56 to 88 across y=420–720. With the grain, the largest step between neighbouring columns is ≤0.2 of 255 in all five themes. Without the grain, it was 1.2–3.3, exactly at x=72, the rail/sidebar boundary.
  • Screenshots are kept locally, not attached (t3-roles-<theme>.png, …-seam.png, t3-roles-live-switch-grove-dark.png, t3-roles-no-shell-default-light.png).

Risks and follow-ups

  • The rail now matches [data-app-sidebar]. The rules that apply to it set only custom properties and, in themed mode, border-color. The current Platform rail has no border, so nothing shows. If the shell ever draws a rail border, T3's sidebar border colour would win over the shell's :host colour. The contract test covers script lookups (see above).
  • Nothing reads the roles yet. Until a Platform shell that uses them is released and served by the Environment's Launchpad, the shell keeps its own colours. This PR ships no release.

🤖 Generated with Claude Code

RetriggerConfidence Score: 5/5

The PR appears safe to merge; no actionable issue was established in the changed code.

Summary

The PR maps twelve Lazurio shell color roles to T3 tokens, places the rail in the sidebar palette scope, and applies T3’s surface grain to the rail.

  • Adds a rebuild contract test for the mappings, required tokens, and sidebar-scope usage.
  • Documents the color behavior and updates the existing allowlist explanation.

Diagram

%%{init: {'theme': 'neutral'}}%%
flowchart LR
  A[T3 theme and contrast tokens] --> B["Root and sidebar palette scopes"]
  B --> C["Lazurio color roles"]
  C --> D["Rail shadow content"]
  C --> E["Column head"]
  A --> F["Surface grain"]
  F --> D
Loading

Reviews (1) · Last reviewed commit: "feat(web): Lazurio shell takes T3's side..."

The Lazurio shell (rail, column head and Environment list) now paints
itself in the colours of the T3 theme it sits in, following root decision
0187: index.html sets the shell's twelve colour roles from T3's own
sidebar tokens, so the shell follows every T3 theme, light and dark, and
a theme switch at runtime.

- The roles name T3's tokens (--sidebar, the --contrast-* foregrounds and
  border T3 actually paints with, the row hover/selected/active and
  control surfaces, --popover and --ring); no colour is copied.
- They are declared on :root and [data-app-sidebar], where T3 recomputes
  its sidebar palette, and <lazurio-rail> carries data-app-sidebar. In
  the default dark theme T3's sidebar is #000 while the document's
  --sidebar is a shade lighter, so without the shared scope the rail
  would stand apart from the sidebar.
- The rail wears T3's surface grain; without it the rail sits 1-3 of 255
  brightness levels off the sidebar and the edge shows. T3's own border
  between its sidebar and the main area is unchanged.
- Without the shell nothing changes: the roles are inert custom
  properties and the undefined rail has no box.

The release contract test keeps the mapping, the tokens it names in
index.css, the sidebar palette scope, and checks that no web source but
AppSidebarLayout uses data-app-sidebar (a script lookup would now find
the rail first). The runbook describes the colours.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

@agentrozjedemeai agentrozjedemeai left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Exact-head QA on 3a76bc2. Reviewed the sidebar-scope token mapping, grain and upstream rebuild contract; node contract suite passes 14/14 locally and CI is green. Rebase merge preserves the distribution patch stack.

@agentrozjedemeai
agentrozjedemeai merged commit 47c8f97 into main Oct 4, 2026
6 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants