Skip to content

feat(dashboard): add Timeline Status Cards (#474)#712

Merged
steilerDev merged 4 commits into
betafrom
feat/474-timeline-status-cards
Mar 10, 2026
Merged

feat(dashboard): add Timeline Status Cards (#474)#712
steilerDev merged 4 commits into
betafrom
feat/474-timeline-status-cards

Conversation

@steilerDev
Copy link
Copy Markdown
Owner

Summary

  • Add 4 timeline status dashboard cards: Upcoming Milestones, Work Item Progress (SVG donut chart), At-Risk Items, and Critical Path
  • Integrate TimelineStatusCards wrapper into DashboardPage with timeline data from existing getTimeline() fetch
  • Add 40 unit tests across 4 test files covering all card logic, empty states, sorting, and health indicators

Fixes #474

Test plan

  • Unit tests pass (40 new tests across 4 files)
  • SVG donut chart renders correctly with proper aria-label
  • Milestone health indicators show On Track/Delayed correctly
  • At-risk items correctly identify overdue and late-start work items
  • Critical path health badge uses correct color thresholds (>14 green, 7-14 yellow, <7 red)
  • Pre-commit hook quality gates pass

Co-Authored-By: Claude Opus 4.6 noreply@anthropic.com

claude added 4 commits March 9, 2026 23:33
Add 4 new dashboard cards for timeline visualization:
- UpcomingMilestonesCard: displays next 5 incomplete milestones with health indicators
- WorkItemProgressCard: SVG donut chart showing work item counts by status
- AtRiskItemsCard: lists up to 5 overdue or late-start items with links
- CriticalPathCard: shows critical path summary with deadline and health color

All components follow existing patterns with CSS modules, design tokens,
and comprehensive data-testid attributes for testing.

Fixes #474
Co-Authored-By: Claude frontend-developer (Haiku 4.5) <noreply@anthropic.com>
…rsing

Add 40 unit tests across 4 test files for the timeline status card
components. Fix CriticalPathCard date parsing to use correct
Date constructor (year, month-1, day) instead of buggy string
manipulation. Fix health threshold boundary to use <= 14 for
warning range (7-14 days) per acceptance criteria.

Fixes #474

Co-Authored-By: Claude Haiku <frontend-developer> <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet <qa-integration-tester> <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Add missing `jest` import from @jest/globals in CriticalPathCard.test.tsx
- Replace hardcoded color: 'white' with CSS Module badge classes
- Use badgeGreen/badgeRed/badgeYellow CSS classes instead of inline
  backgroundColor for health indicator (dark mode safe)
- Fix link focus-visible to use box-shadow: var(--shadow-focus)
- Use var(--spacing-2) token for legend dot dimensions

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Copy link
Copy Markdown
Owner Author

@steilerDev steilerDev left a comment

Choose a reason for hiding this comment

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

[security-engineer]

PR #712 — Timeline Status Cards (Story #474): Security Review

Summary

Reviewed all five new components (, , , , ) and the updated . No security findings.

Checklist

  • XSS vectors: No dangerouslySetInnerHTML, innerHTML, eval(), or document.write in any of the new components or in DashboardPage.tsx. All user-sourced data (titles, dates, counts) is rendered as JSX text nodes — React escapes these automatically.
  • Link injection: AtRiskItemsCard uses React Router <Link to={/work-items/${item.id}}>. item.id flows exclusively from the TimelineResponse API payload (GET /api/timelineTimelineWorkItem.id: string) — not from any user-controlled input. React Router <Link> renders an <a href> into a same-origin SPA path; no open redirect or javascript: URI risk.
  • SVG injection: WorkItemProgressCard renders a donut chart SVG. All dynamic values (strokeDasharray, strokeDashoffset, stroke, center {total}) are derived exclusively from arithmetic on API-sourced integers and hardcoded CSS variable strings. No user-controlled string is interpolated into SVG attributes. The aria-label attribute containing counts is a React JSX attribute — React HTML-encodes it automatically.
  • Prototype pollution / injection in data processing: Filtering and sorting logic in all four cards uses standard array methods (.filter, .sort, .localeCompare, .includes) operating on typed TimelineWorkItem and TimelineMilestone objects. No use of JSON.parse, dynamic property access via user strings, or Object.assign patterns that could introduce prototype pollution.
  • No new API surface or auth changes: The PR adds no new routes, no new backend code, and no auth-related changes. Timeline data continues to flow through the existing authenticated GET /api/timeline endpoint via getTimeline() in timelineApi.ts.
  • Sensitive data exposure: No tokens, credentials, or PII are introduced. Error messages on timeline fetch failure use the established ApiClientError pattern — server error details are not re-exposed in the UI beyond the existing pattern already reviewed in prior PRs.
  • CORS / hardcoded credentials: None introduced.
  • New dependencies: None added.

Verdict

No security issues found. The PR is consistent with the established safe rendering patterns used across the codebase.

Copy link
Copy Markdown
Owner Author

@steilerDev steilerDev left a comment

Choose a reason for hiding this comment

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

[security-engineer]

PR #712 — Timeline Status Cards (Story #474): Security Review

Summary

Reviewed all five new components (TimelineStatusCards.tsx, UpcomingMilestonesCard.tsx, WorkItemProgressCard.tsx, AtRiskItemsCard.tsx, CriticalPathCard.tsx) and the updated DashboardPage.tsx. No security findings.

Checklist

  • XSS vectors: No dangerouslySetInnerHTML, innerHTML, eval(), or document.write in any of the new components or in DashboardPage.tsx. All user-sourced data (titles, dates, counts) is rendered as JSX text nodes — React escapes these automatically.
  • Link injection: AtRiskItemsCard uses React Router <Link to={'/work-items/${item.id}'}>. item.id flows exclusively from the TimelineResponse API payload (GET /api/timeline → TimelineWorkItem.id: string) — not from any user-controlled input. React Router renders an anchor into a same-origin SPA path; no open redirect or javascript: URI risk.
  • SVG injection: WorkItemProgressCard renders a donut chart SVG. All dynamic values (strokeDasharray, strokeDashoffset, stroke, center total) are derived exclusively from arithmetic on API-sourced integers and hardcoded CSS variable strings. No user-controlled string is interpolated into SVG attributes. The aria-label attribute containing counts is a React JSX attribute — React HTML-encodes it automatically.
  • Prototype pollution / injection in data processing: Filtering and sorting logic in all four cards uses standard array methods (.filter, .sort, .localeCompare, .includes) operating on typed TimelineWorkItem and TimelineMilestone objects. No use of JSON.parse, dynamic property access via user strings, or Object.assign patterns that could introduce prototype pollution.
  • No new API surface or auth changes: The PR adds no new routes, no new backend code, and no auth-related changes. Timeline data continues to flow through the existing authenticated GET /api/timeline endpoint via getTimeline() in timelineApi.ts.
  • Sensitive data exposure: No tokens, credentials, or PII are introduced. Error messages on timeline fetch failure use the established ApiClientError pattern — server error details are not re-exposed in the UI beyond the existing pattern already reviewed in prior PRs.
  • CORS / hardcoded credentials: None introduced.
  • New dependencies: None added.

Verdict

No security issues found. The PR is consistent with the established safe rendering patterns used across the codebase.

Copy link
Copy Markdown
Owner Author

@steilerDev steilerDev left a comment

Choose a reason for hiding this comment

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

[product-architect]

Architecture Review: Timeline Status Cards (#474)

Verified

  • Shared types: All components correctly import TimelineResponse, TimelineWorkItem, TimelineMilestone from @cornerstone/shared using type imports
  • No new API calls: Data flows from the existing getTimeline() call in DashboardPage, stored in timelineData state, and passed to TimelineStatusCards via props -- no additional fetches
  • No shared type or schema changes: The existing TimelineResponse shape provides all necessary fields (workItems, milestones, criticalPath)
  • SVG donut chart: Pure SVG implementation with correct role="img" and descriptive aria-label -- no external charting library dependency
  • CSS Modules: The shared stylesheet uses design tokens correctly (semantic Layer 2 tokens, no hardcoded hex values)
  • DashboardCardId: The 'timeline-status' card ID already exists in the shared type -- no type changes needed
  • Component architecture: Clean decomposition into 4 focused sub-components with a thin wrapper
  • Test coverage: 40 tests across 4 files covering empty states, sorting, health indicators, SVG rendering, and aria-labels. Good use of jest.useFakeTimers() for deterministic date tests and distant past/future dates to avoid flakiness

Minor Notes (non-blocking)

  1. CriticalPathCard inline styles (LOW): Unlike the other three cards which use CSS Module classes, CriticalPathCard.tsx uses extensive inline style attributes for layout, typography, and colors. This is a consistency deviation from the CSS Modules convention but is functionally correct. Consider extracting to CSS Module classes in a future polish pass.

  2. Hardcoded color: 'white' (LOW): The health badge in CriticalPathCard.tsx sets color: 'white' inline. In dark mode, white text on a potentially lighter badge background could have contrast issues. A semantic token like var(--color-primary-text) would be more resilient. Non-blocking since the badge backgrounds (danger/warning/success) are strong enough colors for white text in both modes.

Overall this is a clean, well-structured addition that correctly reuses existing data and follows established patterns. Architecture approved.

Copy link
Copy Markdown
Owner Author

@steilerDev steilerDev left a comment

Choose a reason for hiding this comment

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

[product-owner]

PR #712 Review — Story #474: Timeline Status Cards

Acceptance Criteria Verification

AC Description Verdict Notes
1 Upcoming Milestones card: next 5 sorted by target date, on-time/delayed indicators PASS Filters completed, sorts by targetDate ascending, slices to 5. On Track/Delayed badges rendered.
2 Milestone delay: projected completion date vs target date PASS Compares projectedDate against targetDate. Null projectedDate defaults to "On Track".
3 Donut chart: work item counts by status (3 statuses per override) PASS Groups into not_started, in_progress, completed. No blocked status (correct per user override removing 4th status).
4 Design system color tokens, light/dark mode PASS Uses var(--color-text-muted), var(--color-primary), var(--color-success), var(--color-border), var(--color-warning), var(--color-danger) throughout. CSS Module classes use design tokens. Dark mode handled via CSS custom properties.
5 At-Risk Items: up to 5 overdue/late-start items with detail links PASS in_progress + past endDate = "Overdue"; not_started + past startDate = "Late Start". Sorted by date ascending (most overdue first), capped at 5. Each title is a <Link to={/work-items/${id}}>.
6 Critical Path: total items, next deadline, days remaining PASS Shows critical-count (total on path), critical-deadline (formatted via formatDate), critical-days (computed diff).
7 Critical Path health: green >14, yellow 7-14, red <7/overdue PASS Thresholds: < 0 = Overdue (red), < 7 = Critical (red), <= 14 = Warning (yellow), > 14 = On Track (green). Matches AC exactly.
8 Chart library override: pure SVG donut PASS No library dependency added. Pure SVG with <circle> elements, strokeDasharray/strokeDashoffset for segments.
9 Donut aria-label describes status breakdown PASS aria-label="Work item progress: ${completed} completed, ${in_progress} in progress, ${not_started} not started" on SVG with role="img".

Non-Blocking Observations

  1. Test authorship (medium): Both commits are authored by product-architect. Per CLAUDE.md, qa-integration-tester should own unit and integration tests. The 40 tests in this PR were written by the product-architect agent. This is a process deviation -- flag for the team to ensure qa-integration-tester handles test writing in future stories.

  2. Inline styles in CriticalPathCard (low): CriticalPathCard uses extensive inline style={{...}} attributes for layout and typography, while the other three cards use CSS Module classes consistently. Consider migrating to CSS Module classes for consistency in a refinement pass.

  3. CI still pending: Test shards and Docker build are still in progress at time of review. Approval is conditional on CI passing.

Verdict

All 9 acceptance criteria are met. The implementation correctly handles the user-specified overrides (3 statuses instead of 4 in donut chart, pure SVG instead of chart library). At-risk logic correctly uses in_progress+overdue and not_started+late_start definitions without referencing a nonexistent "blocked" status. Critical path health thresholds match the AC exactly.

Decision: APPROVE (conditional on CI green)

Copy link
Copy Markdown
Owner Author

@steilerDev steilerDev left a comment

Choose a reason for hiding this comment

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

[ux-designer]

Design system compliance review for PR #712 — Timeline Status Cards (story #474).

I read wiki/Style-Guide.md, client/src/styles/tokens.css, client/src/styles/shared.module.css, and all five component files before writing this review.


Summary

The CSS module (TimelineStatusCards.module.css) is well-written and almost fully token-compliant. However I am requesting changes due to several issues in the TSX files: inline styles that should live in the CSS module, a hardcoded color: 'white' value that breaks dark mode, a missing badgeYellow variant for the Warning state, and a focus-visible pattern that uses outline instead of box-shadow: var(--shadow-focus).


Findings

Critical / High

1. Hardcoded color: 'white' in CriticalPathCard.tsx (line 112) — breaks dark mode

File: client/src/components/TimelineStatusCards/CriticalPathCard.tsx, line 109–119

The inline-style health badge uses color: 'white' as a hardcoded string. This is a literal hex-equivalent that is invisible to the dark mode token system.

// WRONG — hardcoded string
style={{
  backgroundColor: healthColor,
  color: 'white',    // ← hardcoded
  ...
}}

The badge uses --color-danger or --color-success as its background. The correct text token for text rendered on those filled surfaces is var(--color-danger-text) (which in dark mode resolves to #111827 — dark text on the brighter red) and var(--color-success-text) (white in light mode, white in dark mode). Because both danger and success text are already white in light mode and the dark mode overrides handle the switch, the correct approach is:

// CORRECT — use token
color: 'var(--color-danger-text)',   // for danger/critical states
color: 'var(--color-success-text)',  // for on-track state

Since the badge alternates between danger, warning, and success backgrounds, the cleanest solution is to move this health badge to a CSS Module class set (.badgeHealthGreen, .badgeHealthYellow, .badgeHealthRed) and reference the appropriate text token per variant — which also resolves finding #2 below.

Severity: High — hardcoded color that cannot adapt to dark mode.


2. Missing yellow/warning badge variant — --color-warning used as backgroundColor with no text token

File: client/src/components/TimelineStatusCards/CriticalPathCard.tsx, lines 71–72

healthColor = 'var(--color-warning)'; // yellow 7-14 days

--color-warning (orange-400 in light mode, orange-300 in dark mode) is used as a backgroundColor inline style. There is no accompanying color token that is guaranteed to pass contrast on this background. The amber/orange surface needs dark text for readability: var(--color-text-primary) (dark gray in light mode) or a dedicated --color-warning-text token.

Additionally, neither the CSS module nor the token file defines a badge token family for "warning" surfaces analogous to --color-danger-bg / --color-danger-text-on-light. Using only --color-warning (the full-saturation orange) as a badge background will not pass WCAG AA contrast for any text color — the orange is a mid-tone that sits between light and dark.

Required fix: Either:

  • Add a .badgeYellow (or .badgeOrange) CSS Module class using the --color-hi-status-scheduled-bg / --color-hi-status-scheduled-text token pair (amber-100 / amber-800 in light mode; already has dark mode overrides), which is the established pattern for amber status surfaces in this design system, or
  • Use the --color-status-not-started-bg / --color-status-not-started-text neutral pair as a "Warning" approximation.

The --color-hi-status-scheduled-* family is the correct choice:

/* TimelineStatusCards.module.css */
.badgeYellow {
  background-color: var(--color-hi-status-scheduled-bg);
  color: var(--color-hi-status-scheduled-text);
}

Severity: High — contrast failure risk + no dark mode coverage for the Warning state.


Medium

3. Focus-visible on .link uses outline instead of box-shadow: var(--shadow-focus)

File: client/src/components/TimelineStatusCards/TimelineStatusCards.module.css, lines 83–86

/* WRONG — outline instead of shadow-focus token */
.link:focus-visible {
  outline: 2px solid var(--color-primary);
  outline-offset: 2px;
}

The established pattern across the entire design system is:

/* CORRECT */
.link:focus-visible {
  outline: none;
  box-shadow: var(--shadow-focus);
}

--shadow-focus (0 0 0 3px rgba(59,130,246,0.3)) automatically switches to the darker-mode value (0 0 0 3px rgba(96,165,250,0.4)) via the Layer 3 dark mode override. The outline: 2px solid var(--color-primary) approach does adapt the color via token, but it misses the soft glow (alpha-channel ring) that the design system uses consistently, and it does not match the focus style used on links in LinkedDocumentsSection, GanttTooltip, and everywhere else in the application.

This is a recurring issue flagged in previous PRs (#402, #414); please fix before merging.

Severity: Medium — design consistency and partial dark mode mismatch.


4. Inline styles in CriticalPathCard.tsx should be CSS Module classes

File: client/src/components/TimelineStatusCards/CriticalPathCard.tsx, lines 79–121

CriticalPathCard uses inline style objects for all layout and typography:

<div style={{ display: 'flex', flexDirection: 'column', gap: 'var(--spacing-3)' }}>
<span style={{ color: 'var(--color-text-muted)', fontSize: 'var(--font-size-xs)' }}>
<div style={{ fontSize: 'var(--font-size-2xl)', fontWeight: 'var(--font-weight-bold)' }}>

While inline var() references do resolve to tokens and will switch in dark mode, inline styles:

  • Cannot be overridden via CSS specificity
  • Cannot use pseudo-classes (:focus-visible, :hover, @media)
  • Are not inspectable by the CSS Modules toolchain
  • Violate the project's ADR-006 (CSS Modules) convention

Move these to named classes in TimelineStatusCards.module.css. Suggested additions:

.statLabel {
  font-size: var(--font-size-xs);
  color: var(--color-text-muted);
}

.statValue {
  font-size: var(--font-size-2xl);
  font-weight: var(--font-weight-bold);
  color: var(--color-text-primary);
}

.statValueSm {
  font-size: var(--font-size-sm);
  color: var(--color-text-primary);
}

.statValueMd {
  font-size: var(--font-size-lg);
  font-weight: var(--font-weight-semibold);
  color: var(--color-text-primary);
}

.statRow {
  display: flex;
  align-items: center;
  justify-content: space-between;
}

Severity: Medium — violates CSS Modules convention; inline styles block pseudo-class styling and media queries.


5. Inline styles in UpcomingMilestonesCard.tsx should be CSS Module classes

File: client/src/components/TimelineStatusCards/UpcomingMilestonesCard.tsx, lines 43–44

<div style={{ display: 'flex', gap: 'var(--spacing-2)' }}>
<span style={{ color: 'var(--color-text-muted)' }}>

Same issue as finding #4. Move to named CSS Module classes. The date span in particular needs a class because it communicates secondary information and may need responsive adjustment at the --breakpoint-sm breakpoint. Suggested:

.milestoneDate {
  color: var(--color-text-muted);
  font-size: var(--font-size-sm);
}

.milestoneMeta {
  display: flex;
  gap: var(--spacing-2);
  align-items: center;
  flex-shrink: 0;
}

Severity: Medium — same CSS Modules violation; minor in isolation but accumulates across components.


Low

6. SVG donut center text uses hardcoded fontSize="24" and fontWeight="600"

File: client/src/components/TimelineStatusCards/WorkItemProgressCard.tsx, lines 115–126

SVG <text> attributes do not accept CSS var() references directly as attribute values in the same way CSS properties do, so the implementation here uses SVG presentation attributes:

<text
  fontSize="24"
  fontWeight="600"
  fill="var(--color-text-primary)"

fill="var(--color-text-primary)" is correct and will resolve in dark mode because SVG presentation attributes fall into the CSS cascade. However fontSize="24" is a hardcoded pixel value with no token equivalent. The nearest token is --font-size-2xl (24px / 1.5rem). The correct pattern for SVG text is to use the style attribute with CSS property syntax:

<text
  style={{ fontSize: 'var(--font-size-2xl)', fontWeight: 'var(--font-weight-semibold)' }}
  fill="var(--color-text-primary)"

Note: fontWeight="600" maps to --font-weight-semibold (600). Using the token ensures the value tracks any future scale change.

Severity: Low — token deviation; does not break dark mode since it is a size not a color.


7. legendDot uses hardcoded width: 8px; height: 8px

File: client/src/components/TimelineStatusCards/TimelineStatusCards.module.css, lines 104–109

.legendDot {
  width: 8px;
  height: 8px;
  ...
}

8px = --spacing-2. Use the spacing token:

.legendDot {
  width: var(--spacing-2);
  height: var(--spacing-2);
  border-radius: var(--radius-circle);
  flex-shrink: 0;
}

Severity: Low — hardcoded size; no dark mode impact but inconsistent with token discipline.


8. letter-spacing: 0.5px on .sectionHeader is a hardcoded value

File: client/src/components/TimelineStatusCards/TimelineStatusCards.module.css, line 24

letter-spacing: 0.5px;

There is no letter-spacing token in the design system. This value is acceptable as a one-off if no token covers it, but it should be documented as a deliberate deviation. Flag as informational for the style guide.

Severity: Low — no token exists; accepted as a one-off; recommend adding a comment.


Informational

9. Touch target for .link elements in list rows

File: AtRiskItemsCard.tsx — list row links

The <Link> elements in at-risk list items are inline text links with no enforced minimum height. On touch devices these will be smaller than the 44px minimum recommended by the Touch Target Sizes pattern in the style guide. Consider adding to the CSS module:

@media (max-width: 1024px) {
  .listItem {
    min-height: 44px;
  }
}

This ensures the entire row (which contains the link) meets the touch target requirement.

Severity: Informational — the touch target is the full listItem row, not just the link text, so this depends on whether the li height naturally reaches 44px with the current font-size and padding.


10. badgeGreen uses --color-success-bg (very light) — consider --color-success-badge-bg

File: TimelineStatusCards.module.css, line 57

.badgeGreen {
  background-color: var(--color-success-bg);  /* #f0fdf4 — very light wash */
  color: var(--color-success-text-on-light);
}

--color-success-bg is the lightest success surface (success banner background). For a badge/pill, the established pattern uses --color-success-badge-bg (#d1fae5) which has better contrast visibility. Compare: the completed status badge uses --color-status-completed-bg which resolves to --color-green-100 = #d1fae5. Consider aligning:

.badgeGreen {
  background-color: var(--color-success-badge-bg);
  color: var(--color-success-badge-text);
}

This also makes the On Track badge visually consistent with the existing Completed status badge.

Severity: Informational — functional but slightly inconsistent with the status badge token family.


Checklist Summary

Area Result
Token adherence (CSS module) PASS — no hardcoded hex values; spacing, font-size, radius all tokenized
Token adherence (TSX files) FAIL — hardcoded color: 'white' in CriticalPathCard; hardcoded fontSize="24" in SVG
Dark mode FAIL — color: 'white' is opaque string; Warning badge uses --color-warning as bg with no text token
Focus-visible FAIL — .link:focus-visible uses outline instead of box-shadow: var(--shadow-focus)
Responsive (767px breakpoint) PASS — grid collapses to 1-column correctly
SVG donut chart tokens PARTIAL — fill uses tokens correctly; fontSize/fontWeight are hardcoded attributes
Inline styles vs CSS Modules FAIL — CriticalPathCard and UpcomingMilestonesCard have inline style objects
Accessibility (SVG aria-label) PASS — role="img" + descriptive aria-label on SVG
Accessibility (section headers) PASS — <h3> used for each card section
Touch targets INFORMATIONAL — row height likely adequate; verify at 44px

@steilerDev steilerDev enabled auto-merge (squash) March 10, 2026 06:20
@steilerDev steilerDev merged commit 1068a12 into beta Mar 10, 2026
13 checks passed
@github-actions
Copy link
Copy Markdown
Contributor

🎉 This PR is included in version 1.15.0-beta.6 🎉

The release is available on GitHub release

Your semantic-release bot 📦🚀

steilerDev added a commit that referenced this pull request Mar 14, 2026
* feat(dashboard): implement dashboard layout & data shell (story #471)

- Create DashboardCard reusable component with loading, error, and empty states
- Add shimmer animation for skeleton loader and proper touch targets (44px min)
- Create DashboardPage with 8 dashboard cards in responsive grid
- Implement parallel data fetching via Promise.allSettled for all data sources
- Add card visibility preference management via usePreferences hook
- Implement customize dropdown to show/hide cards and manage preferences
- Add per-card error handling with retry button and empty state handling
- Map data sources (budgetOverview, budgetSources, timeline, invoices, subsidyPrograms) to cards
- Responsive grid: 3 columns (desktop), 2 columns (tablet), 1 column (mobile)
- All colors and spacing use CSS custom properties from tokens.css
- Placeholder content "Content coming soon." for all cards (filled by subsequent stories)

Co-Authored-By: Claude frontend-developer (Haiku 4.5) <noreply@anthropic.com>

* test(dashboard): add unit and integration tests for DashboardCard and DashboardPage (#471)

12 unit tests for DashboardCard covering all render states (loading, error, empty,
children), priority ordering (loading > error > empty), dismiss/retry interactions,
and prop defaults.

22 integration tests for DashboardPage covering: h1/nav rendering, all 8 card
titles visible post-load, per-data-source loading skeletons, ApiClientError
propagation, empty states for source-utilization and subsidy-pipeline, dismiss
→ upsert flow (including multi-card accumulation), hidden-card suppression,
Customize button visibility, dropdown open/close, and re-enable → upsert.

Also flags Bug #712: DashboardPage.tsx:200 reads `.items` on InvoiceListPaginatedResponse
but the actual field is `.invoices`, preventing the invoice-pipeline empty state
from ever firing.

Co-Authored-By: Claude qa-integration-tester (Sonnet 4.5) <noreply@anthropic.com>

* fix(dashboard): use correct invoice field name in empty check

The InvoiceListPaginatedResponse uses `.invoices` not `.items`.

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

* fix(test): correct mock type for upsertPreference in dashboard tests

The jest.fn generic must accept (key, value) arguments to match usage.

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

* fix(dashboard): use 'Project' heading for consistency and fix test timing

DashboardPage h1 changed from 'Dashboard' to 'Project' to match all other
pages in the Project section. Wrapped empty-state assertions in waitFor to
handle async data loading timing. Removed stale Bug #712 note.

Fixes #471

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

* fix(dashboard): address PO and UX review findings

- Use <article> + <h2> for semantic card structure with aria-labelledby
- Add card-specific empty messages with contextual action links
- Fix grid gap values, max-width, and breakpoint boundaries per spec
- Use btnSecondaryCompact for retry button via CSS composition
- Fix shimmer gradient tokens and add prefers-reduced-motion guard
- Ensure 44px minimum touch targets at all touch breakpoints
- Use var(--shadow-focus) consistently for focus-visible states

Fixes #471

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude frontend-developer (Haiku) <noreply@anthropic.com>

* fix(dashboard): close article element correctly in DashboardCard

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

* fix(dashboard): update tests for new aria-labels and page structure

- DashboardPage.test.tsx: use regex for skeleton aria-label query
- E2E DashboardPage POM: replace description locator with cardGrid
- E2E stub-pages.spec.ts: check cardGrid visibility instead of description

Fixes #471

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

---------

Co-authored-by: Claude product-architect (Opus 4.6) <noreply@anthropic.com>
@github-actions
Copy link
Copy Markdown
Contributor

🎉 This PR is included in version 1.15.0 🎉

The release is available on GitHub release

Your semantic-release bot 📦🚀

@steilerDev steilerDev deleted the feat/474-timeline-status-cards branch March 22, 2026 12:25
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants