Files
lda-wf/docs/historical/superpowers/plans/2026-07-08-presentation-surface-theme-normalization.md
T

19 KiB

Presentation Surface Theme Normalization Implementation Plan

For agentic workers: REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (- [ ]) syntax for tracking.

Goal: Normalize /present chat and discussion/Q&A surfaces so light chat, direct discussion branches, and presenter notes share one editorial presentation vocabulary.

Architecture: Add a small semantic surface contract with data-presentation-surface="editorial|night" on presentation side-surfaces. Style OperatorChat and DiscussionPanel through that contract, keeping existing storyboard, layout, chat messages, and discussion routing intact. Do not introduce a new theme engine or component library in this slice.

Tech Stack: React, TypeScript, CSS, Vitest, Testing Library, Playwright CLI screenshot smoke.

Global Constraints

  • Follow design spec: docs/superpowers/specs/2026-07-08-presentation-surface-theme-normalization-design.md.
  • Do not change /console.
  • Do not replace chat UI.
  • Do not alter storyboard content or scene composition.
  • Do not add dependencies.
  • Keep CSS scoped to presentation surfaces.
  • Prefer semantic surface attributes over ad hoc color selectors.
  • Generated .visual-smoke/ screenshots must remain ignored.

File Structure

  • Modify web/apps/console/src/presentation/OperatorChat.tsx
    • Add data-presentation-surface.
  • Modify web/apps/console/src/presentation/OperatorChat.test.tsx
    • Pin editorial/night surface mapping.
  • Modify web/apps/console/src/presentation/DiscussionPanel.tsx
    • Add data-presentation-surface="editorial".
  • Modify web/apps/console/src/presentation/DiscussionPanel.test.tsx
    • Pin discussion panel surface contract for Q&A branches and legacy branches.
  • Modify web/apps/console/src/presentation/presentation.css
    • Normalize chat/discussion/presenter-note surfaces.
  • Modify docs/current_roadmap.md
    • Mark surface normalization complete after implementation.
  • Move this plan to docs/historical/superpowers/plans/2026-07-08-presentation-surface-theme-normalization.md after implementation.

Task 1: Add Semantic Surface Attributes

Files:

  • Modify: web/apps/console/src/presentation/OperatorChat.tsx
  • Modify: web/apps/console/src/presentation/OperatorChat.test.tsx
  • Modify: web/apps/console/src/presentation/DiscussionPanel.tsx
  • Modify: web/apps/console/src/presentation/DiscussionPanel.test.tsx

Interfaces:

  • Consumes:

    • compositionForState(state).chatTheme
    • DiscussionPanel branch ID.
  • Produces:

    • OperatorChat root has data-presentation-surface="editorial" when chatTheme === "light", otherwise "night".
    • DiscussionPanel root has data-presentation-surface="editorial".
  • Step 1: Add failing OperatorChat surface test

In web/apps/console/src/presentation/OperatorChat.test.tsx, add this test:

  it("maps light chat theme to the editorial presentation surface", () => {
    const state = {
      ...initialPresentationState,
      location: { kind: "main" as const, sceneId: "workflow-demo" as const, beatId: "graph", focusPath: [] },
    };

    render(<OperatorChat state={state} />);

    const chat = screen.getByLabelText("scripted operator chat");
    expect(chat).toHaveAttribute("data-chat-theme", "light");
    expect(chat).toHaveAttribute("data-presentation-surface", "editorial");
  });

Add a second test:

  it("maps dark chat theme to the night presentation surface", () => {
    render(<OperatorChat state={initialPresentationState} />);

    const chat = screen.getByLabelText("scripted operator chat");
    expect(chat).toHaveAttribute("data-presentation-surface", "night");
  });
  • Step 2: Add failing DiscussionPanel surface tests

In web/apps/console/src/presentation/DiscussionPanel.test.tsx, add:

  it("renders Q&A branches on the editorial presentation surface", () => {
    render(<DiscussionPanel branchId="where-is-ai-agent" onClose={onClose} />);

    expect(screen.getByRole("dialog")).toHaveAttribute("data-presentation-surface", "editorial");
  });

  it("renders legacy discussion branches on the same editorial surface", () => {
    render(<DiscussionPanel branchId="hosted-automation" onClose={onClose} />);

    expect(screen.getByRole("dialog")).toHaveAttribute("data-presentation-surface", "editorial");
  });
  • Step 3: Run failing tests

Run:

pnpm --dir web --filter @lda/console test -- src/presentation/OperatorChat.test.tsx src/presentation/DiscussionPanel.test.tsx

Expected: FAIL because data-presentation-surface is not emitted yet.

  • Step 4: Add OperatorChat surface attribute

In web/apps/console/src/presentation/OperatorChat.tsx, find:

  const composition = compositionForState(state);
  return (
    <aside
      className="operator-chat"
      data-mode={composition.chatMode}
      data-chat-theme={composition.chatTheme}
      data-readable-surface={composition.chatTheme === "light" ? "light" : "dark"}
      aria-label="scripted operator chat"
    >

Replace with:

  const composition = compositionForState(state);
  const presentationSurface = composition.chatTheme === "light" ? "editorial" : "night";
  return (
    <aside
      className="operator-chat"
      data-mode={composition.chatMode}
      data-chat-theme={composition.chatTheme}
      data-readable-surface={composition.chatTheme === "light" ? "light" : "dark"}
      data-presentation-surface={presentationSurface}
      aria-label="scripted operator chat"
    >
  • Step 5: Add DiscussionPanel surface attribute

In web/apps/console/src/presentation/DiscussionPanel.tsx, find the root <div>:

    <div
      ref={dialogRef}
      className="discussion-panel"
      role="dialog"
      aria-modal="true"
      aria-label={branch.title}
      onKeyDown={trapKeyboardWithinDialog}
    >

Add:

      data-presentation-surface="editorial"

Expected result:

    <div
      ref={dialogRef}
      className="discussion-panel"
      data-presentation-surface="editorial"
      role="dialog"
      aria-modal="true"
      aria-label={branch.title}
      onKeyDown={trapKeyboardWithinDialog}
    >
  • Step 6: Run focused tests

Run:

pnpm --dir web --filter @lda/console test -- src/presentation/OperatorChat.test.tsx src/presentation/DiscussionPanel.test.tsx

Expected: PASS.

  • Step 7: Commit Task 1
git add web/apps/console/src/presentation/OperatorChat.tsx web/apps/console/src/presentation/OperatorChat.test.tsx web/apps/console/src/presentation/DiscussionPanel.tsx web/apps/console/src/presentation/DiscussionPanel.test.tsx
git commit -m "feat: mark presentation side surfaces"

Task 2: Normalize Chat Surface Styling

Files:

  • Modify: web/apps/console/src/presentation/presentation.css
  • Test: web/apps/console/src/presentation/OperatorChat.test.tsx

Interfaces:

  • Consumes:

    • .operator-chat[data-presentation-surface="editorial"]
    • .operator-chat[data-presentation-surface="night"]
    • .chat-message
    • .chat-tool-part
  • Produces:

    • Editorial chat cards are off-paper/tinted, not pure white.
    • Night chat cards stay dark and readable.
  • Step 1: Add test for message surface class stability

In OperatorChat.test.tsx, add this to the light-theme test from Task 1 after the data-presentation-surface assertion:

    expect(screen.getAllByText(/Found prepared workflow recipe/)[0]?.closest(".chat-message"))
      .toHaveClass("chat-message");

This test does not assert computed color. It pins the DOM hook that CSS relies on.

  • Step 2: Update chat CSS

In web/apps/console/src/presentation/presentation.css, keep the base .chat-message dark readable style, then replace the current light override:

.operator-chat[data-readable-surface="light"] .chat-message {
  border-color: color-mix(in oklch, var(--color-editorial-muted, oklch(0.48 0.025 65)) 35%, transparent);
  background: color-mix(in oklch, var(--color-editorial-paper, oklch(0.975 0.012 82)) 92%, white);
  color: var(--color-editorial-ink, oklch(0.19 0.015 65));
}

with:

.operator-chat[data-presentation-surface="editorial"] .chat-message {
  border-color: color-mix(in oklch, var(--color-editorial-muted, oklch(0.48 0.025 65)) 34%, transparent);
  background: color-mix(in oklch, var(--color-editorial-paper, oklch(0.975 0.012 82)) 82%, var(--color-editorial-muted, oklch(0.48 0.025 65)) 18%);
  color: var(--color-editorial-ink, oklch(0.19 0.015 65));
}

Replace:

.operator-chat[data-readable-surface="light"] .chat-message strong {
  color: var(--color-editorial-ink, oklch(0.19 0.015 65));
}

with:

.operator-chat[data-presentation-surface="editorial"] .chat-message strong {
  color: var(--color-editorial-ink, oklch(0.19 0.015 65));
}

Replace:

.operator-chat[data-readable-surface="light"] .chat-tool-part {
  color: var(--color-editorial-ink, oklch(0.19 0.015 65));
}

with:

.operator-chat[data-presentation-surface="editorial"] .chat-tool-part {
  border-color: color-mix(in oklch, var(--color-editorial-muted, oklch(0.48 0.025 65)) 40%, transparent);
  background: color-mix(in oklch, var(--color-editorial-paper, oklch(0.975 0.012 82)) 70%, transparent);
  color: var(--color-editorial-ink, oklch(0.19 0.015 65));
}

Keep data-readable-surface selectors only if another rule still uses them. Prefer the new semantic surface selectors for new rules.

  • Step 3: Run focused test

Run:

pnpm --dir web --filter @lda/console test -- src/presentation/OperatorChat.test.tsx

Expected: PASS.

  • Step 4: Commit Task 2
git add web/apps/console/src/presentation/presentation.css web/apps/console/src/presentation/OperatorChat.test.tsx
git commit -m "fix: normalize presentation chat surface"

Task 3: Normalize Discussion/Q&A Surface Styling

Files:

  • Modify: web/apps/console/src/presentation/presentation.css
  • Test: web/apps/console/src/presentation/DiscussionPanel.test.tsx

Interfaces:

  • Consumes:

    • .discussion-panel[data-presentation-surface="editorial"]
    • .discussion-panel__qna
    • .discussion-panel__presenter-note
  • Produces:

    • Discussion panels use editorial surface colors.
    • Q&A answers are readable.
    • Presenter notes are demoted but legible.
  • Step 1: Add tests for Q&A/presenter-note hooks

In DiscussionPanel.test.tsx, extend "renders defense Q&A fields when present" with:

    expect(screen.getByLabelText("defense question")).toBeInTheDocument();

Extend "renders speaker hints as presenter notes instead of answer content" with:

    expect(note).toHaveClass("discussion-panel__presenter-note");
  • Step 2: Update discussion panel CSS

In web/apps/console/src/presentation/presentation.css, replace the base .discussion-panel rule:

.discussion-panel {
  padding: 1rem;
  border: 1px solid oklch(0.36 0.04 250);
  background: oklch(0.16 0.025 250);
  border-radius: 0.65rem;
}

with:

.discussion-panel {
  padding: 1rem;
  border: 1px solid color-mix(in oklch, var(--color-editorial-muted, oklch(0.48 0.025 65)) 35%, transparent);
  border-radius: 0.75rem;
  background: color-mix(in oklch, var(--color-editorial-paper, oklch(0.975 0.012 82)) 88%, white);
  color: var(--color-editorial-ink, oklch(0.19 0.015 65));
}

Update the following selectors to use editorial colors:

.discussion-panel h2 {
  color: var(--color-editorial-ink, oklch(0.19 0.015 65));
}

.discussion-panel__badge {
  background: color-mix(in oklch, var(--color-editorial-muted, oklch(0.48 0.025 65)) 18%, transparent);
  color: var(--color-editorial-ink, oklch(0.19 0.015 65));
}

.discussion-panel__evidence {
  color: var(--color-editorial-muted, oklch(0.48 0.025 65));
}

.discussion-panel__summary {
  color: var(--color-editorial-ink, oklch(0.19 0.015 65));
}

.discussion-panel__qna {
  border-color: color-mix(in oklch, var(--color-intent, oklch(0.53 0.17 250)) 28%, transparent);
  background: color-mix(in oklch, var(--color-editorial-paper, oklch(0.975 0.012 82)) 74%, white);
}

.discussion-panel__question,
.discussion-panel__short-answer {
  color: var(--color-editorial-ink, oklch(0.19 0.015 65));
}

.discussion-panel__expanded-answer {
  color: color-mix(in oklch, var(--color-editorial-ink, oklch(0.19 0.015 65)) 82%, var(--color-editorial-muted, oklch(0.48 0.025 65)));
}

.discussion-panel__presenter-note {
  border-color: color-mix(in oklch, var(--color-human, oklch(0.68 0.17 55)) 38%, transparent);
  background: color-mix(in oklch, var(--color-human, oklch(0.68 0.17 55)) 10%, var(--color-editorial-paper, oklch(0.975 0.012 82)));
  color: color-mix(in oklch, var(--color-editorial-ink, oklch(0.19 0.015 65)) 76%, var(--color-editorial-muted, oklch(0.48 0.025 65)));
}

.discussion-panel__presenter-note span {
  color: color-mix(in oklch, var(--color-human, oklch(0.68 0.17 55)) 70%, var(--color-editorial-ink, oklch(0.19 0.015 65)));
}

.discussion-panel__detail {
  color: color-mix(in oklch, var(--color-editorial-ink, oklch(0.19 0.015 65)) 82%, var(--color-editorial-muted, oklch(0.48 0.025 65)));
  border-left-color: color-mix(in oklch, var(--color-editorial-muted, oklch(0.48 0.025 65)) 42%, transparent);
}

Keep .discussion-panel__return as a clear action button, but ensure it does not look like a saturated unrelated pill. If needed, update it to:

.discussion-panel__return {
  margin-top: 0.75rem;
  border: 1px solid var(--color-editorial-ink, oklch(0.19 0.015 65));
  border-radius: 0.4rem;
  padding: 0.4rem 0.75rem;
  background: var(--color-editorial-ink, oklch(0.19 0.015 65));
  color: var(--color-editorial-paper, oklch(0.975 0.012 82));
  font-weight: 600;
}
  • Step 3: Run focused test

Run:

pnpm --dir web --filter @lda/console test -- src/presentation/DiscussionPanel.test.tsx

Expected: PASS.

  • Step 4: Commit Task 3
git add web/apps/console/src/presentation/presentation.css web/apps/console/src/presentation/DiscussionPanel.test.tsx
git commit -m "fix: normalize discussion surface theme"

Task 4: Screenshot Smoke And Roadmap

Files:

  • Modify: docs/current_roadmap.md
  • Move: docs/superpowers/plans/2026-07-08-presentation-surface-theme-normalization.md to docs/historical/superpowers/plans/2026-07-08-presentation-surface-theme-normalization.md

Interfaces:

  • Consumes: Tasks 1-3.

  • Produces: verified visual smoke and updated roadmap.

  • Step 1: Run focused tests

Run:

pnpm --dir web --filter @lda/console test -- src/presentation/OperatorChat.test.tsx src/presentation/DiscussionPanel.test.tsx

Expected: PASS.

  • Step 2: Run build checks

Run:

pnpm --dir web --filter @lda/console typecheck
pnpm --dir web --filter @lda/console build
git diff --check

Expected:

  • Typecheck passes.

  • Build succeeds. Existing chunk-size warning is acceptable if unchanged.

  • No whitespace errors.

  • Step 3: Capture screenshot smoke

Start dev server if needed:

pnpm --dir web dev

Capture:

New-Item -ItemType Directory -Force web/apps/console/.visual-smoke | Out-Null
pnpx playwright screenshot --viewport-size=1280,720 "http://127.0.0.1:5173/present#scene/positioning/landscape" web/apps/console/.visual-smoke/surface-positioning-1280x720.png
pnpx playwright screenshot --viewport-size=1280,720 "http://127.0.0.1:5173/present#scene/planner-runtime/planner" web/apps/console/.visual-smoke/surface-planner-runtime-1280x720.png
pnpx playwright screenshot --viewport-size=1280,720 "http://127.0.0.1:5173/present#scene/lifecycle/draft" web/apps/console/.visual-smoke/surface-lifecycle-1280x720.png
pnpx playwright screenshot --viewport-size=1280,720 "http://127.0.0.1:5173/present#discuss/where-is-ai-agent" web/apps/console/.visual-smoke/surface-qna-agent-1280x720.png
pnpx playwright screenshot --viewport-size=1280,720 "http://127.0.0.1:5173/present#discuss/evaluation-validity" web/apps/console/.visual-smoke/surface-qna-evaluation-1280x720.png
pnpx playwright screenshot --viewport-size=1280,720 "http://127.0.0.1:5173/present#scene/interrupt-evidence/approval" web/apps/console/.visual-smoke/surface-approval-1280x720.png

Manual acceptance:

  • Chat is not a bright white card stack.

  • Discussion/Q&A panels look editorial and readable.

  • Presenter note is demoted but legible.

  • Scene 3/4/5 direct surfaces do not look like mismatched dark modals.

  • .visual-smoke/ remains ignored by git.

  • Step 4: Update roadmap

In docs/current_roadmap.md, under Next presentation visual slices, replace:

  2. Surface theme normalization pass: make light chat, discussion modals, and
     Q&A branches use the same editorial presentation surface instead of
     jumping between overly white chat cards and dark modal interiors.

with:

  2. Completed: surface theme normalization pass made light chat, discussion
     modals, and Q&A branches use the same editorial presentation surface.
     Implementation:
     [`presentation surface theme normalization`](historical/superpowers/plans/2026-07-08-presentation-surface-theme-normalization.md).

Renumber the remaining entries so the scene composition pass is 3 and the presentation craft pass is 4.

  • Step 5: Archive plan

Run:

git mv docs/superpowers/plans/2026-07-08-presentation-surface-theme-normalization.md docs/historical/superpowers/plans/2026-07-08-presentation-surface-theme-normalization.md

If the plan is not tracked yet, use PowerShell:

Move-Item docs/superpowers/plans/2026-07-08-presentation-surface-theme-normalization.md docs/historical/superpowers/plans/2026-07-08-presentation-surface-theme-normalization.md
  • Step 6: Commit Task 4
git add docs/current_roadmap.md docs/historical/superpowers/plans/2026-07-08-presentation-surface-theme-normalization.md
git commit -m "docs: record presentation surface normalization"

Final Verification

  • Run focused tests:
pnpm --dir web --filter @lda/console test -- src/presentation/OperatorChat.test.tsx src/presentation/DiscussionPanel.test.tsx

Expected: PASS.

  • Run full web tests:
pnpm --dir web test

Expected: PASS.

  • Run full web typecheck:
pnpm --dir web typecheck

Expected: PASS.

  • Run status checks:
git diff --check
git status --short

Expected:

  • No whitespace errors.
  • No untracked .visual-smoke/ files.
  • Only intentional files remain if not yet committed.

Review Checklist

  • Chat no longer appears as a bright white card stack.
  • Direct discussion routes are readable and editorial.
  • Q&A branches and legacy branches use the same discussion surface.
  • Presenter notes are demoted and legible.
  • /console unchanged.
  • No new dependency added.
  • Scene composition remains deferred to the next slice.