Skip to content

D3: Dark mode toggle (Phase 3 stretch)#2

Closed
riccardofresi wants to merge 3 commits into
phase-1-m1-stabilize-documentfrom
feat/dark-mode
Closed

D3: Dark mode toggle (Phase 3 stretch)#2
riccardofresi wants to merge 3 commits into
phase-1-m1-stabilize-documentfrom
feat/dark-mode

Conversation

@riccardofresi

Copy link
Copy Markdown
Owner

Summary

Implements the D3 desired item from the RFP — operator-selectable theme, targeted at low-light warehouse stations per the proposal. Stacked on top of PR #1; built on a `feat/dark-mode` branch via `git worktree` so it can be reviewed (or shelved) independently of the M1+M2 work.

Branch `feat/dark-mode`
Base `phase-1-m1-stabilize-document` (PR #1)
Tests 53 / 53 green (was 48 in PR #1; +5 for Flow lindsey-anthropic#9)

What's in this PR

Composable: `client/src/composables/useTheme.js`

  • Persists choice in `localStorage` (key `app-theme`)
  • Applies `data-theme="dark"` attribute to `` via `watch`
  • Module-scope ref so `useTheme()` returns a shared singleton across consumers

Component: `client/src/components/ThemeToggle.vue`

  • Small banner button next to the language switcher
  • Sun/moon icon swap based on current theme
  • Accessible label flips ("Switch to dark mode" ↔ "Switch to light mode")

Theming: `client/src/App.vue` (non-scoped `<style>`)

  • CSS-variable palette declared at `:root`; `:root[data-theme="dark"]` overrides
  • `color-scheme` set so native form controls follow
  • Global overrides for high-traffic surfaces: banner, filter bar, `.card`, `.stat-card`, tables (`thead`, `th`, `td`, `tr:hover`), `.badge`, page headers, form inputs
  • Per-component scoped styles still apply on top — long-tail surfaces remain on the original light palette and are flagged as Phase-3 polish follow-on

E2E spec: `tests/e2e/specs/09-dark-mode.spec.js`

5 tests:

  • Toggle is visible in the banner
  • Default load is light
  • Click toggles to dark; `data-theme` flips, `localStorage` records `'dark'`, body bg luminance drops below 50
  • Click again returns to light
  • Choice survives a full `page.reload()` (localStorage persistence)

Coverage limitation (intentional)

Most view-level components have hardcoded colors in their scoped styles (`background: white;`, `color: #0f172a;`). Reaching them from App.vue's global stylesheet would require either `:deep()` boilerplate in every consumer or a per-file refactor. This PR refactors the most-visible surfaces only; full coverage of every nested card and modal is a Phase-3 polish task.

For the workshop demo, this gives a clean dark mode for the dashboard, filter bar, all data tables, badges, and stat cards — which covers the screens an operator looks at all day.

Worktree workflow note

This branch was developed on a sibling `git worktree` at `../claude-darkmode` so the main directory could keep R1-R4 work intact and the dev servers could move to the worktree without conflict on ports 3000/8001. After this PR is reviewed:

Test plan

  • `cd tests/e2e && npm test` → 53 / 53 green
  • Manual probe via Playwright MCP: data-theme flips, body bg switches, preference survives reload
  • (Reviewer) Toggle on each view (Dashboard, Inventory, Orders, Spending, Demand, Reports, Restocking, Backlog) and confirm acceptable contrast

🤖 Generated with Claude Code

riccardofresi and others added 2 commits April 27, 2026 14:44
Implements the D3 desired item from the RFP — operator-selectable theme,
targeted at low-light warehouse stations per the proposal. Built on a
separate feat/dark-mode branch via git worktree so the main PR stays focused
on R1-R4 and this can be reviewed (or shelved) independently.

Architecture
  - useTheme composable persists the choice in localStorage and applies a
    data-theme="dark" attribute to <html>.
  - App.vue's <style> (intentionally non-scoped) hosts the CSS-variable
    palette: :root for light, :root[data-theme="dark"] for dark. Per-component
    scoped styles still apply on top — overrides cover only the high-traffic
    surfaces (banner, filter bar, cards, tables, badges, page headers).
  - ThemeToggle.vue is a small banner button with a sun/moon icon swap.
  - color-scheme set to match so native form controls (selects, scrollbars)
    follow.

Coverage
  - High-traffic surfaces flip cleanly.
  - Long-tail per-view scoped styles remain on the original light palette in
    dark mode and are tracked as Phase-3 polish follow-on (see PR description).

Tests
  - tests/e2e/specs/09-dark-mode.spec.js — 5 specs covering toggle visibility,
    default light, click → dark with localStorage persistence and luminance
    check, click again → light, full-reload persistence.
  - Test note: localStorage seed must be done via page.evaluate AFTER goto
    (not addInitScript) so a later page.reload() doesn't re-clear the value.
    Body has a 0.2s background-color transition; tests poll until the colour
    settles.

Verification
  - 53/53 Playwright tests pass on this branch (was 48; +5 for Flow lindsey-anthropic#9).
  - Manual probe via Playwright MCP: toggle flips data-theme, body bg switches
    from rgb(248,250,252) to rgb(11,17,32), preference survives reload.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Initial dark mode (commit 09bbbe5 on this branch — wait, prior commit on
this branch) covered the global surfaces but missed several view-scoped
classes whose CSS specificity ties with our globals and whose stylesheet
is loaded after App.vue. Surfaced visually as white islands on the
dashboard:

  - Dashboard `.kpi-card` (the five Key Performance Indicator cards)
  - FilterBar `.filter-select` and `.filters-bar`
  - LanguageSwitcher `.language-button`
  - ProfileMenu `.profile-button`
  - FilterBar `.reset-filters-btn`
  - Dashboard `.h-bar-container` and `.task-item:hover`
  - `.clickable-row:hover` (Dashboard had `!important` on light hover)

Approach
  - Add `:root[data-theme="dark"]` selectors with `!important` for the
    specific class names. The trade-off (using !important) is bounded
    to the dark-mode override block; light mode is unaffected.
  - Cover dropdown menus too (LanguageSwitcher dropdown, ProfileMenu
    items) so the click states stay coherent.

This is still a prototype; full per-view refactor of every scoped style
to use the CSS variables would be the production-grade path.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@riccardofresi

Copy link
Copy Markdown
Owner Author

Update — coverage extended to view-scoped surfaces (commit `d1bf40b`)

After visual review, several surfaces remained on the light palette in dark mode because their styles live in component-scoped CSS (Vue's `data-v-xxx` attribute) which loads after App.vue and wins on tied specificity.

Added high-specificity `!important` overrides under `:root[data-theme="dark"]` for:

  • `.kpi-card` (Dashboard's five Key Performance Indicator cards)
  • `.filter-select` and `.filters-bar` (FilterBar)
  • `.language-button`, `.profile-button`, `.reset-filters-btn` (banner controls)
  • `.dropdown-menu`, `.profile-menu`, `.menu-item`, `.dropdown-item` (popover surfaces)
  • `.clickable-row:hover`, `.h-bar-container`, `.task-item:hover` (interactive states)

Verified via Playwright MCP: `getComputedStyle` of every offending surface now resolves to `rgb(17, 24, 39)` (the dark elevated token) or `transparent`. Suite still 53/53 green.

The `!important` is deliberate and bounded — applies only inside the dark-mode override block. The clean production path remains a per-view refactor of scoped styles to consume the CSS variables directly.

Two issues spotted in the profile-menu popover:

1. Dropdown header banner (.dropdown-header), user name/email, divider, and
   logout item still rendered with their light-mode component-scoped styles.
   Added overrides under :root[data-theme="dark"] for:
     .dropdown-header, .user-name, .user-email, .profile-name,
     .dropdown-divider, .dropdown-item.logout (+ hover), .task-badge

2. composables/useAuth.js still hardcoded john.doe@catalystcomponents.com —
   the previous brand-correction pass (lindsey-anthropic#15) only caught the H1 in the locale
   files. This is the email field shown inside the dropdown header. Renamed
   to john.doe@meridiancomponents.example to match the brand.

Verified via Playwright MCP:
  - All dropdown surfaces resolve to dark tokens
  - Email text in DOM now reads "john.doe@meridiancomponents.example"

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
riccardofresi added a commit that referenced this pull request Apr 27, 2026
…E2E green (#1)

* Phase 1 M1: stabilize Reports module, document architecture, baseline E2E

Delivers the Phase 1 milestone (M1) of the Meridian engagement (RFP MC-2026-0417):
the foundation for every subsequent change — Reports module remediation,
current-state architecture documentation, and a Playwright E2E framework that
gates future regressions.

R1 — Reports module remediation (closes 9 of 15 logged defects)
  - Reports.vue rewritten in Composition API (was Options API)
  - Filters now wired through useFilters composable + watch reload
  - Backend /api/reports/* endpoints accept warehouse, category, status, month
  - i18n integration via new reports.* namespace (English + Japanese)
  - api.js centralises calls (no more direct axios from views)
  - Stable v-for keys, console-log spam removed
  - W3 cleanup: brand corrected to Meridian Components, "1 day" pluralization
    helper, Spending alert() removed and toLocaleString -> formatCurrency

R3 — Browser test framework (Playwright, standalone)
  - tests/e2e/ self-contained: package.json, config, 17 specs across 2 files
  - Flow #1 smoke (shell + 6 routes + interactive nav)
  - Flow lindsey-anthropic#4 Reports (existing-behaviour + R1 regression contract)
  - Trace/screenshot/video on failure for IT debuggability
  - README documents stack, conventions, sample CI

R4 — Architecture documentation
  - proposal/architecture.html: 9 sections covering system overview, request
    lifecycle, API reference, frontend/backend patterns, data layer, 10-item
    technical-debt catalogue, and 5 ADRs

Act 1 proposal package (was already produced; included in this milestone for
repo coherence): executive summary, technical approach, timeline + Gantt,
pricing, relevant-experience scaffold, clarifying questions, capabilities deck.

Verification
  - 17/17 Playwright tests pass against the running app
  - Manual snapshot via Playwright MCP confirms brand, pluralization, no
    Reports console noise
  - Defect log (docs/r1-defect-log.md) updated with disposition for all 15
    findings; remaining 6 are deferred (4 await Tanaka decisions, 2 absorbed
    into R2 build) — within the +50% pricing trigger, no T&M conversion

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* Phase 2 W4-W5: extend E2E coverage to 4 more critical flows

Adds Playwright specs for the four remaining non-gated critical flows in the
proposal's R3 plan, raising suite coverage from 17 to 35 green tests.

R3 — Critical flow coverage (4 new flows)
  - Flow #2 Inventory: Category and Location filtering, intersection,
    Reset-button behaviour. 5 tests.
  - Flow #3 Orders: status stat cards, header/row-count consistency,
    items <details> drill-down, Status + Time Period intersection. 4 tests.
  - Flow lindsey-anthropic#5 Demand: three trend cards (increasing/stable/decreasing),
    forecasts table columns, per-row trend value validation, summary-vs-table
    consistency check. 4 tests.
  - Flow lindsey-anthropic#7 Spending: 4 KPI cards with currency values, Revenue vs Costs
    chart bar groups, 4 cost categories with name/amount/percentage,
    Recent Transactions structure, post-cleanup non-clickability. 6 tests.

Tooling
  - Authoring convention reinforced: page.waitForRequest must be armed
    BEFORE the action; selectOption fires synchronously and the request is
    gone by the time a post-action listener attaches.

Defect log
  - lindsey-anthropic#17 added (Low): Spending category percentages labelled "% of total" but
    sum to ~124%. Discovered while writing the Flow lindsey-anthropic#7 spec. Open; resolution
    requires Operations input on the intended denominator.

Verification
  - 35/35 Playwright tests pass against the running app (was 17/17)
  - Defect-log severity counts and disposition table updated to reflect lindsey-anthropic#17
    and the four newly-shipped flows in tests/e2e/README.md

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* Phase 2 W5-W7: ship R2 Restocking feature end-to-end

The R2 deliverable from RFP §3.1: a new Restocking view that recommends
purchase orders given current stock, demand forecast, and an operator-supplied
budget ceiling. Built without a Discovery session (Tanaka unavailable in the
workshop simulation), using the assumptions from the proposal — (s, S) policy
with newsvendor service-level calibration (Silver, Pyke & Thomas, 2017).

Backend (server/main.py)
  - GET /api/restocking/recommendations?budget=&service_level=&warehouse=&category=
  - For each inventory item with a matching demand forecast:
      shortfall = max(0, forecast - on_hand)
      qty       = shortfall (or reorder_point - on_hand if below ROP)
      cost      = qty * unit_cost
      criticality = shortfall * unit_cost  (urgency × value at stake)
  - Greedy budget allocation: sort by criticality desc, mark in_budget while
    cumulative cost <= ceiling.
  - Service level snaps to the closest tabulated z-score (0.80 .. 0.99). Today
    z is informational; when forecasts add σ_LT, it moves into the formula.
  - Documented limitation: only SKUs with a forecast in
    server/data/demand_forecasts.json get scored (currently 9 of 32).

Frontend (client/src/views/Restocking.vue)
  - Operator controls: budget input, service-level slider (80–99%),
    "Generate" button.
  - Five summary cards: Candidates / In budget / Out of budget /
    Total selected cost / Budget remaining.
  - Recommendations table: 11 columns including operator-overridable Qty
    input. Editing Qty recomputes Est. Cost client-side; status badges
    distinguish in-budget vs out-of-budget rows.
  - Auto-loads on mount; integrates with useFilters? — no, this is a
    single-purpose tool that uses its own controls (budget/service-level)
    rather than the global filter bar.
  - Composition API throughout, useI18n for all strings.

Routing (client/src/main.js, App.vue)
  - /restocking route registered.
  - "Restocking" added to the top nav after "Reports".

Internationalisation
  - Full restocking.* namespace added to en.js and ja.js (37 keys × 2 locales).

Browser tests (tests/e2e/specs/08-restocking.spec.js)
  - 6 specs covering: page render, table columns, summary↔table consistency,
    sort-by-criticality invariant, budget-tightening behaviour, operator
    override propagation.

Verification
  - 41/41 Playwright tests pass (was 35/35; +6 for Flow lindsey-anthropic#8).
  - Manual probe via Playwright MCP confirms: 4 candidates ranked by Est.
    Cost desc, all in budget at $100k ($65,965 total, $34,035 remaining),
    overrides update the table in real time.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* W7: route Backlog (close #DEBT-05) + i18n cleanup + Flow lindsey-anthropic#6

Closes the last gated R1 cleanup items by registering a route for the
Backlog view (which had been dead code in the previous vendor's handoff)
and translating it through the same useI18n + pluralize plumbing used
elsewhere.

Routing
  - /backlog route registered in main.js
  - "Backlog" link added to App.vue nav after "Restocking"

Backlog.vue rewrite
  - Adds useI18n; all hardcoded English strings move to a new backlog.*
    namespace (en + ja, 21 keys)
  - Days Delayed renders via pluralize() — "1 day" vs "n days"
  - Priority badges use t(`priority.${item.priority}`) so the lowercase
    raw value never reaches the DOM (defect lindsey-anthropic#9)
  - Empty-state copy moved out of inline styles into scoped CSS
  - useFilters integration preserved (Backlog already had this part right)

Browser tests
  - tests/e2e/specs/06-backlog.spec.js — 7 specs covering routing,
    priority stats, table structure, translated badges, "1 day"
    pluralization, EN→JA translation contract
  - Locale-leak fix: tests that switch the language now reset
    localStorage in afterEach (specs 04 + 06). Without this the JP UI
    leaks across worker boundaries and breaks subsequent tests that
    assert English text — surfaced by a flaky run during this commit's
    development.

Defect log + README
  - lindsey-anthropic#8, lindsey-anthropic#9 marked closed (were gated by #DEBT-05)
  - #DEBT-05 closed implicitly (route now exists)
  - Flow lindsey-anthropic#6 status flipped to shipped in tests/e2e/README.md

Verification
  - 48/48 Playwright tests pass (was 41)
  - Manual probe via Playwright MCP confirms: page reachable from nav,
    "1 day" vs "5 days" rendered correctly, priority badges translated,
    JA switch translates the title to バックログ管理

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@riccardofresi riccardofresi deleted the branch phase-1-m1-stabilize-document April 27, 2026 13:04
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.

1 participant