You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Severity: Medium (aggregate; two High items) — found during a deep project review (2026-07-06) against the Component Reuse Policy in CLAUDE.md. Compliant-for-balance: all *Pickers except the one below correctly wrap SearchPicker; all *Modal components use Modal; SuggestionBadge is legitimately not a Badge variant.
Parallel implementations of shared components
HIGH — components/milestones/WorkItemSelector.tsx is a full SearchPicker fork: reimplements debounced search (:77,167), click-outside close (:88-111), Escape handling (:98-102), dropdown open/loading/error state — even though WorkItemPicker (a SearchPicker wrapper for the same entity) already exists. Its own doc comment admits it "can replace linker search logic". Bonus: inline zIndex: 1100 in JS (:40).
HIGH — components/KeyboardShortcutsHelp/KeyboardShortcutsHelp.tsx:13-26 is a hand-rolled modal: own role="dialog" + backdrop + close button; unlike Modal it has no portal, no Escape handler, no initial-focus management.
components/TimelineStatusCards/UpcomingMilestonesCard.tsx:51 — inline badge span with local .badgeGreen/.badgeRed classes.
components/SubsidyPipelineCard/SubsidyPipelineCard.tsx:51-56 — third private re-creation of Badge's variant-map pattern.
documents/DocumentSkeleton.tsx — parallel skeleton implementation (own shimmer CSS) rather than extending Skeleton (judgment call: card-shaped placeholders don't fit Skeleton's current props — policy says extend with props, not fork).
components/milestones/MilestonePanel.tsx hand-rolls dialog plumbing (createPortal:140,626, role="dialog":101,364, own Escape :192-195) — judgment call (slide-in panel), but the plumbing should be extracted/shared.
Shared components bypassed wholesale
Ad-hoc error banners instead of FormError in 15+ places: <div className={styles.errorBanner} role="alert"> at pages/VendorsPage:460,594, pages/BudgetSourcesPage:757,769,950,1308, pages/WorkItemDetailPage:1333, pages/InvoicesPage:667, pages/MilestoneDetailPage:515, pages/DiaryEntryDetailPage:359, pages/MilestoneCreatePage:86, pages/HouseholdItemDetailPage:1393,1466.
Hand-rolled empty states duplicating EmptyState's API: pages/BudgetOverviewPage.tsx:323-325, pages/TimelinePage.tsx:589-604, plus simpler styles.emptyState divs in WorkItemDetailPage:1581,1669,1982,2016 (one with hardcoded English), InvoiceBudgetLinesSection:511,748, SubsidyProgramsPage:708, HouseholdItemDetailPage:1236, BudgetSourcesPage:938.
Plain-text styles.loading divs in 14+ pages instead of Skeleton (judgment call — Skeleton is line-oriented).
No useClickOutside — duplicated 5×: SearchPicker.tsx:114, WorkItemSelector.tsx:88, DataTable/DataTableHeader.tsx:37, DataTable/DataTableColumnSettings.tsx:43, OverflowMenu/OverflowMenu.tsx:58.
Modal has no focus trap — Modal.tsx:32-41 only focuses the first element on mount; photos/PhotoMetadataModal.tsx:41-63 bolts a private Tab-cycling trap on top. The trap belongs in the shared Modal so every dialog gets it.
Cleanup
components/StatusBadge/ and components/HouseholdItemStatusBadge/ contain only test files (components were consolidated into Badge; remove stale directories).
Severity: Medium (aggregate; two High items) — found during a deep project review (2026-07-06) against the Component Reuse Policy in CLAUDE.md. Compliant-for-balance: all
*Pickers except the one below correctly wrapSearchPicker; all*Modalcomponents useModal;SuggestionBadgeis legitimately not a Badge variant.Parallel implementations of shared components
components/milestones/WorkItemSelector.tsxis a full SearchPicker fork: reimplements debounced search (:77,167), click-outside close (:88-111), Escape handling (:98-102), dropdown open/loading/error state — even thoughWorkItemPicker(a SearchPicker wrapper for the same entity) already exists. Its own doc comment admits it "can replace linker search logic". Bonus: inlinezIndex: 1100in JS (:40).components/KeyboardShortcutsHelp/KeyboardShortcutsHelp.tsx:13-26is a hand-rolled modal: ownrole="dialog"+ backdrop + close button; unlikeModalit has no portal, no Escape handler, no initial-focus management.components/BudgetHealthIndicator/BudgetHealthIndicator.tsx:60-64reimplementsBadge(own<span className={styles.badge}>+ status→class variant map — literally Badge'svariants: BadgeVariantMapAPI).components/TimelineStatusCards/UpcomingMilestonesCard.tsx:51— inline badge span with local.badgeGreen/.badgeRedclasses.components/SubsidyPipelineCard/SubsidyPipelineCard.tsx:51-56— third private re-creation of Badge's variant-map pattern.documents/DocumentSkeleton.tsx— parallel skeleton implementation (own shimmer CSS) rather than extendingSkeleton(judgment call: card-shaped placeholders don't fit Skeleton's current props — policy says extend with props, not fork).components/milestones/MilestonePanel.tsxhand-rolls dialog plumbing (createPortal:140,626,role="dialog":101,364, own Escape:192-195) — judgment call (slide-in panel), but the plumbing should be extracted/shared.Shared components bypassed wholesale
FormErrorin 15+ places:<div className={styles.errorBanner} role="alert">atpages/VendorsPage:460,594,pages/BudgetSourcesPage:757,769,950,1308,pages/WorkItemDetailPage:1333,pages/InvoicesPage:667,pages/MilestoneDetailPage:515,pages/DiaryEntryDetailPage:359,pages/MilestoneCreatePage:86,pages/HouseholdItemDetailPage:1393,1466.EmptyState's API:pages/BudgetOverviewPage.tsx:323-325,pages/TimelinePage.tsx:589-604, plus simplerstyles.emptyStatedivs inWorkItemDetailPage:1581,1669,1982,2016(one with hardcoded English),InvoiceBudgetLinesSection:511,748,SubsidyProgramsPage:708,HouseholdItemDetailPage:1236,BudgetSourcesPage:938.styles.loadingdivs in 14+ pages instead ofSkeleton(judgment call — Skeleton is line-oriented).Missing shared hooks (logic copy-pasted)
useDebounce— debounced-search logic duplicated 6×:SearchPicker.tsx:91,204,milestones/WorkItemSelector.tsx:77,167,documents/DocumentBrowser.tsx:42-53,pages/DiaryPage.tsx:57-68,pages/BudgetOverviewPage.tsx:45,153(+ autosave debounceDiaryEntryEditPage.tsx:80,428).useClickOutside— duplicated 5×:SearchPicker.tsx:114,WorkItemSelector.tsx:88,DataTable/DataTableHeader.tsx:37,DataTable/DataTableColumnSettings.tsx:43,OverflowMenu/OverflowMenu.tsx:58.Modalhas no focus trap —Modal.tsx:32-41only focuses the first element on mount;photos/PhotoMetadataModal.tsx:41-63bolts a private Tab-cycling trap on top. The trap belongs in the sharedModalso every dialog gets it.Cleanup
components/StatusBadge/andcomponents/HouseholdItemStatusBadge/contain only test files (components were consolidated into Badge; remove stale directories).