Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .claude/agent-memory/qa-integration-tester/MEMORY.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@

## Recent bug/story notes (2026-07)

- [Issue #1815 — stylelint remediation](issue-1815-stylelint-remediation.md) (2026-07-07) — 370-violation CSS lint sweep + CI gate wiring, clean verification pass (0 bugs); @extend-bug diff-check technique, keep-list grep technique
- [Issue #1814 — i18n parity guard + usePhotos](issue-1814-i18n-parity-guard.md) (2026-07-07) — new `client/src/i18n/i18n.parity.test.ts`: generalized 14-namespace en/de key parity + 28-file duplicate-key raw-text scanner (canonical pattern going forward); `usePhotos.test.ts` reworked to assert real translateApiError/t() output; parallel translator/frontend edits landed before my first test run — all-green on first try was real, verified via git diff
- [Issue #1813 — formatter consolidation](issue-1813-formatter-consolidation.md) (2026-07-07) — "shadow render" LocaleProvider-wrap pattern; CODE_BUG found (BudgetSourcesPage.tsx `formatPercent` ReferenceError, real TS2552, latent/untested by pre-existing tests); client-project ts-jest does NOT catch this TS error class at test-run time (inline tsconfig ≠ full program) — always cross-check with `npx tsc -p client/tsconfig.json --noEmit`; SignatureCapture.tsx lines 113-127 are genuinely unreachable dead code (pre-existing, out of scope); canvas/jsdom mocking recipe for SignatureCapture.test.tsx (new file, closed test-parity gap)
- [issue-1812-i18n-sweep-json-dup-keys.md](issue-1812-i18n-sweep-json-dup-keys.md) (2026-07-07) — 3 CODE_BUGs found: diary.json duplicate top-level keys (filterBar/page/detailPage) silently wiped ~25 pre-existing translations via JSON.parse last-key-wins; GanttChart.tsx crashed (ReferenceError: t is not defined — useTranslation never added despite spec); 2 wrong-key-path typos. Detection recipe + patterns documented.
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
---
name: issue-1815-stylelint-remediation
description: QA verification of large-scale stylelint remediation (370 violations) and CI gate wiring
metadata:
type: project
---

Issue #1815 (2026-07-07): stylelint remediation across ~35 CSS/module files + `.stylelintrc.json` config
fix + `npm run stylelint` wired into root `lint`/`lint:fix` + CI `static-analysis` job. Verified clean —
no bugs found, no test assertion updates needed.

**Verification approach that worked well for a CSS-only PR with no new test files:**
- Ran the 5 touched-component test dirs batched in one `jest` invocation (29 suites / 1365 tests / 6 todo,
all green) rather than one invocation per directory — faster and the spec explicitly listed them as a
batch.
- For "keep-list" verification (duplicate-selector deletions), `grep -n "^\.selectorName"` across the
whole file is enough to prove a selector now appears exactly once (dedup confirmed) or that a specific
class survived untouched — no need to read the whole 1600-line CSS file.
- For the `@extend` bug fix (SCSS syntax silently ignored by plain CSS Modules — real latent bug, not just
a lint nit), confirmed via `git diff main -- <file>` rather than grep, since one of the six `.td*`
classes (`.tdLinkedItem`) already had its own explicit `padding: var(--spacing-2) var(--spacing-3)`
override sitting alongside the dead `@extend .td;` — the implementer correctly just deleted the `@extend`
line there instead of literally inserting `padding: var(--spacing-3)` (which would have been wrong,
overriding the more specific existing value). Worth checking the diff, not just grepping for the expected
literal string, when a spec's "apply this pattern to N instances" has an exception baked into one instance.
- `--color-warning-bg` token reuse for `InvoicePipelineCard.itemOverdue` is a deliberate normalization
(solid `#fff7ed` light / translucent `rgba(...,0.1)` dark) replacing a flat rgba that had **no** dark-mode
awareness before — this is a documented latent-dark-mode-gap fix, not a regression. Don't flag it as a
bug if you see it again in a snapshot/visual diff.
- eslint baseline check (`npx eslint .` → 8 errors, 50 warnings) is a good confirmatory sanity check even
though this PR deliberately does NOT gate CI on eslint (only stylelint) — confirms the PR didn't
introduce new eslint issues while doing 35-file CSS surgery.

No memory-worthy bugs found in this story — pure clean verification pass.
10 changes: 10 additions & 0 deletions .claude/agent-memory/ux-designer/pr-review-findings.md
Original file line number Diff line number Diff line change
Expand Up @@ -61,3 +61,13 @@ Confirmed `gh pr review --approve` (not just `--request-changes`) also fails wit
- Cannot `--request-changes` on your own PRs — use `--comment` instead, and note this in the review body
- `gh pr review --comment` via `--body-file` can fail silently — use `gh api repos/.../issues/{N}/comments` instead (the issues API works for PR comments too)
- When posting long GitHub comments with special chars (backticks, CSS `var()` calls), write to `/tmp/spec.md` (or similar) and use `--body-file`

## PR #1846 — i18n parity guard + usePhotos error translation (APPROVED via gh api comment, own PR)

Small surface: usePhotos hook errors now translated (2 new photoViewer keys `networkError`/`unexpectedError`, en+de), 5 orphaned photoAnnotator keys + dead CSS (`.resetButton`/`.modalActions`/`.confirmButton`) removed, 5 shadowed literal-duplicate JSON keys removed (diary/householdItems/schedule, both locales), new `i18n.parity.test.ts` guard (en/de key parity + raw-text duplicate-key scanner across all namespace files).

Script-verify method for duplicate-key removals: parse the pre-PR JSON with Python's `json.loads(..., object_pairs_hook=...)` tracking repeated keys per object scope — reproduces exactly what `JSON.parse`/last-wins resolves to (same semantics as the browser/webpack JSON loader). Then grep the consuming `.tsx` for the actual `t('ns:path...')` calls to confirm which of the two (shadowed vs. surviving) values was ever rendered. Found: in `schedule.json`, `milestones.detail` had two `edit` keys and two `view` keys as *direct siblings* (not nested — watch for this, easy to misread indentation and think one is nested inside the other); `MilestoneDetailPage.tsx` only ever consumes the surviving (last) `view` block's fields, the first (removed) `view` block's `linkedItems`/`workItem`/etc. were separately duplicated as sibling top-level `detail.*` keys the component actually uses — so the whole first `view` object was double-dead (both a duplicate AND functionally redundant with existing live keys).

Bonus finding (informational, not blocking, not caused by this PR): even after dedup, `schedule.json`'s surviving `detail.edit` object (`title`/`form.*`) and `diary.json`'s `create.title`/`edit.title` keys are themselves entirely unreferenced by any `t()` call — real page headings/field labels live under separate `createPage.*`/`editPage.*`/`entryForm.*`/`detail.view.*` keys. Orphan-key sweeps that target only *literal duplicates* (this PR) or only *known-dead feature keys* (the 5 photoAnnotator keys) can still leave a residue of never-referenced-at-all keys behind — worth a follow-up sweep that cross-references every namespace key against `grep -rn "t('<ns-prefix>"` call sites, not just against duplicate-key detection.

For dead-CSS verification: CSS Modules are locally scoped, so a repo-wide grep for a removed class name (e.g. `modalActions`) will hit dozens of *unrelated* components' own same-named local classes — always narrow the grep to the specific component file that imports the CSS module being edited, not the whole repo.
3 changes: 3 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,9 @@ jobs:
- name: Type check
run: npm run typecheck

- name: Stylelint
run: npm run stylelint

- name: Build
run: npm run build

Expand Down
3 changes: 2 additions & 1 deletion .stylelintrc.json
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,8 @@
"rules": {
"color-no-hex": null,
"function-disallowed-list": null,
"declaration-property-value-disallowed-list": null
"declaration-property-value-disallowed-list": null,
"color-function-alias-notation": null
}
},
{
Expand Down
2 changes: 1 addition & 1 deletion CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -370,7 +370,7 @@ Before creating a new UI component, check if an existing shared component can be
3. **Every new component must be built as a reusable shared component** — no one-off implementations. If a UI pattern doesn't fit an existing shared component, create a new shared component in `client/src/components/` that can be reused by future features
4. New shared components require UX designer visual spec approval
5. All CSS values must use design tokens from `tokens.css` — no hardcoded colors, spacing, radii, or font sizes
6. Stylelint enforces token usage automatically
6. Stylelint enforces token usage automatically (via `npm run lint` locally and the CI `static-analysis` job's `Stylelint` step; covers `client/src/**/*.css` and `client/src/**/*.module.css`, not `docs/`)

### Internationalization & Translation

Expand Down
2 changes: 1 addition & 1 deletion client/src/components/AppShell/AppShell.module.css
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@
position: fixed;
inset: 0;
background: var(--color-overlay);
z-index: 50;
z-index: var(--z-overlay);
display: none;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,7 @@
outline: none;
box-shadow: var(--shadow-focus);
position: relative;
z-index: 1;
z-index: var(--z-raised);
}

.perspectiveSegmentActive {
Expand Down Expand Up @@ -290,10 +290,6 @@
border-bottom: 1px solid var(--color-border);
}

.rowSourceDetail .colName {
border-left: 3px solid var(--color-primary-bg-hover);
}

.nameIndented {
padding-left: var(--spacing-8);
}
Expand Down Expand Up @@ -486,7 +482,7 @@
padding: 0;
margin: -1px;
overflow: hidden;
clip: rect(0, 0, 0, 0);
clip-path: inset(50%);
white-space: nowrap;
border: 0;
}
Expand Down Expand Up @@ -521,7 +517,7 @@
.colName {
position: sticky;
left: 0;
z-index: 1;
z-index: var(--z-raised);
}

/* Sticky column backgrounds must match their row level */
Expand Down Expand Up @@ -638,7 +634,6 @@
.rowLevel2,
.rowLevel3 {
break-inside: avoid;
page-break-inside: avoid;
}

.nameLink {
Expand All @@ -657,7 +652,7 @@
border-top: 3pt solid var(--color-border-strong);
border-bottom: none;
background-color: var(--color-bg-secondary) !important;
font-weight: 600;
font-weight: var(--font-weight-semibold);
}

/* ========= OUTER (area) BOX — medium gray ========= */
Expand Down Expand Up @@ -753,10 +748,6 @@
border-left: 3px solid var(--color-border);
}

.nameIndented {
padding-left: var(--spacing-8);
}

/* Toggle source detail row */
.rowSourceDetailToggle {
cursor: pointer;
Expand Down Expand Up @@ -801,7 +792,7 @@
outline: none;
box-shadow: var(--shadow-focus);
position: relative;
z-index: 1;
z-index: var(--z-raised);
}

/* Available Funds filter caption */
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -126,7 +126,7 @@
flex-shrink: 0;
}

.dayButton:hover:not(.daySelected):not(.dayDisabled) {
.dayButton:hover:not(.daySelected, .dayDisabled) {
background: var(--color-bg-hover);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@

.groupHeader {
font-size: 0.8125rem;
font-weight: 600;
font-weight: var(--font-weight-semibold);
color: var(--color-text-muted);
margin: 0;
text-transform: lowercase;
Expand Down Expand Up @@ -60,7 +60,7 @@
color: var(--color-primary);
text-decoration: none;
font-size: 0.875rem;
font-weight: 500;
font-weight: var(--font-weight-medium);
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
.diamond:hover,
.diamond:focus-visible {
/* filter uses the --milestone-hover-glow CSS var set inline per diamond */
filter: drop-shadow(0 0 5px var(--milestone-hover-glow, rgba(59, 130, 246, 0.25)));
filter: drop-shadow(0 0 5px var(--milestone-hover-glow, var(--color-milestone-hover-glow)));
}

.diamond:focus-visible {
Expand Down
2 changes: 1 addition & 1 deletion client/src/components/GanttChart/GanttSidebar.module.css
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
background: var(--color-bg-primary);
border-right: 1px solid var(--color-border-strong);
box-shadow: var(--shadow-md);
z-index: 2;
z-index: calc(var(--z-raised) + 1);
display: flex;
flex-direction: column;
overflow: hidden;
Expand Down
52 changes: 6 additions & 46 deletions client/src/components/GanttChart/GanttTooltip.module.css
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,6 @@
* ============================================================ */

.tooltip {
/* Local custom properties for tooltip surface — override in dark mode below */
--color-tooltip-subdued: rgba(255, 255, 255, 0.55);
--color-tooltip-bullet: rgba(255, 255, 255, 0.4);

position: fixed;
z-index: var(--z-modal);
background: var(--color-bg-inverse);
Expand Down Expand Up @@ -55,7 +51,7 @@

.separator {
height: 1px;
background: rgba(255, 255, 255, 0.15);
background: var(--color-tooltip-separator);
margin: var(--spacing-2) 0;
}

Expand All @@ -75,7 +71,7 @@
}

.detailLabel {
color: rgba(255, 255, 255, 0.55);
color: var(--color-tooltip-subdued);
flex-shrink: 0;
min-width: 52px;
}
Expand Down Expand Up @@ -129,22 +125,22 @@
}

.statusNotStarted {
background: rgba(156, 163, 175, 0.25);
background: var(--color-tooltip-status-not-started-bg);
color: var(--color-gray-200);
}

.statusInProgress {
background: rgba(59, 130, 246, 0.3);
background: var(--color-tooltip-status-in-progress-bg);
color: var(--color-blue-200);
}

.statusCompleted {
background: rgba(16, 185, 129, 0.3);
background: var(--color-tooltip-status-completed-bg);
color: var(--color-emerald-200);
}

.statusLate {
background: rgba(239, 68, 68, 0.3);
background: var(--color-tooltip-status-late-bg);
color: var(--color-red-200);
}

Expand Down Expand Up @@ -260,26 +256,10 @@
/* ---- Dark mode: tooltip uses --color-bg-inverse which already flips ---- */
/* In dark mode, bg-inverse = gray-100 (light), so text and badge colors flip */

[data-theme='dark'] .tooltip {
--color-tooltip-subdued: rgba(0, 0, 0, 0.5);
--color-tooltip-bullet: rgba(0, 0, 0, 0.3);

background: var(--color-bg-inverse);
color: var(--color-text-inverse);
}

[data-theme='dark'] .separator {
background: rgba(0, 0, 0, 0.15);
}

[data-theme='dark'] .title {
color: var(--color-text-inverse);
}

[data-theme='dark'] .detailLabel {
color: rgba(0, 0, 0, 0.5);
}

[data-theme='dark'] .linkedItem {
color: var(--color-text-inverse);
}
Expand All @@ -296,26 +276,6 @@
color: var(--color-red-400);
}

[data-theme='dark'] .statusNotStarted {
background: rgba(107, 114, 128, 0.2);
color: var(--color-gray-700);
}

[data-theme='dark'] .statusInProgress {
background: rgba(59, 130, 246, 0.15);
color: var(--color-primary);
}

[data-theme='dark'] .statusCompleted {
background: rgba(16, 185, 129, 0.15);
color: var(--color-green-600);
}

[data-theme='dark'] .statusLate {
background: rgba(239, 68, 68, 0.15);
color: var(--color-red-600);
}

.detailValueFloored {
color: var(--color-hi-status-scheduled-text);
font-weight: var(--font-weight-medium);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@

.itemOverdue {
border-color: var(--color-warning);
background-color: rgba(251, 146, 60, 0.08);
background-color: var(--color-warning-bg);
}

.vendorName {
Expand Down Expand Up @@ -58,7 +58,7 @@
border-radius: var(--radius-full);
font-size: var(--font-size-xs);
font-weight: var(--font-weight-medium);
background-color: rgba(251, 146, 60, 0.15);
background-color: var(--color-warning-badge-bg);
color: var(--color-warning);
white-space: nowrap;
flex-shrink: 0;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
left: 0;
right: 0;
bottom: 0;
z-index: 1000;
z-index: var(--z-modal);
display: flex;
align-items: center;
justify-content: center;
Expand Down Expand Up @@ -45,7 +45,7 @@

.modalTitle {
font-size: 1.25rem;
font-weight: 600;
font-weight: var(--font-weight-semibold);
color: var(--color-text-primary);
margin: 0;
}
Expand Down Expand Up @@ -91,7 +91,7 @@
padding: 0.75rem 1.5rem;
text-align: left;
font-size: 0.75rem;
font-weight: 600;
font-weight: var(--font-weight-semibold);
color: var(--color-text-muted);
text-transform: uppercase;
letter-spacing: 0.05em;
Expand Down Expand Up @@ -134,7 +134,7 @@
ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, 'Liberation Mono', 'Courier New',
monospace;
font-size: 0.75rem;
font-weight: 600;
font-weight: var(--font-weight-semibold);
line-height: 1;
color: var(--color-bg-inverse);
background-color: var(--color-bg-tertiary);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,7 @@
padding: 0;
margin: -1px;
overflow: hidden;
clip: rect(0, 0, 0, 0);
clip-path: inset(50%);
white-space: nowrap;
border: 0;
}
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,7 @@
border-radius: var(--radius-full);
font-size: var(--font-size-xs);
font-weight: var(--font-weight-medium);
background-color: rgba(251, 146, 60, 0.15);
background-color: var(--color-warning-badge-bg);
color: var(--color-warning);
white-space: nowrap;
flex-shrink: 0;
Expand Down
Loading
Loading