From bba9b0cd8ebf7cb4ba236458f1b85ce2832e1760 Mon Sep 17 00:00:00 2001 From: Talisson Costa Date: Fri, 27 Feb 2026 23:59:07 -0300 Subject: [PATCH 01/11] chore: Add design system audit report for #6606 Scan the frontend codebase for design token misuse, dark mode gaps, and component fragmentation. Produces a structured report with 85 findings across 7 areas (colours, typography, spacing, buttons, forms, icons, notifications) and a component inventory covering 11 categories. Co-Authored-By: Claude Opus 4.6 --- frontend/DESIGN_SYSTEM_AUDIT.md | 652 +++++++++++++++++++++++++++ frontend/DESIGN_SYSTEM_AUDIT_PLAN.md | 62 +++ 2 files changed, 714 insertions(+) create mode 100644 frontend/DESIGN_SYSTEM_AUDIT.md create mode 100644 frontend/DESIGN_SYSTEM_AUDIT_PLAN.md diff --git a/frontend/DESIGN_SYSTEM_AUDIT.md b/frontend/DESIGN_SYSTEM_AUDIT.md new file mode 100644 index 000000000000..190201bef347 --- /dev/null +++ b/frontend/DESIGN_SYSTEM_AUDIT.md @@ -0,0 +1,652 @@ +# Flagsmith Frontend Design System Audit + +**Issue**: #6606 +**Date**: 2026-02-27 +**Scope**: Code-first audit of token misuse, dark mode gaps, and component fragmentation + +--- + +## Executive Summary + +This audit scanned the Flagsmith frontend codebase for design system inconsistencies across 7 areas (colours, typography, spacing, buttons, forms, icons, notifications) and catalogued 11 UI component categories. + +### Key findings + +| Severity | Count | Description | +|----------|-------|-------------| +| **P0** | 21 | Broken in dark mode or accessibility issue | +| **P1** | 34 | Visual inconsistency with the token system | +| **P2** | 30 | Token hygiene (hardcoded value that should use a variable) | + +### Top 5 fixes by impact + +1. **Icon.tsx default fills** — ~60 icons default to `#1A2634` (near-black), invisible in dark mode. Switching to `currentColor` would fix all at once. +2. **Feature pipeline status** — Entire release pipeline visualisation is broken in dark mode (white circles, grey lines on dark background). +3. **Chart components** — All 4 Recharts-based charts use hardcoded light-mode axis/grid colours. Need dark mode conditionals. +4. **Button variants missing dark mode** — `btn-tertiary`, `btn-danger`, and `btn--transparent` have no dark mode overrides. +5. **Toast notifications** — No dark mode styles at all. + +### Top 5 consolidation opportunities + +1. **Icons** — Refactor the monolithic `Icon.tsx` (70 inline SVGs), unify with 19 separate SVG components and `IonIcon` usage. +2. **Modals** — Migrate from global imperative API, consolidate 13+ near-identical confirmation modals. +3. **Notifications** — Remove 2 duplicate legacy `.js` message components, unify 4 alert variants into a single `Alert` component. +4. **Menus/Dropdowns** — Extract a shared dropdown primitive from 4 independent implementations. +5. **Layout** — Convert legacy JS class components (`Flex.js`, `Column.js`), remove Material UI dependency from `AccordionCard`. + +--- + +# Part A — Component Inventory + +## 1. Modals/Dialogs + +**Files**: 52 files in `web/components/modals/` + +**Base infrastructure** (6 files): +- `web/components/modals/base/Modal.tsx` — Core modal system: `openModal`, `openModal2`, `openConfirm` globals +- `web/components/modals/base/ModalDefault.tsx` — Standard modal wrapper using reactstrap `Modal` +- `web/components/modals/base/ModalConfirm.tsx` — Confirmation dialog (yes/no with danger variant) +- `web/components/modals/base/ModalAlert.tsx` — Simple alert modal with single OK button +- `web/components/modals/base/ModalHeader.tsx` — Custom header with close button +- `web/components/modals/base/ModalClose.tsx` — Close button component + +**Confirmation modals** (13 files): +`ConfirmCloneSegment`, `ConfirmDeleteAccount`, `ConfirmDeleteRole`, `ConfirmHideFlags`, `ConfirmRemoveAuditWebhook`, `ConfirmRemoveEnvironment`, `ConfirmRemoveFeature`, `ConfirmRemoveOrganisation`, `ConfirmRemoveProject`, `ConfirmRemoveSegment`, `ConfirmRemoveTrait`, `ConfirmRemoveWebhook`, `ConfirmToggleFeature`, `ConfirmToggleEnvFeature` + +**Creation/editing modals** (15+ files): +`CreateAuditLogWebhook`, `CreateEditIntegrationModal`, `CreateGroup`, `CreateMetadataField`, `CreateOrganisation`, `CreateProject.js`, `CreateRole`, `CreateSAML`, `CreateSegment`, `CreateTrait`, `CreateUser`, `CreateWebhook.js`, `CreateWebhook.tsx`, `ChangeEmailAddress`, `ChangeRequestModal`, `ForgotPasswordModal`, `InviteUsers`, `Payment.js` + +**Complex multi-tab modals** (2 subdirectories): +- `create-feature/` (7 files) +- `create-experiment/` (2 files) + +**Also**: `web/components/InlineModal.tsx` — separate inline (non-overlay) modal + +**Variant count**: 4 base modal types (ModalDefault, ModalConfirm, ModalAlert, InlineModal) + +**Pattern issues**: +- Modal system uses deprecated `react-dom` `render`/`unmountComponentAtNode` (removed in React 18) and attaches `openModal`/`closeModal` to `global` +- `openModal2` exists for stacking modals, acknowledged as a pattern to avoid +- Duplicate: `CreateWebhook.js` and `CreateWebhook.tsx` coexist +- `CreateProject.js` and `Payment.js` remain as unconverted JS class components +- 13+ confirmation modals each implement their own layout rather than composing from a shared template +- `InlineModal` has `displayName = 'Popover'`, which is misleading + +**Consolidation notes**: The 13+ confirmation modals follow nearly identical patterns (title, message, ModalHR, Cancel/Confirm buttons) and could be a single configurable `ConfirmModal`. The global `openModal`/`closeModal` imperative API should migrate to a React context-based modal manager. + +--- + +## 2. Menus/Dropdowns + +**Files**: +- `web/components/base/DropdownMenu.tsx` — Icon-triggered vertical "more" menu using portal positioning +- `web/components/base/forms/ButtonDropdown.tsx` — Split button with dropdown actions +- `web/components/base/Popover.tsx` — Toggle-based popover using `FocusMonitor` HOC +- `web/components/InlineModal.tsx` — Panel-style dropdown (used by TableFilter) + +**Variant count**: 4 distinct dropdown/overlay patterns + +**Pattern issues**: +- `DropdownMenu` and `ButtonDropdown` both implement their own outside-click handling, positioning logic, and dropdown rendering independently +- Both share CSS class `feature-action__list` / `feature-action__item` but are not composed from shared primitives +- `Popover` uses a different mechanism (`FocusMonitor` HOC) for state +- `InlineModal` functions as another dropdown variant but is named "Modal" + +**Consolidation notes**: A single base `Dropdown`/`Popover` primitive with portal support, outside-click handling, and positioning could replace all four. + +--- + +## 3. Selects + +**Files**: 28 files (2 base + 26 domain-specific) + +**Base**: `web/components/base/select/SearchableSelect.tsx`, `web/components/base/select/multi-select/MultiSelect.tsx` + +**Domain-specific** (26): `EnvironmentSelect`, `EnvironmentTagSelect`, `OrgEnvironmentSelect`, `OrganisationSelect.js`, `ProjectSelect.js`, `FlagSelect.js`, `SegmentSelect`, `GroupSelect`, `ConnectedGroupSelect`, `MyGroupsSelect`, `UserSelect`, `RolesSelect`, `MyRoleSelect`, `IntegrationSelect`, `DateSelect`, `ConversionEventSelect`, `IdentitySelect`, `GitHubRepositoriesSelect`, `GitHubResourcesSelect`, `MyRepositoriesSelect`, `RepositoriesSelect`, `ColourSelect`, `SupportedContentTypesSelect`, `SelectOrgAndProject`, `RuleConditionPropertySelect`, `EnvironmentSelectDropdown` + +**Pattern issues**: 3 remain as `.js` files. Potential overlap between `GroupSelect`/`ConnectedGroupSelect`/`MyGroupsSelect`. + +**Consolidation notes**: The large number of domain selects is reasonable since each encapsulates data fetching. Ensure all consistently use `SearchableSelect` or `MultiSelect` as their base. Convert `.js` files to TypeScript. + +--- + +## 4. Toasts/Notifications + +**Files**: + +Toast system: +- `web/project/toast.tsx` — Global toast with `success` and `danger` themes, attached to `window.toast` + +Inline alert components (6 files, 2 duplicated): +- `web/components/messages/ErrorMessage.tsx` — TypeScript FC version +- `web/components/messages/SuccessMessage.tsx` — TypeScript FC version +- `web/components/ErrorMessage.js` — Legacy class component duplicate +- `web/components/SuccessMessage.js` — Legacy class component duplicate +- `web/components/InfoMessage.tsx` — Info alert with collapsible content +- `web/components/WarningMessage.tsx` — Warning alert + +**Variant count**: 1 toast system (2 themes) + 4 inline alert variants + +**Pattern issues**: +- `ErrorMessage` and `SuccessMessage` each exist in two versions (legacy `.js` + modern `.tsx`) +- Toast only supports `success` and `danger` — no `info` or `warning` toast themes +- Toast is attached to `window` as a global function + +**Consolidation notes**: Remove legacy `.js` duplicates. Unify all inline alerts into a single `Alert` component with a `variant` prop. Add `info`/`warning` themes to the toast system. + +--- + +## 5. Tables & Filters + +**Files**: 1 core table + 9 filter components + +**Core**: `web/components/PanelSearch.tsx` — Searchable, paginated, sortable list using `react-virtualized` + +**Filters** (in `web/components/tables/`): +`TableFilter`, `TableFilterItem`, `TableFilterOptions`, `TableGroupsFilter`, `TableOwnerFilter`, `TableSearchFilter`, `TableSortFilter`, `TableTagFilter`, `TableValueFilter` + +**Pattern issues**: `PanelSearch` is monolithic (20+ props). Uses `react-virtualized` (older library). + +**Consolidation notes**: Filter components are well-structured around `TableFilter` base. Consider decomposing `PanelSearch` into smaller composable pieces. + +--- + +## 6. Tabs + +**Files**: +- `web/components/navigation/TabMenu/Tabs.tsx` — Main container with `tab` and `pill` themes, URL param sync, overflow handling +- `web/components/navigation/TabMenu/TabItem.tsx` — Tab content wrapper +- `web/components/navigation/TabMenu/TabButton.tsx` — Tab button using `Button` component + +**Variant count**: 1 implementation with 2 themes + +**Pattern issues**: Well-consolidated. Minor prop bloat (13 props including feature-specific `isRoles`). + +--- + +## 7. Buttons + +**Files**: `web/components/base/forms/Button.tsx` + +**Themes** (9): `primary`, `secondary`, `tertiary`, `danger`, `success`, `text`, `outline`, `icon`, `project` + +**Sizes** (5): `default`, `large`, `small`, `xSmall`, `xxSmall` + +**Pattern issues**: Doubles as a link when `href` is provided. Plan-gating (`feature` prop) is baked in rather than being a wrapper. + +**Consolidation notes**: Clean system overall. Consider extracting link behaviour and plan-gating concern. + +--- + +## 8. Icons + +**Files**: +- `web/components/Icon.tsx` — 70 inline SVG icons in a switch statement +- `web/components/svg/` — 19 standalone SVG components: `ArrowUpIcon`, `AuditLogIcon`, `CaretDownIcon`, `CaretRightIcon`, `DocumentationIcon`, `DropIcon`, `EnvironmentSettingsIcon`, `FeaturesIcon`, `LogoutIcon`, `NavIconSmall`, `OrgSettingsIcon`, `PlayIcon`, `PlusIcon`, `ProjectSettingsIcon`, `SegmentsIcon`, `SparklesIcon`, `UpgradeIcon`, `UserSettingsIcon`, `UsersIcon` + +**Also**: `@ionic/react` `IonIcon` used in `InfoMessage`, `SuccessMessage`, `AccordionCard` — a third icon system. + +**Variant count**: 70 inline + 19 SVG files + IonIcon = 3 separate icon systems, ~89 total icons + +**Pattern issues**: +- `Icon.tsx` is extremely large because every icon is an inline SVG in a switch +- The 19 `svg/` components are completely separate, not accessible via `Icon.tsx` +- `IonIcon` adds a third, heavy dependency for just a few icons + +**Consolidation notes**: Highest priority. Refactor `Icon.tsx` to import individual SVG files. Integrate `svg/` components. Migrate `IonIcon` usage. Consider SVG sprites or individual imports for tree-shaking. + +--- + +## 9. Empty States + +**Files**: +- `web/components/EmptyState.tsx` — Generic empty state with icon, title, description, docs link, action +- `web/components/pages/features/components/FeaturesEmptyState.tsx` — Specialised onboarding variant + +**Variant count**: 2 (1 generic + 1 specialised) + +**Pattern issues**: `FeaturesEmptyState` does not use the generic `EmptyState`. Reasonable separation since it serves a different purpose (onboarding walkthrough). + +--- + +## 10. Tooltips + +**Files**: +- `web/components/Tooltip.tsx` — Main tooltip using `react-tooltip`, with HTML sanitisation and portal support +- `web/components/base/LabelWithTooltip.tsx` — Convenience wrapper: label + info icon tooltip + +**Variant count**: 2 (1 core + 1 convenience wrapper) + +**Pattern issues**: +- Inverted API: `title` is the trigger element and `children` is the tooltip content +- `children` rendered via `dangerouslySetInnerHTML` (sanitised with DOMPurify) +- New `id` (GUID) generated on every render +- `TooltipPortal` creates DOM nodes but never removes them (memory leak) + +--- + +## 11. Layout + +**Files**: + +Grid primitives in `web/components/base/grid/`: +- `Panel.tsx` — Panel with optional title/action header (class component) +- `FormGroup.tsx` — Simple `.form-group` wrapper +- `Row.tsx` — Flex row with `space` and `noWrap` props +- `Flex.js` — Legacy flex wrapper (class component, `module.exports`) +- `Column.js` — Legacy flex-column wrapper (class component, `module.exports`) + +Composite: +- `web/components/PanelSearch.tsx` — Searchable panel/list +- `web/components/base/accordion/AccordionCard.tsx` — Collapsible card using Material UI's `Collapse` and `IconButton` + +**Pattern issues**: +- `Panel` is a class component (`PureComponent`) +- `Flex.js` and `Column.js` are the oldest-style components (class, `module.exports`, `propTypes`, globals) +- `AccordionCard` depends on `@material-ui/core` — the only place Material UI appears, adding a heavy dependency + +**Consolidation notes**: Convert `Panel`, `Flex.js`, `Column.js` to TypeScript FCs. Replace Material UI in `AccordionCard` with CSS transition or `
`/``. Consider removing `Flex`/`Column` given modern CSS utility classes. + +--- + +# Part B — Token & Consistency Findings + +## 1. Colours + +### P0 — Broken Dark Mode / Accessibility + +**1.1 Icon.tsx — ~60 icons default to `#1A2634` fill, invisible in dark mode (CRITICAL)** +- `web/components/Icon.tsx` — lines 225, 482, 559, 578, 597, 616, 636, 655, 674, 693, 712, 731, 751, 771, 790, 809, 828, 867, 888, 908, 928, 1011, 1031, 1050, 1069, 1089, 1108, 1127, 1146, 1165, 1185, 1205, 1224, 1244, 1251, 1258, 1278, 1328, 1345, 1365, 1406 +- Pattern: `fill={fill || '#1A2634'}` — dark navy fill is invisible on dark backgrounds unless every caller passes an explicit fill +- **Fix**: Switch default to `currentColor` or add `getDarkMode()` awareness + +**1.2 Icon.tsx — 3 icons hardcode `#163251`, an orphan colour** +- `web/components/Icon.tsx:237,245,253` — `fill || '#163251'` — not in any token + +**1.3 Icon.tsx `expand` icon defaults to `fill='#000000'`** +- `web/components/Icon.tsx:1502` — black fill invisible on dark backgrounds + +**1.4 Feature pipeline status — entire component has no `.dark` override** +- `web/styles/components/_feature-pipeline-status.scss:2,4,16,20,30,39,43,52` +- Hardcoded: `white` (bg), `#AAA` (border/bg), `#6837fc` (border), `#53af41` (border/bg) +- White circles and grey lines invisible on dark backgrounds + +**1.5 FeaturesPage action item — hardcoded dark text** +- `web/styles/project/_FeaturesPage.scss:42` — `color: #2d3443` +- Nearly invisible on dark backgrounds. Value doesn't match `$body-color` (`#1a2634`) + +**1.6 Striped section — no dark mode override** +- `web/styles/styles.scss:66` — `background-color: #f7f7f7` +- Should use `$bg-light200`; light grey will clash with dark body + +**1.7 Alert bar — orphan colour, no dark override** +- `web/styles/styles.scss:172,175` — `color: #fff`, `background-color: #384f68` +- `#384f68` not in any token definition + +**1.8 Modal tab dropdown hover — wrong purple, no dark override** +- `web/styles/project/_modals.scss:294` — `background-color: #6610f210 !important` +- `#6610f2` does not match `$primary` (`#6837fc`); non-standard 8-digit hex + +**1.9 BooleanDotIndicator — orphan disabled colour** +- `web/components/BooleanDotIndicator.tsx:4` — `enabled ? '#6837fc' : '#dbdcdf'` +- `#dbdcdf` is an orphan; disabled dot barely visible in dark mode + +**1.10 DateSelect — dark fill invisible** +- `web/components/DateSelect.tsx:136` — `fill={isOpen ? '#1A2634' : '#9DA4AE'}` +- `#1A2634` invisible on dark backgrounds + +### P1 — Visual Inconsistency + +**1.11 Charts use hardcoded light-mode colours (4 files)** +- `web/components/organisation-settings/usage/OrganisationUsage.container.tsx:44-63` +- `web/components/organisation-settings/usage/components/SingleSDKLabelsChart.tsx:42-62` +- `web/components/feature-page/FeatureNavTab/FeatureAnalytics.tsx:101-107` +- `web/components/modals/create-experiment/ExperimentResultsTab.tsx:81-129` +- Hardcoded: `stroke='#EFF1F4'` (grid), `fill: '#656D7B'` (tick), `fill: '#1A2634'` (tick) +- All charts render light-mode colours on dark backgrounds + +**1.12 `#53af41` orphan green used across release pipeline** +- `web/components/release-pipelines/StageStatus.tsx:91` +- `web/components/release-pipelines/StageSummaryData.tsx:57,79` +- `web/styles/components/_feature-pipeline-status.scss:20,39,52` +- Not in token palette; closest is `$success` (`#27ab95`) + +**1.13 Unread badge — off-brand primary** +- `web/styles/project/_utils.scss:155` — `background: #7b51fb` +- Should use `$primary` (`#6837fc`) + +**1.14 Button remove hover — inconsistent red** +- `web/styles/project/_buttons.scss:183` — `fill: #d93939` +- Neither `$danger` nor `$btn-danger-hover`; a third red shade + +**1.15 Panel change-request — raw rgba with wrong base colour** +- `web/styles/components/_panel.scss:225,228-229,233,240` +- `rgba(102, 51, 255, 0.08)` uses `#6633ff` not `#6837fc`; should use `$primary-alfa-8` + +**1.16 ArrowUpIcon / DocumentationIcon / Logo — off-brand purple** +- `web/components/svg/ArrowUpIcon.tsx:18`, `web/components/svg/DocumentationIcon.tsx:23`, `web/components/Logo.tsx:20` +- `fill='#63f'` = `#6633ff`, not `$primary` (`#6837fc`) + +**1.17 `#53af41` orphan green in CreatePipelineStage** +- `web/components/release-pipelines/CreatePipelineStage.tsx:180,184,187` +- Hardcoded danger colour with inline box-shadow + +**1.18 VCSProviderTag — orphan dark colour** +- `web/components/tags/VCSProviderTag.tsx:47` — `backgroundColor: isWarning ? '#ff9f43' : '#343a40'` +- `#343a40` not in any token + +**1.19 PanelSearch — inline style colours bypass dark mode** +- `web/components/PanelSearch.tsx:225,230` — `style={{ color: isActive ? '#6837FC' : '#656d7b' }}` +- Should use CSS classes with `.dark` support + +**1.20 FeaturesPage hover uses full `$primary` in dark mode** +- `web/styles/project/_FeaturesPage.scss:72` — `.dark .feature-action__item:hover` uses `background: $primary` +- Light mode uses `$bg-light200`; dark mode is jarring/oversaturated + +**1.21 Step list — orphan colour** +- `web/styles/project/_lists.scss:9,14,21` — `#2e2e2e` (border and background) +- Not in the token palette + +### P2 — Token Hygiene + +**1.22 ~35 TSX files hardcode `fill='#9DA4AE'` / `fill='#656D7B'` / `fill='#6837FC'`** +- Values match tokens (`$text-icon-light-grey`, `$text-icon-grey`, `$primary`) but bypass the token system +- If token values change, these will not update +- Files include: `ChangeRequestsList`, `SegmentOverrideActions`, `TopNavbar`, `AccountDropdown`, `SDKKeysPage`, `RolesTable`, `SamlTab`, `GroupSelect`, `UserGroupList`, and many more + +**1.23 Admin dashboard tables — ~20 orphan colours in inline styles** +- `web/components/pages/admin-dashboard/components/OrganisationUsageTable.tsx` +- `web/components/pages/admin-dashboard/components/ReleasePipelineStatsTable.tsx` +- `web/components/pages/admin-dashboard/components/IntegrationAdoptionTable.tsx` +- Colours: `#f8f9fa`, `#fafbfc`, `#eee`, `#e74c3c`, `#e9ecef`, `#e0e0e0`, `#f4f5f7` + +**1.24 Language colour blocks — intentional but undocumented** +- `web/styles/components/_color-block.scss:21-36` — GitHub language colours (`#3572A5`, `#f1e05a`, etc.) +- Should be extracted to named variables + +**1.25 GitHub icon colours — brand colours** +- `web/components/Icon.tsx:1417-1488` — `#8957e5`, `#238636`, `#da3633` +- Likely intentional but should be named constants + +**1.26 SidebarLink — orphan muted colour** +- `web/components/navigation/SidebarLink.tsx:42` — `fill={'#767D85'}` +- Closest token is `$text-icon-grey` (`#656D7B`) + +**1.27 CreateFeature tab — orphan purple** +- `web/components/modals/create-feature/tabs/CreateFeature.tsx:107` — `color: '#6A52CF'` + +--- + +## 2. Typography + +### Systemic Issues + +**Missing general-purpose font-weight tokens**: The token system only defines weight variables scoped to components (`$btn-font-weight: 700`, `$input-font-weight: 500`). No general tokens like `$font-weight-regular: 400`, `$font-weight-medium: 500`, `$font-weight-semibold: 600`, `$font-weight-bold: 700`. + +**Off-scale values indicate design drift**: Values like 9px, 10px, 15px, 17px, 19px, 20px, 22px, 34px appear but have no corresponding token. + +### P0 + +**2.1 FeaturesPage — hardcoded dark text colour** +- `web/styles/project/_FeaturesPage.scss:42` — `color: #2d3443` with no adequate dark mode override +- (Also listed under Colours 1.5) + +**2.2 Dark mode text colour gaps in inline styles** +- `web/components/pages/admin-dashboard/components/OrganisationUsageTable.tsx:21` — `color: '#e74c3c'` +- `web/components/pages/admin-dashboard/components/ReleasePipelineStatsTable.tsx:404` — `color: '#27AB95'` +- `web/components/tags/TagFilter.tsx:45` — `color: '#656D7B'` +- `web/components/navigation/AccountDropdown.tsx:80` — `color: '#656D7B'` +- Inline colours cannot respond to dark mode + +### P1 — Hardcoded Font-Size Off-Scale + +| File | Line | Value | Notes | +|------|------|-------|-------| +| `web/styles/3rdParty/_hljs.scss` | 100 | `font-size: 17px` | Between `$h6` (16px) and `$h5` (18px) | +| `web/styles/3rdParty/_hw-badge.scss` | 13 | `font-size: 9px` | Below `$font-caption-xs` (11px) | +| `web/styles/project/_utils.scss` | 164 | `font-size: 10px` | Below scale | +| `web/styles/project/_alert.scss` | 52 | `font-size: 20px` | Between `$h5` (18px) and `$h4` (24px) | +| `web/styles/project/_icons.scss` | 9 | `font-size: 19px` | Between `$h5` (18px) and `$h4` (24px) | +| `web/components/pages/admin-dashboard/components/ReleasePipelineStatsTable.tsx` | 282 | `fontSize: 10` | Below scale | + +### P1 — Hardcoded Line-Height Off-Scale + +| File | Line | Value | Notes | +|------|------|-------|-------| +| `web/styles/components/_input.scss` | 71 | `line-height: 22px` | Between `$line-height-sm` (20px) and `$line-height-lg` (24px) | +| `web/styles/components/_paging.scss` | 14 | `line-height: 34px` | Above `$line-height-xlg` (28px) | +| `web/styles/components/_tabs.scss` | 167 | `line-height: 15px` | Below `$line-height-xxsm` (16px) | +| `web/styles/project/_utils.scss` | 163 | `line-height: 14px` | Below scale | + +### P1 — Inline Font Weight 600 (no token exists) + +| File | Line | Value | +|------|------|-------| +| `web/components/pages/admin-dashboard/components/OrganisationUsageTable.tsx` | 21, 27 | `fontWeight: 600` | +| `web/components/pages/admin-dashboard/components/ReleasePipelineStatsTable.tsx` | 283 | `fontWeight: 600` | +| `web/components/navigation/AccountDropdown.tsx` | 83 | `fontWeight: 600` | + +### P2 — Hardcoded Font-Size On-Scale + +| File | Line | Value | Token | +|------|------|-------|-------| +| `web/styles/styles.scss` | 185 | `font-size: 30px` | `$h3-font-size` | +| `web/styles/styles.scss` | 191 | `font-size: 14px` | `$font-size-base` | +| `web/styles/styles.scss` | 193 | `font-size: 11px` | `$font-caption-xs` | +| `web/styles/components/_switch.scss` | 79 | `font-size: 14px` | `$font-size-base` | +| `web/styles/components/_metrics.scss` | 15 | `font-size: 16px` | `$h6-font-size` | +| `web/styles/project/_type.scss` | 104 | `font-size: 0.75rem` | `$font-caption-sm` (12px) | + +### P2 — Admin Dashboard Inline fontSize Hotspot + +The files under `web/components/pages/admin-dashboard/components/` contain over 50 inline `fontSize` overrides across `OrganisationUsageTable.tsx`, `ReleasePipelineStatsTable.tsx`, and `IntegrationAdoptionTable.tsx`. These bypass both the SCSS token system and dark mode theming. This is the single largest area of typography token misuse. + +--- + +## 3. Spacing + +### P1 — Off-Grid Spacing Values + +| File | Line | Value | Issue | +|------|------|-------|-------| +| `web/styles/project/_buttons.scss` | 149 | `padding: 19px 0 18px 0` | Both off-grid; asymmetric by 1px | +| `web/styles/styles.scss` | 173 | `padding: 15px` | Off-grid; should be 16px (`$spacer`) | +| `web/styles/styles.scss` | 206 | `padding: 10px 10px 5px 0` | 10px and 5px both off-grid | +| `web/styles/components/_chip.scss` | 42 | `padding: 5px 12px` | 5px off-grid (chip is heavily reused) | +| `web/styles/components/_chip.scss` | 84 | `padding: 3px 8px` | 3px off-grid | +| `web/styles/components/_chip.scss` | 85, 89, 101, 113 | `margin-right: 5px` | 5px off-grid (repeated 4 times) | +| `web/styles/project/_FeaturesPage.scss` | 11, 24 | `margin-top: 6px`, `margin-bottom: 6px` | 6px off-grid | +| `web/styles/components/_metrics.scss` | 11 | `margin-bottom: 6px` | 6px off-grid | +| `web/styles/project/_forms.scss` | 149 | `margin-bottom: 6px` | 6px off-grid | +| `web/styles/project/_alert.scss` | 4, 7, 14 | `margin-top: 60px, 130px, 70px` | Layout offsets coupled to fixed header height | + +### P1 — Inconsistent Spacing Across Similar Components + +**Chip margin-right inconsistency**: +- `.chip`: `margin-right: 0.5rem` (8px) — good +- `.chip--sm`: `margin-right: 5px` — off-grid +- `.chip--xs`: `margin-right: 5px` — off-grid +- `.chip .margin-right`: `12px` — different value entirely + +### P2 — On-Grid But Hardcoded + +| File | Line | Value | Notes | +|------|------|-------|-------| +| `web/styles/project/_panel.scss` | 43 | `padding: 20px` | Should use `$spacer * 1.25` | +| `web/styles/project/_modals.scss` | 106 | `padding: 20px` | Inconsistent with `$modal-body-padding-y` (24px) | +| `web/styles/project/_utils.scss` | 78, 82 | `margin-top: 20px`, `margin-bottom: 20px` | Custom utility duplicating Bootstrap `.mt-4`/`.mb-4` | +| `web/styles/project/_utils.scss` | 175 | `padding: 10px` | Off-grid | +| `web/styles/styles.scss` | 161 | `margin-left: 10px` | Off-grid | +| `web/styles/3rdParty/_hljs.scss` | 184, 186 | `padding: 7px 12px`, `margin-right: 5px` | Off-grid | +| `web/styles/components/_switch.scss` | 80 | `padding-left: 10px` | Off-grid | + +### Systemic Patterns + +- **5px is the most common off-grid offender** (10+ occurrences). Bulk replace to 4px would be highest-impact single change. +- **12px is an unofficial spacing token** used 5+ times for `margin-right`. Should be formalised as `$spacer * 0.75`. +- **6px appears as a small vertical spacer** in 3+ files. Standardise to 4px or 8px. + +--- + +## 4. Button Styles + +### P0 + +**4.1 `btn--transparent` hover — no dark mode override** +- `web/styles/project/_buttons.scss:155` — hover uses `rgba(0,0,0,0.1)`, nearly invisible on dark backgrounds + +**4.2 `btn-tertiary` — no dark mode override** +- `web/styles/project/_buttons.scss:113-127` — gold/yellow variant with `$primary900` text. Not overridden in `.dark` block (lines 432-563). May be unreadable on dark backgrounds. + +**4.3 `btn-danger` — dark hover resolves to primary colour** +- `web/styles/project/_buttons.scss:32-43` — `.dark` block does not include `btn-danger` override. Generic `.dark .btn:hover` (line 436) overwrites danger-specific hover with primary colour. + +### P1 + +**4.4 `btn--outline` hardcodes `background: white`** +- `web/styles/project/_buttons.scss:59` — should be `$bg-light100` or `$input-bg` + +**4.5 `btn-link .dark-link` — invisible in dark mode** +- `web/styles/project/_buttons.scss:379` — `.dark-link` sets `color: $body-color` (dark text), no dark override + +### P2 + +**4.6 `btn--remove` hover — orphan red `#d93939`** +- `web/styles/project/_buttons.scss:183` — not a design token + +**4.7 Excessive `!important` in button styles** +- `web/styles/project/_buttons.scss:60,86-87,100-101,104,109,114,201,209,228,238,252-257` +- At least 15 `!important` declarations creating specificity debt + +**4.8 Inline style overrides on Button components in TSX** +- Multiple TSX files apply `style={{...}}` directly on ` + + + {/* Hover note — we describe it rather than simulate, as CSS :hover cannot be forced */} +
+ Hover via CSS +
+ (see description) +
+ + {/* Disabled state */} +
+ +
+ + {/* Description */} +

+ {description} +

+ + ) + + if (highlightGap) { + return ( +
+
+ + No .dark override + +
+ {row} + {noDarkReason && ( +

+ {noDarkReason} +

+ )} +
+ ) + } + + return row +} + +// --------------------------------------------------------------------------- +// Meta +// --------------------------------------------------------------------------- + +const meta: Meta = { + parameters: { layout: 'padded' }, + title: 'Design System/Buttons', +} +export default meta + +// --------------------------------------------------------------------------- +// Stories +// --------------------------------------------------------------------------- + +export const AllVariants: StoryObj = { + render: () => ( +
+

+ Button Variants ({variants.length}) +

+

+ Source: web/styles/project/_buttons.scss. All variants use + raw HTML <button> elements with Bootstrap/project CSS + classes — there is no React Button component. Toggle dark mode in the + Storybook toolbar to see how each variant responds. +

+ + {/* Legend */} +
+
+ + + No .dark override in _buttons.scss + +
+
+ + {/* Column headers */} +
+ {['Variant', 'Default', 'Hover', 'Disabled', 'Notes'].map((h) => ( + + {h} + + ))} +
+ + {/* Rows */} +
+ {variants.map((variant) => ( + + ))} +
+
+ ), +} + +export const DarkModeGaps: StoryObj = { + render: () => ( +
+

+ Dark Mode Gaps ({noDarkOverrideVariants.length} variants) +

+

+ These button variants have no override rules inside the{' '} + .dark .btn block in{' '} + web/styles/project/_buttons.scss (lines 432–563). Each one + either looks wrong in dark mode or relies on accident rather than + intention. +

+

+ Enable dark mode via the Storybook toolbar (moon icon) to see the issues + live. +

+ + {noDarkOverrideVariants.map((variant) => { + const buttonContent = variant.iconOnly ? ( + + ) : ( + variant.label + ) + + return ( +
+ {/* Header */} +
+ + No .dark override + + + {variant.label} + +
+ + {/* Button states side by side */} +
+
+
+ Default +
+ +
+
+
+ Disabled +
+ +
+
+ + {/* Issue description */} +

+ {variant.description} +

+ + {/* Dark mode specific callout */} + {variant.noDarkReason && ( +
+

+ Dark mode issue: {variant.noDarkReason} +

+
+ )} +
+ ) + })} + + {/* Summary table */} +

+ Recommended fixes +

+ + + + {['Variant', 'Issue', 'Fix'].map((h) => ( + + ))} + + + + {[ + [ + 'btn-tertiary', + 'Yellow (#F7D56E) background unchanged in dark mode; hover colour not re-specified.', + 'Add .dark .btn.btn-tertiary { background-color: $btn-tertiary-bg-dark ($secondary600); } and re-specify hover.', + ], + [ + 'btn-danger', + 'Hover (#cd384d) and active (#ac2646) colours are hardcoded light-mode values; no dark adjustment.', + 'Add .dark .btn.btn-danger { &:hover, &:focus { background-color: $danger400; } } to use the lighter danger shade.', + ], + [ + 'btn--transparent', + 'Hover is rgba(0,0,0,0.1) — nearly invisible on the dark background (#101628).', + 'Add .dark .btn.btn--transparent { &:hover { background-color: $white-alpha-8; } }', + ], + ].map(([variant, issue, fix]) => ( + + + + + + ))} + +
+ {h} +
+ {variant} + + {issue} + + {fix} +
+
+ ), +} + +export const SizeVariants: StoryObj = { + render: () => ( +
+

+ Size Variants +

+

+ Button sizes are controlled by modifier classes. Heights and + line-heights come from _variables.scss. Shown here with{' '} + btn-primary and btn-secondary for contrast. +

+ + {[ + { height: '56px', label: 'btn-lg', modifier: 'btn-lg' }, + { height: '44px', label: '(default)', modifier: '' }, + { height: '40px', label: 'btn-sm', modifier: 'btn-sm' }, + { height: '32px', label: 'btn-xsm', modifier: 'btn-xsm' }, + { height: '24px', label: 'btn-xxsm', modifier: 'btn-xxsm' }, + ].map(({ height, label, modifier }) => ( +
+ + {label} + + + h={height} + + + +
+ ))} +
+ ), +} diff --git a/frontend/stories/ColourPalette.stories.tsx b/frontend/stories/ColourPalette.stories.tsx new file mode 100644 index 000000000000..53a197bc7392 --- /dev/null +++ b/frontend/stories/ColourPalette.stories.tsx @@ -0,0 +1,1175 @@ +import React from 'react' +import type { Meta, StoryObj } from '@storybook/react' + +const meta: Meta = { + parameters: { layout: 'padded' }, + title: 'Design System/Colours/Palette Audit', +} +export default meta + +// --------------------------------------------------------------------------- +// Shared primitives +// --------------------------------------------------------------------------- + +const fontStack = "'OpenSans', sans-serif" + +function getBadgeForLabel( + label: string, +): { colour: string; text: string } | undefined { + if (label === '$primary400') { + return { colour: '#ef4d56', text: 'LIGHTER than base' } + } + if (label === '$primary (base)') { + return { colour: '#27ab95', text: 'Base' } + } + return undefined +} + +function getBadgeForSlot( + label: string, +): { colour: string; text: string } | undefined { + if (label === 'primary-500') { + return { colour: '#27ab95', text: 'Existing $primary' } + } + if (label === 'primary-400') { + return { colour: '#6837fc', text: 'Correct 400 slot' } + } + return undefined +} + +const Swatch: React.FC<{ + hex: string + label: string + sublabel?: string + width?: number + height?: number + badge?: { text: string; colour: string } +}> = ({ badge, height = 56, hex, label, sublabel, width = 56 }) => ( +
+
+
+ {badge && ( +
+ {badge.text} +
+ )} +
+
+
+ {label} +
+ {sublabel && ( +
+ {sublabel} +
+ )} +
+
+) + +const SwatchRow: React.FC<{ swatches: React.ReactNode; label?: string }> = ({ + label, + swatches, +}) => ( +
+ {label && ( +
+ {label} +
+ )} +
+ {swatches} +
+
+) + +const IssueCallout: React.FC<{ children: React.ReactNode }> = ({ + children, +}) => ( +
+ {children} +
+) + +const SectionHeading: React.FC<{ children: React.ReactNode }> = ({ + children, +}) => ( +

+ {children} +

+) + +// --------------------------------------------------------------------------- +// Story 1: TonalScaleInconsistency +// --------------------------------------------------------------------------- + +export const TonalScaleInconsistency: StoryObj = { + render: () => { + const actualScale = [ + { hex: '#906af6', label: '$primary400', note: 'Lighter' }, + { hex: '#6837fc', label: '$primary (base)', note: 'Base' }, + { hex: '#4e25db', label: '$primary600', note: 'Darker' }, + { hex: '#3919b7', label: '$primary700', note: 'Darker' }, + { hex: '#2a2054', label: '$primary800', note: 'Darker' }, + { hex: '#1e0d26', label: '$primary900', note: 'Darkest' }, + ] + + // What a correct 50–900 scale would look like (illustrative, HSL-derived from #6837fc) + const correctScale = [ + { hex: '#f0ebff', label: 'primary-50', note: 'Lightest' }, + { hex: '#ddd0ff', label: 'primary-100', note: '' }, + { hex: '#c4aaff', label: 'primary-200', note: '' }, + { hex: '#a882ff', label: 'primary-300', note: '' }, + { + hex: '#8b5cff', + label: 'primary-400', + note: 'Should be darker than 300', + }, + { hex: '#6837fc', label: 'primary-500', note: 'Base (current $primary)' }, + { hex: '#4e25db', label: 'primary-600', note: 'Matches $primary600' }, + { hex: '#3919b7', label: 'primary-700', note: 'Matches $primary700' }, + { hex: '#2a2054', label: 'primary-800', note: 'Matches $primary800' }, + { hex: '#1e0d26', label: 'primary-900', note: 'Matches $primary900' }, + ] + + return ( +
+

+ Tonal Scale Inconsistency +

+

+ The convention for numbered colour scales is: higher numbers = darker + shades. The current primary scale violates this at{' '} + $primary400, which is lighter than the + unnumbered $primary base. There is also no{' '} + $primary100, $primary200, or{' '} + $primary300 — the scale jumps straight from 400 to the + base, with no low-numbered light stops defined at all. +

+ + + Issue: $primary400 (#906AF6) is{' '} + lighter than $primary (#6837FC). In a + conventional 50–900 scale, 400 should sit below the 500-equivalent + base, not above it. This makes the scale non-intuitive and + error-prone: a developer reaching for a lighter variant correctly + picks 400 and gets the wrong result. + + + + Current scale — as defined in _variables.scss + + ( + + ))} + /> + +
+ Note: $primary-faded (#e2d4fe) is referenced in component + code but is not defined in _variables.scss. It + appears as an orphan hex. It would naturally belong at the primary-100 + or primary-50 level. +
+ + + What a correct 50–900 scale would look like + +

+ Illustrative target — HSL-interpolated from the existing base + (#6837FC). The 600–900 range already matches existing variables; the + gap is the 50–400 range. +

+ ( + + ))} + /> +
+ ) + }, +} + +// --------------------------------------------------------------------------- +// Story 2: AlphaColourMismatches +// --------------------------------------------------------------------------- + +export const AlphaColourMismatches: StoryObj = { + render: () => { + type AlphaEntry = { + name: string + solidHex: string + solidRgb: string + alphaBaseRgb: string + alphaBaseHex: string + alphaVars: Array<{ label: string; actual: string; correct: string }> + } + + const entries: AlphaEntry[] = [ + { + alphaBaseHex: '#956cff', + alphaBaseRgb: '149, 108, 255', + alphaVars: [ + { + actual: 'rgba(149,108,255,0.08)', + correct: 'rgba(104,55,252,0.08)', + label: 'alfa-8', + }, + { + actual: 'rgba(149,108,255,0.16)', + correct: 'rgba(104,55,252,0.16)', + label: 'alfa-16', + }, + { + actual: 'rgba(149,108,255,0.24)', + correct: 'rgba(104,55,252,0.24)', + label: 'alfa-24', + }, + { + actual: 'rgba(149,108,255,0.32)', + correct: 'rgba(104,55,252,0.32)', + label: 'alfa-32', + }, + ], + name: 'Primary', + solidHex: '#6837fc', + solidRgb: '104, 55, 252', + }, + { + alphaBaseHex: '#ff424b', + alphaBaseRgb: '255, 66, 75', + alphaVars: [ + { + actual: 'rgba(255,66,75,0.08)', + correct: 'rgba(239,77,86,0.08)', + label: 'alfa-8', + }, + { + actual: 'rgba(255,66,75,0.16)', + correct: 'rgba(239,77,86,0.16)', + label: 'alfa-16', + }, + ], + name: 'Danger', + solidHex: '#ef4d56', + solidRgb: '239, 77, 86', + }, + { + alphaBaseHex: '#ff9f00', + alphaBaseRgb: '255, 159, 0', + alphaVars: [ + { + actual: 'rgba(255,159,0,0.08)', + correct: 'rgba(255,159,67,0.08)', + label: 'alfa-8', + }, + ], + name: 'Warning', + solidHex: '#ff9f43', + solidRgb: '255, 159, 67', + }, + ] + + return ( +
+

+ Alpha Colour Mismatches +

+

+ Alpha variants (e.g. $primary-alfa-8) should be derived + from the same RGB values as their solid counterpart. In this codebase, + all three checked colours use adifferent RGB base for their + alpha variants. This causes the alpha overlays to tint differently + from the solid colour they are meant to complement. +

+ + + Issue: $primary-alfa-* uses RGB (149, + 108, 255) — which corresponds to $primary400 (#956CFF), + not $primary (#6837FC / 104, 55, 252). The danger and + warning alphas similarly derive from undocumented RGB values that do + not match their solid variables. + + + {entries.map((entry) => ( +
+ {entry.name} + +
+
+
+ Solid token +
+ +
+ +
+
+ Alpha base actually used +
+ +
+
+ +
+ Alpha variants: actual (left) vs correct (right) +
+ +
+ {entry.alphaVars.map((v) => ( +
+
+ -{v.label} +
+
+
+
+
+ ACTUAL +
+
+
+ vs +
+
+
+
+ CORRECT +
+
+
+ + {v.actual} + +
+ ))} +
+
+ ))} +
+ ) + }, +} + +// --------------------------------------------------------------------------- +// Story 3: OrphanHexValues +// --------------------------------------------------------------------------- + +export const OrphanHexValues: StoryObj = { + render: () => { + type OrphanEntry = { + hex: string + uses: number + description: string + shouldMapTo: string | null + } + + const orphans: OrphanEntry[] = [ + { + description: 'Mid grey — most used orphan', + hex: '#9DA4AE', + shouldMapTo: + '$text-icon-light-grey (rgba(157,164,174,1) ≈ same colour)', + uses: 52, + }, + { + description: 'Red — not using $danger', + hex: '#e74c3c', + shouldMapTo: '$danger (#ef4d56)', + uses: 8, + }, + { + description: 'Green — not using $success', + hex: '#53af41', + shouldMapTo: '$success (#27ab95)', + uses: 5, + }, + { + description: 'Grey', + hex: '#767d85', + shouldMapTo: 'No exact match — nearest $text-icon-grey (#656d7b)', + uses: 7, + }, + { + description: 'Grey-blue', + hex: '#5D6D7E', + shouldMapTo: 'No exact match — nearest $text-icon-grey (#656d7b)', + uses: 4, + }, + { + description: 'Dark grey', + hex: '#343a40', + shouldMapTo: 'No exact match — nearest $body-color (#1a2634)', + uses: 6, + }, + { + description: 'GitHub purple', + hex: '#8957e5', + shouldMapTo: 'No variable — third-party brand colour', + uses: 3, + }, + { + description: 'GitHub green', + hex: '#238636', + shouldMapTo: 'No variable — third-party brand colour', + uses: 3, + }, + { + description: 'GitHub red', + hex: '#da3633', + shouldMapTo: 'No variable — third-party brand colour', + uses: 3, + }, + { + description: 'Navy', + hex: '#1c2840', + shouldMapTo: 'No exact match — nearest $bg-dark500 (#101628)', + uses: 2, + }, + ] + + const isThirdParty = (entry: OrphanEntry) => + entry.shouldMapTo?.startsWith('No variable — third-party') ?? false + const hasNearMatch = (entry: OrphanEntry) => + !isThirdParty(entry) && + (entry.shouldMapTo?.startsWith('No exact') ?? false) + const hasExactMap = (entry: OrphanEntry) => + !isThirdParty(entry) && !hasNearMatch(entry) + + const tagColour = (entry: OrphanEntry) => { + if (isThirdParty(entry)) return '#0aaddf' + if (hasNearMatch(entry)) return '#ff9f43' + return '#ef4d56' + } + + const tagLabel = (entry: OrphanEntry) => { + if (isThirdParty(entry)) return '3rd party' + if (hasNearMatch(entry)) return 'near match' + return 'has variable' + } + + return ( +
+

+ Orphan Hex Values +

+

+ These hex values appear directly in component files — not referencing + any SCSS variable or design token. They bypass the design system and + will not respond to theme changes or future token migrations. The + usage counts are approximate from a codebase grep. +

+ + + Issue: #9DA4AE appears in 52 places + across the codebase. It is visually equivalent to{' '} + $text-icon-light-grey (rgba(157,164,174,1)) but is + written as a raw hex. Any change to the grey text colour will miss + these 52 instances. Similarly, #e74c3c and{' '} + #53af41 duplicate existing semantic variables ( + $danger and $success) without referencing + them. + + +
+ Legend: + {[ + { colour: '#ef4d56', label: 'Has a variable — should be replaced' }, + { colour: '#ff9f43', label: 'Near match — needs review' }, + { colour: '#0aaddf', label: 'Third-party brand colour' }, + ].map(({ colour, label }) => ( + + + {label} + + ))} +
+ +
+ {orphans.map((entry) => ( +
+
+
+
+ + {entry.hex} + + + {tagLabel(entry)} + +
+
+ {entry.description} +
+
+ {entry.shouldMapTo ?? 'No match'} +
+
+ {entry.uses} uses in codebase +
+
+
+ ))} +
+
+ ) + }, +} + +// --------------------------------------------------------------------------- +// Story 4: GreyScaleGaps +// --------------------------------------------------------------------------- + +export const GreyScaleGaps: StoryObj = { + render: () => { + type GreyEntry = { + variable: string + hex: string + role: string + } + + const currentGreys: GreyEntry[] = [ + { hex: '#656d7b', role: 'Body / icon text', variable: '$text-icon-grey' }, + { + hex: 'rgba(157,164,174,1)', + role: 'Muted / placeholder text', + variable: '$text-icon-light-grey', + }, + { + hex: '#ffffff', + role: 'Page background (light)', + variable: '$bg-light100', + }, + { hex: '#fafafb', role: 'Panel background', variable: '$bg-light200' }, + { + hex: '#eff1f4', + role: 'Subtle surface / pills', + variable: '$bg-light300', + }, + { + hex: '#e0e3e9', + role: 'Border / divider surface', + variable: '$bg-light500', + }, + { + hex: '#e7e7e7', + role: 'Footer — no exact variable', + variable: '$footer-grey (approx)', + }, + { + hex: '#2d3443', + role: 'Dark surface (dark mode only)', + variable: '$bg-dark100', + }, + { + hex: '#202839', + role: 'Dark surface — code bg', + variable: '$bg-dark200', + }, + { hex: '#161d30', role: 'Dark panel secondary', variable: '$bg-dark300' }, + { hex: '#15192b', role: 'Dark panel / modal', variable: '$bg-dark400' }, + { hex: '#101628', role: 'Dark page background', variable: '$bg-dark500' }, + ] + + // A well-structured neutral 50–900 scale (illustrative target) + const idealGreys = [ + { hex: '#f9fafb', note: '≈ $bg-light200 (#fafafb)', step: 'neutral-50' }, + { hex: '#f3f4f6', note: '≈ $bg-light300 (#eff1f4)', step: 'neutral-100' }, + { hex: '#e5e7eb', note: '≈ $bg-light500 (#e0e3e9)', step: 'neutral-200' }, + { hex: '#d1d5db', note: 'Missing — no variable', step: 'neutral-300' }, + { + hex: '#9ca3af', + note: '≈ $text-icon-light-grey / #9DA4AE orphan', + step: 'neutral-400', + }, + { + hex: '#6b7280', + note: '≈ $text-icon-grey (#656d7b)', + step: 'neutral-500', + }, + { hex: '#4b5563', note: 'Missing — no variable', step: 'neutral-600' }, + { hex: '#374151', note: 'Missing — no variable', step: 'neutral-700' }, + { hex: '#1f2937', note: '≈ $bg-dark200 (#202839)', step: 'neutral-800' }, + { hex: '#111827', note: '≈ $bg-dark500 (#101628)', step: 'neutral-900' }, + ] + + return ( +
+

+ Grey Scale Gaps +

+

+ The current grey system has no systematic numbering. Greys are split + across two naming conventions ($bg-light* /{' '} + $bg-dark* for surfaces, and $text-icon-* for + foreground) with no unified neutral scale. Light and dark backgrounds + are defined in entirely separate variable sets with no tonal + relationship between them. Several mid-grey values (neutral-300 to + neutral-700) are completely absent, which is why orphan hex values + like #767d85 and #5D6D7E appear in + components. +

+ + + Issue: There is no $bg-light400 (it + skips from 300 to 500), the dark scale goes in the opposite direction + (100 is the lightest dark shade, 500 the darkest), and there is no + cross-referencing between the light and dark ends of the scale. A + developer cannot predictably choose a surface colour by number. + + + + Current greys — as defined in _variables.scss + +

+ Shown lightest to darkest. Note: there is no $bg-light400{' '} + step, and the dark-mode backgrounds form a completely separate set + rather than extending the same scale. +

+
+ {currentGreys.map((g) => ( +
+
+
+ {g.variable} +
+
+ {g.hex} +
+
+ {g.role} +
+
+ ))} +
+ + + What a proper neutral-50 through neutral-900 scale would look like + +

+ A unified scale unifies foreground and background colours into one + progressive sequence. Existing SCSS variables map closely to several + steps — shown in the annotations below. The gaps (300, 600, 700) are + where orphan hex values currently fill in ad hoc. +

+
+ {idealGreys.map((g) => { + const isGap = g.note.startsWith('Missing') + return ( +
+
+
+ {isGap && ( +
+ GAP +
+ )} +
+
+ {g.step} +
+
+ {g.hex} +
+
+ {g.note} +
+
+ ) + })} +
+ +
+ + Recommendation: + {' '} + Define a single $neutral-50 through{' '} + $neutral-900 scale, then alias the existing semantic + names ($text-icon-grey, $bg-light*,{' '} + $bg-dark*) as references to the appropriate steps. This + eliminates the need for orphan hex values and makes dark-mode surface + colours predictable by number. +
+
+ ) + }, +} diff --git a/frontend/stories/Colours.stories.tsx b/frontend/stories/Colours.stories.tsx new file mode 100644 index 000000000000..d4718303cbfe --- /dev/null +++ b/frontend/stories/Colours.stories.tsx @@ -0,0 +1,346 @@ +import React from 'react' +import type { Meta, StoryObj } from '@storybook/react' +import * as tokens from 'common/theme/tokens' + +// --------------------------------------------------------------------------- +// Helpers +// --------------------------------------------------------------------------- + +const Swatch: React.FC<{ name: string; value: string }> = ({ name, value }) => ( +
+
+
+
+ {name} +
+ + {value} + +
+
+) + +const Section: React.FC<{ title: string; children: React.ReactNode }> = ({ + children, + title, +}) => ( +
+

+ {title} +

+
+ {children} +
+
+) + +// --------------------------------------------------------------------------- +// Token groups — extracted from the token file +// --------------------------------------------------------------------------- + +const tokenGroups: Record> = { + Brand: { + colorBrandPrimary: tokens.colorBrandPrimary, + colorBrandPrimaryActive: tokens.colorBrandPrimaryActive, + colorBrandPrimaryAlpha16: tokens.colorBrandPrimaryAlpha16, + colorBrandPrimaryAlpha24: tokens.colorBrandPrimaryAlpha24, + colorBrandPrimaryAlpha8: tokens.colorBrandPrimaryAlpha8, + colorBrandPrimaryHover: tokens.colorBrandPrimaryHover, + colorBrandSecondary: tokens.colorBrandSecondary, + colorBrandSecondaryHover: tokens.colorBrandSecondaryHover, + }, + Feedback: { + colorFeedbackDanger: tokens.colorFeedbackDanger, + colorFeedbackDangerLight: tokens.colorFeedbackDangerLight, + colorFeedbackDangerSurface: tokens.colorFeedbackDangerSurface, + colorFeedbackInfo: tokens.colorFeedbackInfo, + colorFeedbackInfoSurface: tokens.colorFeedbackInfoSurface, + colorFeedbackSuccess: tokens.colorFeedbackSuccess, + colorFeedbackSuccessLight: tokens.colorFeedbackSuccessLight, + colorFeedbackSuccessSurface: tokens.colorFeedbackSuccessSurface, + colorFeedbackWarning: tokens.colorFeedbackWarning, + colorFeedbackWarningSurface: tokens.colorFeedbackWarningSurface, + }, + Icon: { + colorIconInverse: tokens.colorIconInverse, + colorIconSecondary: tokens.colorIconSecondary, + colorIconStandard: tokens.colorIconStandard, + colorIconTertiary: tokens.colorIconTertiary, + }, + Interactive: { + colorInteractiveSecondary: tokens.colorInteractiveSecondary, + colorInteractiveSecondaryActive: tokens.colorInteractiveSecondaryActive, + colorInteractiveSecondaryHover: tokens.colorInteractiveSecondaryHover, + colorInteractiveSwitchOff: tokens.colorInteractiveSwitchOff, + colorInteractiveSwitchOffHover: tokens.colorInteractiveSwitchOffHover, + }, + Stroke: { + colorStrokeFocus: tokens.colorStrokeFocus, + colorStrokeInput: tokens.colorStrokeInput, + colorStrokeInputFocus: tokens.colorStrokeInputFocus, + colorStrokeInputHover: tokens.colorStrokeInputHover, + colorStrokeInverse: tokens.colorStrokeInverse, + colorStrokeSecondary: tokens.colorStrokeSecondary, + colorStrokeStandard: tokens.colorStrokeStandard, + }, + Surface: { + colorSurfaceInput: tokens.colorSurfaceInput, + colorSurfaceInverse: tokens.colorSurfaceInverse, + colorSurfaceModal: tokens.colorSurfaceModal, + colorSurfaceMuted: tokens.colorSurfaceMuted, + colorSurfacePanel: tokens.colorSurfacePanel, + colorSurfacePanelSecondary: tokens.colorSurfacePanelSecondary, + colorSurfaceSecondary: tokens.colorSurfaceSecondary, + colorSurfaceStandard: tokens.colorSurfaceStandard, + colorSurfaceTertiary: tokens.colorSurfaceTertiary, + }, + Text: { + colorTextHeading: tokens.colorTextHeading, + colorTextInverse: tokens.colorTextInverse, + colorTextOnBrand: tokens.colorTextOnBrand, + colorTextSecondary: tokens.colorTextSecondary, + colorTextStandard: tokens.colorTextStandard, + colorTextTertiary: tokens.colorTextTertiary, + }, +} + +// --------------------------------------------------------------------------- +// Current SCSS variables — documents what exists today (before token migration) +// --------------------------------------------------------------------------- + +const currentScssVars: Record< + string, + Record +> = { + 'Backgrounds': { + '$bg-dark100': { dark: '#2d3443', light: '(unused in light)' }, + '$bg-light100 / $bg-dark500': { dark: '#101628', light: '#ffffff' }, + '$bg-light200 / $bg-dark400': { dark: '#15192b', light: '#fafafb' }, + '$bg-light300 / $bg-dark300': { dark: '#161d30', light: '#eff1f4' }, + '$bg-light500 / $bg-dark200': { dark: '#202839', light: '#e0e3e9' }, + }, + 'Body / Text': { + '$body-color': { dark: 'white', light: '#1a2634' }, + '$header-color': { dark: '#ffffff', light: '#1e0d26' }, + '$text-icon-grey': { dark: '(no override)', light: '#656d7b' }, + '$text-icon-light-grey': { + dark: '(no override)', + light: 'rgba(157,164,174,1)', + }, + }, + 'Borders / Strokes': { + '$basic-alpha-16 / $white-alpha-16': { + dark: 'rgba(255,255,255,0.16)', + light: 'rgba(101,109,123,0.16)', + }, + '$input-border-color': { dark: '#15192b', light: 'rgba(101,109,123,0.16)' }, + '$panel-border-color': { + dark: 'rgba(255,255,255,0.16)', + light: 'rgba(101,109,123,0.16)', + }, + }, + 'Brand Palette': { + '$danger': { dark: '#ef4d56', light: '#ef4d56' }, + '$info': { dark: '#0aaddf', light: '#0aaddf' }, + '$primary': { dark: '#6837fc', light: '#6837fc' }, + '$primary400': { dark: '#906af6', light: '#906af6' }, + '$primary600': { dark: '#4e25db', light: '#4e25db' }, + '$success': { dark: '#27ab95', light: '#27ab95' }, + '$warning': { dark: '#ff9f43', light: '#ff9f43' }, + }, +} + +const ScssVarRow: React.FC<{ name: string; light: string; dark: string }> = ({ + dark, + light, + name, +}) => ( + + + {name} + + +
+
+ {light} +
+ + +
+
+ {dark} +
+ + +) + +// --------------------------------------------------------------------------- +// Stories +// --------------------------------------------------------------------------- + +const meta: Meta = { + parameters: { + layout: 'padded', + }, + title: 'Design System/Colours', +} +export default meta + +export const SemanticTokens: StoryObj = { + render: () => ( +
+

+ Semantic Colour Tokens +

+

+ These tokens automatically adapt to light/dark mode via CSS custom + properties. Toggle the theme in the toolbar above to preview both modes. +
+ Source: common/theme/tokens.ts +{' '} + web/styles/_tokens.scss +

+ {Object.entries(tokenGroups).map(([group, values]) => ( +
+ {Object.entries(values).map(([name, value]) => ( + + ))} +
+ ))} +
+ ), +} + +export const CurrentScssVariables: StoryObj = { + render: () => ( +
+

+ Current SCSS Variables +

+

+ The existing colour system in _variables.scss. Each + variable has a separate $*-dark counterpart used in{' '} + .dark selectors. This is what the token migration will + consolidate. +

+ {Object.entries(currentScssVars).map(([section, vars]) => ( +
+

+ {section} +

+ + + + + + + + + + {Object.entries(vars).map(([name, { dark, light }]) => ( + + ))} + +
+ Variable + + Light + + Dark +
+
+ ))} +
+ ), +} diff --git a/frontend/stories/DarkModeIssues.stories.tsx b/frontend/stories/DarkModeIssues.stories.tsx new file mode 100644 index 000000000000..f6362c5520e3 --- /dev/null +++ b/frontend/stories/DarkModeIssues.stories.tsx @@ -0,0 +1,679 @@ +import React from 'react' +import type { Meta, StoryObj } from '@storybook/react' + +// --------------------------------------------------------------------------- +// Meta +// --------------------------------------------------------------------------- + +const meta: Meta = { + parameters: { layout: 'padded' }, + title: 'Design System/Dark Mode Issues', +} +export default meta + +// --------------------------------------------------------------------------- +// Data +// --------------------------------------------------------------------------- + +const hardcodedColourRows = [ + { + code: "fill='#1A2634'", + colour: '#1A2634', + component: 'StatItem', + file: 'web/components/StatItem.tsx', + issue: 'Invisible in dark mode', + line: 43, + }, + { + code: "fill={checked ? '#656D7B' : '#1A2634'}", + colour: '#1A2634', + component: 'Switch', + file: 'web/components/Switch.tsx', + issue: '#1A2634 invisible in dark mode', + line: 57, + }, + { + code: "fill={isOpen ? '#1A2634' : '#9DA4AE'}", + colour: '#1A2634', + component: 'DateSelect', + file: 'web/components/DateSelect.tsx', + issue: '#1A2634 invisible in dark mode', + line: 136, + }, + { + code: "fill={'#1A2634'}", + colour: '#1A2634', + component: 'ScheduledChangesPage', + file: 'web/components/pages/ScheduledChangesPage.tsx', + issue: 'Invisible in dark mode', + line: 126, + }, + { + code: "tick={{ fill: '#1A2634' }}", + colour: '#1A2634', + component: 'OrganisationUsage', + file: 'web/components/organisation-settings/usage/OrganisationUsage.container.tsx', + issue: 'Chart labels invisible in dark mode', + line: 63, + }, + { + code: "tick={{ fill: '#1A2634' }}", + colour: '#1A2634', + component: 'SingleSDKLabelsChart', + file: 'web/components/organisation-settings/usage/components/SingleSDKLabelsChart.tsx', + issue: 'Chart labels invisible in dark mode', + line: 62, + }, +] + +// --------------------------------------------------------------------------- +// Shared styles +// --------------------------------------------------------------------------- + +const tableHeaderStyle: React.CSSProperties = { + borderBottom: '2px solid var(--colorStrokeStandard)', + color: 'var(--colorTextSecondary)', + fontSize: 13, + fontWeight: 600, + padding: '8px 12px', + textAlign: 'left', + whiteSpace: 'nowrap', +} + +const tableCellStyle: React.CSSProperties = { + borderBottom: '1px solid var(--colorStrokeStandard)', + color: 'var(--colorTextStandard)', + fontSize: 13, + padding: '10px 12px', + verticalAlign: 'middle', +} + +const codeStyle: React.CSSProperties = { + background: 'var(--colorSurfacePanel)', + borderRadius: 4, + color: 'var(--colorTextSecondary)', + fontFamily: "'Fira Code', 'Consolas', 'Courier New', monospace", + fontSize: 12, + padding: '2px 6px', + wordBreak: 'break-all', +} + +const badgeStyle: React.CSSProperties = { + background: 'rgba(239,77,86,0.12)', + borderRadius: 4, + color: '#EF4D56', + display: 'inline-block', + fontSize: 11, + fontWeight: 700, + padding: '2px 8px', + whiteSpace: 'nowrap', +} + +// --------------------------------------------------------------------------- +// Story 1: HardcodedColoursInComponents +// --------------------------------------------------------------------------- + +export const HardcodedColoursInComponents: StoryObj = { + render: () => ( +
+

+ Hardcoded Colours in Components +

+

+ These components pass #1A2634 as a + hardcoded fill to icons or chart tick props. In dark mode the background + is #101628, making the element + effectively invisible. Each value should be replaced with a CSS custom + property or currentColor. +

+ + + + + + + + + + + + + + + {hardcodedColourRows.map((row) => ( + + + + + + + + + + ))} + +
ComponentFileLineColourCodeIssueReplace with
+ {row.component} + + {row.file} + + {row.line} + +
+
+ {row.colour} +
+
+ {row.code} + + {row.issue} + + + {row.code.includes('tick') + ? "tick={{ fill: 'var(--colorTextStandard)' }}" + : "fill='currentColor'"} + +
+
+ ), +} + +// --------------------------------------------------------------------------- +// Story 2: DarkModeImplementationPatterns +// --------------------------------------------------------------------------- + +const patternBlockStyle: React.CSSProperties = { + background: 'var(--colorSurfacePanel)', + border: '1px solid var(--colorStrokeStandard)', + borderRadius: 8, + marginBottom: 24, + padding: 20, +} + +const patternTitleStyle: React.CSSProperties = { + color: 'var(--colorTextHeading)', + fontSize: 15, + fontWeight: 700, + marginBottom: 4, + marginTop: 0, +} + +const patternSubtitleStyle: React.CSSProperties = { + color: 'var(--colorTextSecondary)', + fontSize: 13, + marginBottom: 12, +} + +const preStyle = (borderColour: string): React.CSSProperties => ({ + background: '#0d1117', + borderLeft: `4px solid ${borderColour}`, + borderRadius: 6, + color: '#e6edf3', + fontFamily: "'Fira Code', 'Consolas', 'Courier New', monospace", + fontSize: 13, + lineHeight: 1.6, + margin: '0 0 12px 0', + overflow: 'auto', + padding: '14px 18px', +}) + +const problemPillStyle: React.CSSProperties = { + alignItems: 'center', + background: 'rgba(239,77,86,0.12)', + borderRadius: 20, + color: '#EF4D56', + display: 'inline-flex', + fontSize: 12, + fontWeight: 600, + gap: 6, + marginTop: 4, + padding: '4px 10px', +} + +const warningPillStyle: React.CSSProperties = { + alignItems: 'center', + background: 'rgba(255,159,67,0.12)', + borderRadius: 20, + color: '#FF9F43', + display: 'inline-flex', + fontSize: 12, + fontWeight: 600, + gap: 6, + marginTop: 4, + padding: '4px 10px', +} + +const infoPillStyle: React.CSSProperties = { + alignItems: 'center', + background: 'rgba(10,173,223,0.12)', + borderRadius: 20, + color: '#0AADDF', + display: 'inline-flex', + fontSize: 12, + fontWeight: 600, + gap: 6, + marginTop: 4, + padding: '4px 10px', +} + +export const DarkModeImplementationPatterns: StoryObj = { + render: () => ( +
+

+ Dark Mode Implementation Patterns +

+

+ Three parallel mechanisms handle dark mode today. Each solves a + different layer of the problem, but none covers everything — so + components fall through the cracks. +

+ + {/* Pattern 1 */} +
+

+ Pattern 1 — SCSS .dark selectors +

+

+ 48 rules across 29 files. Applied by toggling the .dark{' '} + class on document.body. +

+
+          {`.dark .panel {
+  background-color: $panel-bg-dark;
+}
+
+.dark .aside-nav {
+  background: $aside-nav-bg-dark;
+  border-right: 1px solid $nav-line-dark;
+}`}
+        
+ + Problem: compile-time only — cannot be used in inline styles or JS + expressions + +
+ + {/* Pattern 2 */} +
+

+ Pattern 2 — getDarkMode() runtime ternaries +

+

+ 13 components call getDarkMode() to branch on colour + values at render time. +

+
+          {`// From Switch.tsx (line 57)
+const color = getDarkMode() ? '#ffffff' : '#1A2634'
+
+// Or inline
+
+
+// From OrganisationUsage.container.tsx (line 63)
+tick={{ fill: getDarkMode() ? '#ffffff' : '#1A2634' }}`}
+        
+ + Problem: manual ternaries, easy to forget, requires re-render on theme + change + +
+ + {/* Pattern 3 */} +
+

+ Pattern 3 — Bootstrap data-bs-theme attribute +

+

+ Set on document.documentElement alongside the{' '} + .dark class. Intended to unlock Bootstrap's built-in + dark mode variables, but adoption is inconsistent. +

+
+          {`// Set in theme toggle handler
+document.documentElement.setAttribute('data-bs-theme', 'dark')
+document.body.classList.add('dark')
+
+// Bootstrap provides these automatically when data-bs-theme="dark":
+// --bs-body-bg, --bs-body-color, --bs-border-color, etc.
+// But our custom components don't use Bootstrap tokens consistently.`}
+        
+ + Problem: conflicts with .dark class approach; + inconsistent adoption across custom components + +
+ + {/* Proposed solution */} +
+

+ Proposed solution — CSS custom properties (design tokens) +

+

+ A single source of truth. The token value changes per theme; + components never branch on theme state. +

+
+          {`:root {
+  --colorTextStandard: #1A2634;
+  --colorIconStandard: #1A2634;
+}
+
+[data-bs-theme="dark"],
+.dark {
+  --colorTextStandard: #ffffff;
+  --colorIconStandard: #ffffff;
+}
+
+/* Component just uses the token — no ternary, no getDarkMode() */
+`}
+        
+ + Single mechanism, no runtime branching, works in SCSS and inline + styles + +
+
+ ), +} + +// --------------------------------------------------------------------------- +// Story 3: ThemeTokenComparison +// --------------------------------------------------------------------------- + +const comparisonCardStyle = (accent: string): React.CSSProperties => ({ + background: 'var(--colorSurfacePanel)', + border: `1px solid ${accent}`, + borderRadius: 8, + display: 'flex', + flex: 1, + flexDirection: 'column', + gap: 12, + minWidth: 0, + padding: 20, +}) + +const comparisonHeadingStyle = (colour: string): React.CSSProperties => ({ + borderBottom: `1px solid var(--colorStrokeStandard)`, + color: colour, + fontSize: 14, + fontWeight: 700, + margin: 0, + paddingBottom: 8, +}) + +export const ThemeTokenComparison: StoryObj = { + render: () => ( +
+

+ Theme Token Comparison +

+

+ Side-by-side view of the current approach versus token-based theming. + The goal is to remove all dark mode awareness from component code. +

+ + {/* Row 1: Icon fill */} +
+

+ Example 1 — Icon fill colour +

+
+
+

Before (current)

+
+              {`// Component has to know about dark mode
+const color = getDarkMode()
+  ? '#ffffff'
+  : '#1A2634'
+
+`}
+            
+
    +
  • Component coupled to theme state
  • +
  • Re-renders needed on theme change
  • +
  • Easy to forget one branch
  • +
  • Raw hex values scattered across codebase
  • +
+
+ +
+

After (with tokens)

+
+              {`// Component just uses semantic token
+import { colorIconStandard }
+  from 'common/theme'
+
+
+
+// Or directly via CSS variable
+`}
+            
+
    +
  • Component has zero theme knowledge
  • +
  • CSS updates the value — no re-render
  • +
  • One token covers both light and dark
  • +
  • Token name documents intent
  • +
+
+
+
+ + {/* Row 2: Chart tick */} +
+

+ Example 2 — Recharts tick colour +

+
+
+

Before (current)

+
+              {`// OrganisationUsage.container.tsx, line 63
+// Chart labels are invisible in dark mode
+
+
+`}
+            
+
+ +
+

After (with tokens)

+
+              {`// Reads computed token at render time
+const textColour = getComputedStyle(
+  document.documentElement
+).getPropertyValue('--colorTextStandard').trim()
+
+
+
+`}
+            
+
+
+
+ + {/* Token mapping table */} +
+

+ Proposed token mapping +

+ + + + + + + + + + + {[ + { + dark: '#ffffff', + light: '#1A2634', + replaces: "fill='#1A2634' (icons)", + token: '--colorIconStandard', + }, + { + dark: '#ffffff', + light: '#1A2634', + replaces: "tick={{ fill: '#1A2634' }} (charts)", + token: '--colorTextStandard', + }, + { + dark: '#9DA4AE', + light: '#9DA4AE', + replaces: "fill='#9DA4AE' (secondary icons)", + token: '--colorIconSubtle', + }, + { + dark: '#656D7B', + light: '#656D7B', + replaces: "fill='#656D7B' (sun icon in Switch)", + token: '--colorIconDisabled', + }, + ].map(({ dark, light, replaces, token }) => ( + + + + + + + ))} + +
Token nameLight valueDark valueReplaces
+ {token} + +
+
+ {light} +
+
+
+
+ {dark} +
+
+ + {replaces} + +
+
+
+ ), +} diff --git a/frontend/stories/Icons.stories.tsx b/frontend/stories/Icons.stories.tsx new file mode 100644 index 000000000000..bb5f86989b1e --- /dev/null +++ b/frontend/stories/Icons.stories.tsx @@ -0,0 +1,605 @@ +import React from 'react' +import type { Meta, StoryObj } from '@storybook/react' +import Icon from 'components/Icon' +import type { IconName } from 'components/Icon' + +// --------------------------------------------------------------------------- +// Helpers +// --------------------------------------------------------------------------- + +function getBorderStyle(broken?: boolean, hardcoded?: boolean): string { + if (broken) return '2px solid #ef4d56' + if (hardcoded) return '2px solid #ff9f43' + return '1px solid var(--colorStrokeStandard)' +} + +// --------------------------------------------------------------------------- +// Data +// --------------------------------------------------------------------------- + +/** All icon names from the IconName union type in Icon.tsx */ +const allIconNames: IconName[] = [ + 'arrow-left', + 'arrow-right', + 'award', + 'bar-chart', + 'bell', + 'calendar', + 'checkmark', + 'checkmark-circle', + 'checkmark-square', + 'chevron-down', + 'chevron-left', + 'chevron-right', + 'chevron-up', + 'clock', + 'close-circle', + 'code', + 'copy', + 'copy-outlined', + 'dash', + 'diff', + 'edit', + 'edit-outlined', + 'email', + 'expand', + 'eye', + 'eye-off', + 'features', + 'file-text', + 'flash', + 'flask', + 'github', + 'google', + 'height', + 'info', + 'info-outlined', + 'issue-closed', + 'issue-linked', + 'layers', + 'layout', + 'link', + 'list', + 'lock', + 'minus-circle', + 'moon', + 'more-vertical', + 'nav-logo', + 'open-external-link', + 'options-2', + 'people', + 'person', + 'pie-chart', + 'plus', + 'pr-closed', + 'pr-draft', + 'pr-linked', + 'pr-merged', + 'radio', + 'refresh', + 'request', + 'required', + 'rocket', + 'search', + 'setting', + 'settings-2', + 'shield', + 'star', + 'sun', + 'timer', + 'trash-2', + 'warning', +] + +/** Icons that default to #1A2634 — invisible in dark mode */ +const darkModeBreakageIcons: IconName[] = [ + 'checkmark', + 'chevron-down', + 'chevron-left', + 'chevron-right', + 'chevron-up', + 'arrow-left', + 'arrow-right', + 'clock', + 'code', + 'copy', + 'copy-outlined', + 'dash', + 'diff', + 'edit', + 'edit-outlined', + 'email', + 'file-text', + 'flash', + 'flask', + 'height', + 'bell', + 'calendar', + 'layout', + 'layers', + 'list', + 'lock', + 'minus-circle', + 'more-vertical', + 'people', + 'person', + 'pie-chart', + 'refresh', + 'request', + 'setting', + 'settings-2', + 'shield', + 'star', + 'timer', + 'trash-2', + 'bar-chart', + 'award', + 'options-2', + 'open-external-link', + 'features', + 'rocket', + 'expand', + 'radio', +] + +/** Icons with hardcoded fills that can't be overridden */ +const hardcodedFillIcons: IconName[] = [ + 'github', + 'google', + 'link', + 'pr-merged', + 'pr-closed', + 'pr-linked', + 'pr-draft', + 'issue-closed', + 'issue-linked', +] + +/** Icons using specific semantic defaults (not #1A2634) */ +const semanticDefaultIcons: Record< + string, + { icons: IconName[]; colour: string } +> = { + 'Cyan (#0AADDF)': { colour: '#0AADDF', icons: ['info'] }, + 'Grey (#9DA4AE)': { + colour: '#9DA4AE', + icons: ['eye', 'eye-off', 'search', 'info-outlined'], + }, + 'Orange (#FF9F43)': { colour: '#FF9F43', icons: ['warning'] }, + 'Purple (#6837FC)': { colour: '#6837FC', icons: ['checkmark-square'] }, + 'Red (#EF4D56)': { colour: '#EF4D56', icons: ['close-circle'] }, + 'Sun/Moon grey (#656D7B)': { colour: '#656D7B', icons: ['sun', 'moon'] }, + 'White (#ffffff)': { colour: '#ffffff', icons: ['plus', 'nav-logo'] }, +} + +/** Separate SVG components outside of Icon.tsx */ +const separateSvgComponents = [ + { name: 'ArrowUpIcon', path: 'web/components/svg/ArrowUpIcon.tsx' }, + { name: 'AuditLogIcon', path: 'web/components/svg/AuditLogIcon.tsx' }, + { name: 'CaretDownIcon', path: 'web/components/svg/CaretDownIcon.tsx' }, + { name: 'CaretRightIcon', path: 'web/components/svg/CaretRightIcon.tsx' }, + { + name: 'DocumentationIcon', + path: 'web/components/svg/DocumentationIcon.tsx', + }, + { name: 'DropIcon', path: 'web/components/svg/DropIcon.tsx' }, + { + name: 'EnvironmentSettingsIcon', + path: 'web/components/svg/EnvironmentSettingsIcon.tsx', + }, + { name: 'FeaturesIcon', path: 'web/components/svg/FeaturesIcon.tsx' }, + { name: 'LogoutIcon', path: 'web/components/svg/LogoutIcon.tsx' }, + { name: 'NavIconSmall', path: 'web/components/svg/NavIconSmall.tsx' }, + { name: 'OrgSettingsIcon', path: 'web/components/svg/OrgSettingsIcon.tsx' }, + { name: 'PlayIcon', path: 'web/components/svg/PlayIcon.tsx' }, + { name: 'PlusIcon', path: 'web/components/svg/PlusIcon.tsx' }, + { + name: 'ProjectSettingsIcon', + path: 'web/components/svg/ProjectSettingsIcon.tsx', + }, + { name: 'SegmentsIcon', path: 'web/components/svg/SegmentsIcon.tsx' }, + { name: 'SparklesIcon', path: 'web/components/svg/SparklesIcon.tsx' }, + { name: 'UpgradeIcon', path: 'web/components/svg/UpgradeIcon.tsx' }, + { name: 'UserSettingsIcon', path: 'web/components/svg/UserSettingsIcon.tsx' }, + { name: 'UsersIcon', path: 'web/components/svg/UsersIcon.tsx' }, + { name: 'GithubIcon', path: 'web/components/base/icons/GithubIcon.tsx' }, + { name: 'GitlabIcon', path: 'web/components/base/icons/GitlabIcon.tsx' }, + { + name: 'IdentityOverridesIcon', + path: 'web/components/IdentityOverridesIcon.tsx', + }, + { + name: 'SegmentOverridesIcon', + path: 'web/components/SegmentOverridesIcon.tsx', + }, +] + +// --------------------------------------------------------------------------- +// Helpers +// --------------------------------------------------------------------------- + +const IconCell: React.FC<{ + name: IconName + broken?: boolean + hardcoded?: boolean +}> = ({ broken, hardcoded, name }) => ( +
+
+ +
+ + {name} + + {broken && ( + + DARK MODE BROKEN + + )} + {hardcoded && ( + + HARDCODED FILL + + )} +
+) + +// --------------------------------------------------------------------------- +// Stories +// --------------------------------------------------------------------------- + +const meta: Meta = { + parameters: { layout: 'padded' }, + title: 'Design System/Icons', +} +export default meta + +export const AllIcons: StoryObj = { + render: () => ( +
+

+ All Icons ({allIconNames.length}) +

+

+ Source: Icon.tsx — 71 inline SVGs in a single switch + statement (1,543 lines). Toggle dark mode in the toolbar to see which + icons disappear. +

+
+ + + Defaults to #1A2634 — invisible in dark mode + + + + Hardcoded fill — not overridable via props + +
+
+ {allIconNames.map((name) => ( + + ))} +
+
+ ), +} + +export const DarkModeBroken: StoryObj = { + render: () => ( +
+

+ Dark Mode Broken Icons ({darkModeBreakageIcons.length}) +

+

+ These icons default to fill="#1A2634" (dark + navy). On the dark background (#101628), they are + invisible. +
+
+ Fix: Replace{' '} + + fill={'{'}fill || '#1A2634'{'}'} + {' '} + with{' '} + + fill={'{'}fill || 'currentColor'{'}'} + + . +

+
+ {darkModeBreakageIcons.map((name) => ( + + ))} +
+
+ ), +} + +export const HardcodedFills: StoryObj = { + render: () => ( +
+

+ Hardcoded Fill Icons ({hardcodedFillIcons.length}) +

+

+ These icons have fill colours baked into the SVG paths. The{' '} + fill prop has no effect. Some (like Google) intentionally + use brand colours, others (like + github and pr-draft) use #1A2634{' '} + which breaks in dark mode. +

+
+ {hardcodedFillIcons.map((name) => ( + + ))} +
+
+ ), +} + +export const SemanticDefaults: StoryObj = { + render: () => ( +
+

+ Icons with Semantic Defaults +

+

+ These icons use meaningful default fill colours (success, danger, info + etc.) rather than the generic #1A2634. +

+ {Object.entries(semanticDefaultIcons).map( + ([label, { colour, icons }]) => ( +
+

+ {label}{' '} + +

+
+ {icons.map((name) => ( + + ))} +
+
+ ), + )} +
+ ), +} + +export const SeparateSvgComponents: StoryObj = { + render: () => ( +
+

+ Separate SVG Components ({separateSvgComponents.length}) +

+

+ These icons exist as individual .tsx files outside of{' '} + Icon.tsx. They are imported directly by components, not via + the <Icon name="..." /> API. +
+
+ Consolidation opportunity: Either move these into the + Icon.tsx switch statement, or extract Icon.tsx's inline SVGs into + individual files and use a consistent pattern for all icons. +

+ + + + + + + + + {separateSvgComponents.map(({ name, path }) => ( + + + + + ))} + +
+ Component + + File Path +
+ {name} + + + {path} + +
+
+ ), +} + +export const IconSystemSummary: StoryObj = { + render: () => ( +
+

+ Icon System Summary +

+ + + {[ + ['Total icon names in Icon.tsx', '71'], + [ + 'Inline SVGs in switch statement', + '70 (paste declared but not implemented)', + ], + ['Separate SVG components', '23 files across 3 directories'], + ['Integration SVG files', '37 in /static/images/integrations/'], + [ + 'Icons defaulting to #1A2634', + `${darkModeBreakageIcons.length} — invisible in dark mode`, + ], + [ + 'Icons with hardcoded fills', + `${hardcodedFillIcons.length} — cannot be overridden via props`, + ], + ['Icons using currentColor', '0'], + ['Icon.tsx total lines', '1,543'], + ['Unused dependency: ionicons', 'v7.2.1 installed but not used'], + ].map(([label, value]) => ( + + + + + ))} + +
+ {label} + + {value} +
+
+ ), +} diff --git a/frontend/stories/Typography.stories.tsx b/frontend/stories/Typography.stories.tsx new file mode 100644 index 000000000000..766920b2733c --- /dev/null +++ b/frontend/stories/Typography.stories.tsx @@ -0,0 +1,1003 @@ +import React from 'react' +import type { Meta, StoryObj } from '@storybook/react' + +// --------------------------------------------------------------------------- +// Meta +// --------------------------------------------------------------------------- + +const meta: Meta = { + parameters: { layout: 'padded' }, + title: 'Design System/Typography', +} +export default meta + +// --------------------------------------------------------------------------- +// Shared helpers +// --------------------------------------------------------------------------- + +function getA11yBackground(a11y: string): string { + if (a11y.startsWith('Accessible')) return 'rgba(39, 171, 149, 0.1)' + if (a11y.startsWith('Fails')) return 'rgba(239, 77, 86, 0.1)' + return 'rgba(255, 159, 67, 0.1)' +} + +function getA11yColour(a11y: string): string { + if (a11y.startsWith('Accessible')) return '#27ab95' + if (a11y.startsWith('Fails')) return '#ef4d56' + return '#ff9f43' +} + +const SectionHeading: React.FC<{ title: string; subtitle?: string }> = ({ + subtitle, + title, +}) => ( +
+

+ {title} +

+ {subtitle && ( +

+ {subtitle} +

+ )} +
+) + +const Divider: React.FC = () => ( +
+) + +const SpecBadge: React.FC<{ label: string }> = ({ label }) => ( + + {label} + +) + +// --------------------------------------------------------------------------- +// TypeScale story +// --------------------------------------------------------------------------- + +const headingLevels = [ + { lineHeight: 46, size: 42, tag: 'h1', weight: 700 }, + { lineHeight: 40, size: 34, tag: 'h2', weight: 700 }, + { lineHeight: 40, size: 30, tag: 'h3', weight: 700 }, + { lineHeight: 32, size: 24, tag: 'h4', weight: 700 }, + { lineHeight: 28, size: 18, tag: 'h5', weight: 700 }, + { lineHeight: 24, size: 16, tag: 'h6', weight: 700 }, +] as const + +const bodyLevels = [ + { label: 'Body', lineHeight: 20, scssVar: '$font-size-base', size: 14 }, + { label: 'Caption', lineHeight: 20, scssVar: '$font-caption', size: 13 }, + { + label: 'Caption SM', + lineHeight: 18, + scssVar: '$font-caption-sm', + size: 12, + }, + { + label: 'Caption XS', + lineHeight: 16, + scssVar: '$font-caption-xs', + size: 11, + }, +] as const + +export const TypeScale: StoryObj = { + render: () => ( +
+ + + {/* Headings */} +

+ Headings +

+ + {headingLevels.map(({ lineHeight, size, tag, weight }) => { + const Tag = tag as keyof JSX.IntrinsicElements + return ( +
+ {/* Tag label */} + + {tag} + + + {/* Spec badges */} +
+ + + +
+ + {/* Live element — picks up project CSS */} + + The quick brown fox + +
+ ) + })} + + + + {/* Body */} +

+ Body sizes +

+

+ There are no CSS classes for body sizes — they are set via SCSS + variables only. Rendered here with matching inline styles so you can see + the scale. +

+ + {bodyLevels.map(({ label, lineHeight, scssVar, size }) => ( +
+ {/* Label */} +
+
+ {label} +
+ + {scssVar} + +
+ + {/* Spec badges */} +
+ + +
+ + {/* Live sample */} + + The quick brown fox jumps over the lazy dog + +
+ ))} +
+ ), +} + +// --------------------------------------------------------------------------- +// HardcodedFontSizes story +// --------------------------------------------------------------------------- + +type HardcodedGroup = { + value: string | number + count: number + files: string[] +} + +const hardcodedGroups: HardcodedGroup[] = [ + { + count: 17, + files: [ + 'IntegrationAdoptionTable', + 'ReleasePipelineStatsTable', + 'various admin components', + ], + value: 13, + }, + { + count: 12, + files: ['Labels', 'captions', 'badges across many components'], + value: 12, + }, + { + count: 7, + files: ['TableFilter', 'TableFilterOptions', 'small labels'], + value: 11, + }, + { + count: 1, + files: ['Icon labels in stories'], + value: 10, + }, + { + count: 1, + files: ['IonIcon in CreateRole'], + value: 16, + }, + { + count: 1, + files: ['Scattered'], + value: '18px', + }, + { + count: 1, + files: ['Scattered'], + value: '14px', + }, + { + count: 3, + files: ['Scattered'], + value: '13px', + }, + { + count: 2, + files: ['Scattered'], + value: '12px', + }, + { + count: 1, + files: ['Scattered'], + value: '0.875rem', + }, +] + +const totalHardcoded = hardcodedGroups.reduce((acc, g) => acc + g.count, 0) + +const CountBadge: React.FC<{ count: number }> = ({ count }) => ( + + {count} + +) + +export const HardcodedFontSizes: StoryObj = { + render: () => ( +
+ + + {/* Summary callout */} +
+ +
+ + {totalHardcoded} inline fontSize values bypass the type scale. + + + These should use typography tokens instead. Each hardcoded value + creates drift between components and makes future type-scale changes + labour-intensive. + +
+
+ + {/* Column headers */} +
+ {['Value', 'Count', 'Sample', 'Files'].map((h) => ( + + {h} + + ))} +
+ + {hardcodedGroups.map((group) => { + const numericSize = + typeof group.value === 'number' + ? group.value + : parseFloat(group.value as string) * + (String(group.value).endsWith('rem') ? 16 : 1) + + return ( +
+ {/* Value */} + + {`fontSize: ${ + typeof group.value === 'string' + ? `'${group.value}'` + : group.value + }`} + + + {/* Count badge */} +
+ +
+ + {/* Sample — rendered at the actual size with audit border */} +
+ + Sample text at {group.value} + +
+ + {/* Files */} +
+ {group.files.map((f) => ( +
+ {f} +
+ ))} +
+
+ ) + })} + + {/* Legend */} +
+
+ + Red border = sample rendered at the hardcoded size + +
+
+ ), +} + +// --------------------------------------------------------------------------- +// WeightAndStyleUsage story +// --------------------------------------------------------------------------- + +const weightTiers = [ + { + classes: 'fw-bold, .bold-link', + label: 'Bold', + occurrences: '~14 in SCSS', + scssVar: '$btn-font-weight', + usage: 'Headings (h1–h6), buttons, badges', + value: 700, + }, + { + classes: 'fw-semibold', + label: 'Semibold', + occurrences: '~3 in SCSS, 3 inline in TSX', + scssVar: '$tab-btn-active-font-weight', + usage: 'Active tab states, admin dashboard tables', + value: 600, + }, + { + classes: '.font-weight-medium, fw-normal (sometimes)', + label: 'Medium', + occurrences: '~33 in SCSS, 1 inline in TSX', + scssVar: '$input-font-weight', + usage: 'Body text, inputs, alerts, tabs, links — most common weight', + value: 500, + }, + { + classes: 'fw-normal', + label: 'Regular', + occurrences: '~5 in SCSS, 1 inline in TSX', + scssVar: '(none)', + usage: 'Secondary/muted text, placeholders, dividers', + value: 400, + }, +] + +const mutedTextPatterns = [ + { + a11y: 'Accessible', + method: 'Colour-based', + pattern: '$text-muted / .text-muted', + value: '#656D7B', + }, + { + a11y: 'Accessible', + method: 'Colour + size', + pattern: '.faint', + value: '$text-muted + 0.8em', + }, + { + a11y: 'Risky — depends on bg', + method: 'Opacity', + pattern: 'opacity: 0.6', + value: '60% transparency', + }, + { + a11y: 'Risky — low contrast', + method: 'Opacity', + pattern: 'opacity: 0.5', + value: '50% transparency', + }, + { + a11y: 'Fails WCAG AA', + method: 'Opacity', + pattern: 'opacity: 0.4', + value: '40% transparency', + }, + { + a11y: 'Intentional (disabled)', + method: 'Opacity', + pattern: '$btn-disabled-opacity (0.32)', + value: '32% transparency', + }, +] + +const bugs = [ + { + code: "fontWeight: 'semi-bold'", + file: 'web/components/messages/SuccessMessage.tsx', + fix: 'fontWeight: 600', + issue: + 'Invalid CSS — "semi-bold" is not a valid fontWeight value. Browser ignores it.', + }, + { + code: "fontWeight: 'semi-bold'", + file: 'web/components/SuccessMessage.js', + fix: 'fontWeight: 600', + issue: 'Same bug, duplicate file (JS version of the above).', + }, +] + +export const WeightAndStyleUsage: StoryObj = { + render: () => ( +
+ + + {/* Weight tiers */} +

+ Weight Tiers in Use +

+ + + + {['Weight', 'Sample', 'SCSS variable', 'CSS classes', 'Usage'].map( + (h) => ( + + ), + )} + + + + {weightTiers.map((tier) => ( + + + + + + + + ))} + +
+ {h} +
+ + + {tier.label} + + + + The quick brown fox + + + + {tier.scssVar} + + + + {tier.classes} + + + {tier.usage} +
+ + {/* Inconsistency callout */} +
+ + Inconsistency: Two class systems in use + +

+ Custom class .font-weight-medium (500) competes with + Bootstrap utilities fw-bold (18 uses),{' '} + fw-semibold (16 uses), fw-normal (32 uses). + Components pick whichever they find first. Should standardise on one + system. +

+
+ + + + {/* Muted text patterns */} +

+ "Subtle" / Muted Text Patterns +

+

+ Text hierarchy is achieved through both colour and opacity. + Opacity-based patterns are an accessibility concern — the resulting + contrast depends on the background, making it unpredictable across + light/dark modes. +

+ + + + {['Pattern', 'Method', 'Value', 'Accessibility'].map((h) => ( + + ))} + + + + {mutedTextPatterns.map((row) => ( + + + + + + + ))} + +
+ {h} +
+ + {row.pattern} + + + {row.method} + + {row.value} + + + {row.a11y} + +
+ + + + {/* Bugs */} +

+ Bugs Found +

+ {bugs.map((bug) => ( +
+ + {bug.file} + +
+ {bug.code} +
+

+ {bug.issue} +

+

+ Fix: {bug.fix} +

+
+ ))} + + + + {/* Other styles */} +

+ Other Styles +

+ + + {[ + [ + 'Italic', + '2 occurrences (CodeReferenceItem, RuleConditionPropertySelect)', + 'Not a pattern — ad-hoc usage only', + ], + [ + 'Underline', + '4 declarations in SCSS (.link-style, a:hover, .btn-link:hover)', + 'Consistent — used for interactive text', + ], + [ + 'text-transform: uppercase', + '1 occurrence (.btn-project-letter)', + 'Rare — not systemic', + ], + [ + 'text-transform: capitalize', + '1 occurrence (.panel heading)', + 'Rare — not systemic', + ], + ['letter-spacing', '1 class (.letter-spacing: 1px)', 'Barely used'], + ].map(([style, usage, note]) => ( + + + + + + ))} + +
+ {style} + + {usage} + + {note} +
+
+ ), +} From beebc060307223e04aa8a28e524ec62f567173c7 Mon Sep 17 00:00:00 2001 From: Talisson Costa Date: Thu, 5 Mar 2026 23:25:24 -0300 Subject: [PATCH 06/11] docs(design-system): add 26 actionable issue templates from audit Categorised as quick wins (QW-1 to QW-8), medium efforts (ME-1 to ME-10), and long refactors (LR-1 to LR-8). Each template includes problem description, affected files, and acceptance criteria. Part of #6606 Co-Authored-By: Claude Opus 4.6 --- .../LR-1-break-up-icon-tsx.md | 37 +++++++++++ .../LR-2-semantic-colour-tokens.md | 58 +++++++++++++++++ .../LR-3-modal-system-migration.md | 47 ++++++++++++++ .../LR-4-standardise-tables-lists.md | 48 ++++++++++++++ .../LR-5-remove-legacy-js-components.md | 41 ++++++++++++ .../LR-6-formal-colour-palette.md | 50 +++++++++++++++ .../LR-7-full-dark-mode-audit.md | 51 +++++++++++++++ .../LR-8-standardise-typography.md | 45 +++++++++++++ .../ME-1-consolidate-confirm-remove-modals.md | 52 +++++++++++++++ .../ME-10-typography-inconsistencies.md | 53 ++++++++++++++++ .../ME-2-button-dark-mode-gaps.md | 56 +++++++++++++++++ .../ME-3-convert-input-to-typescript.md | 41 ++++++++++++ .../ME-4-toast-dark-mode.md | 34 ++++++++++ .../ME-5-unify-dropdowns.md | 47 ++++++++++++++ .../ME-6-checkbox-switch-dark-mode.md | 41 ++++++++++++ .../ME-7-consolidate-svg-icon-components.md | 53 ++++++++++++++++ .../ME-8-storybook-a11y-addon.md | 54 ++++++++++++++++ .../ME-9-expand-a11y-coverage.md | 63 +++++++++++++++++++ .../QW-1-icon-currentcolor.md | 46 ++++++++++++++ .../QW-2-hardcoded-1a2634-in-components.md | 55 ++++++++++++++++ .../QW-3-chart-axis-dark-mode.md | 50 +++++++++++++++ .../QW-4-release-pipeline-colours.md | 48 ++++++++++++++ .../QW-5-remove-ionicons.md | 45 +++++++++++++ .../QW-6-hardcoded-icon-fills.md | 58 +++++++++++++++++ .../QW-7-a11y-tests-ci.md | 48 ++++++++++++++ .../QW-8-semi-bold-bug.md | 44 +++++++++++++ 26 files changed, 1265 insertions(+) create mode 100644 frontend/design-system-issues/LR-1-break-up-icon-tsx.md create mode 100644 frontend/design-system-issues/LR-2-semantic-colour-tokens.md create mode 100644 frontend/design-system-issues/LR-3-modal-system-migration.md create mode 100644 frontend/design-system-issues/LR-4-standardise-tables-lists.md create mode 100644 frontend/design-system-issues/LR-5-remove-legacy-js-components.md create mode 100644 frontend/design-system-issues/LR-6-formal-colour-palette.md create mode 100644 frontend/design-system-issues/LR-7-full-dark-mode-audit.md create mode 100644 frontend/design-system-issues/LR-8-standardise-typography.md create mode 100644 frontend/design-system-issues/ME-1-consolidate-confirm-remove-modals.md create mode 100644 frontend/design-system-issues/ME-10-typography-inconsistencies.md create mode 100644 frontend/design-system-issues/ME-2-button-dark-mode-gaps.md create mode 100644 frontend/design-system-issues/ME-3-convert-input-to-typescript.md create mode 100644 frontend/design-system-issues/ME-4-toast-dark-mode.md create mode 100644 frontend/design-system-issues/ME-5-unify-dropdowns.md create mode 100644 frontend/design-system-issues/ME-6-checkbox-switch-dark-mode.md create mode 100644 frontend/design-system-issues/ME-7-consolidate-svg-icon-components.md create mode 100644 frontend/design-system-issues/ME-8-storybook-a11y-addon.md create mode 100644 frontend/design-system-issues/ME-9-expand-a11y-coverage.md create mode 100644 frontend/design-system-issues/QW-1-icon-currentcolor.md create mode 100644 frontend/design-system-issues/QW-2-hardcoded-1a2634-in-components.md create mode 100644 frontend/design-system-issues/QW-3-chart-axis-dark-mode.md create mode 100644 frontend/design-system-issues/QW-4-release-pipeline-colours.md create mode 100644 frontend/design-system-issues/QW-5-remove-ionicons.md create mode 100644 frontend/design-system-issues/QW-6-hardcoded-icon-fills.md create mode 100644 frontend/design-system-issues/QW-7-a11y-tests-ci.md create mode 100644 frontend/design-system-issues/QW-8-semi-bold-bug.md diff --git a/frontend/design-system-issues/LR-1-break-up-icon-tsx.md b/frontend/design-system-issues/LR-1-break-up-icon-tsx.md new file mode 100644 index 000000000000..29a8a2f7881e --- /dev/null +++ b/frontend/design-system-issues/LR-1-break-up-icon-tsx.md @@ -0,0 +1,37 @@ +--- +title: "Icon.tsx — break up monolithic component" +labels: ["design-system", "large-refactor", "icons"] +--- + +## Problem + +`Icon.tsx` is 1,543 lines containing 70+ inline SVG definitions in a single switch statement. It is the largest component in the codebase. One icon (`paste`) is declared in the `IconName` type but has no implementation — it falls through the switch silently. + +## Files + +- `web/components/Icon.tsx` — monolithic switch statement with all SVG inline, 1,543 lines + +## Proposed Fix + +Extract each icon into its own file under `web/components/icons/`. Create an `IconMap` that lazy-loads icons by name. Keep the `` API unchanged so no call sites need to change. + +The `paste` icon must either be implemented or removed from the `IconName` type. + +## Acceptance Criteria + +- [ ] Each icon lives in its own file under `web/components/icons/` +- [ ] The `` public API is unchanged +- [ ] `paste` is either implemented or removed from `IconName` +- [ ] Bundle size is not increased (verify with bundle analyser) +- [ ] All existing icon usages render correctly + +## Storybook Validation + +Browse the existing icon story and confirm all icons render after the refactor. + +## Dependencies + +QW-1 (fix `currentColor` defaults first — icons must default to `currentColor` before extraction to avoid baking in the wrong fill) + +--- +Part of the Design System Audit (#6606) diff --git a/frontend/design-system-issues/LR-2-semantic-colour-tokens.md b/frontend/design-system-issues/LR-2-semantic-colour-tokens.md new file mode 100644 index 000000000000..c32b515d3763 --- /dev/null +++ b/frontend/design-system-issues/LR-2-semantic-colour-tokens.md @@ -0,0 +1,58 @@ +--- +title: "Introduce semantic colour tokens (CSS custom properties)" +labels: ["design-system", "large-refactor", "dark-mode", "colours"] +--- + +## Problem + +Dark mode is implemented via three parallel mechanisms that do not compose: + +1. **SCSS `.dark` selectors** (48 rules across 29 files) — compile-time only, cannot be used in inline styles +2. **`getDarkMode()` runtime calls** (13 components) — manual ternaries that are easy to forget or get wrong +3. **Bootstrap `data-bs-theme`** — set on the root element but underused; conflicts with `.dark` selectors in places + +The `$component-property-dark` SCSS variable suffix convention requires every value to be duplicated. There is no single source of truth for what a colour means in each theme. + +## Files + +- `web/styles/_variables.scss` — 48 `.dark` override rules +- `common/utils/colour.ts` (or equivalent) — `getDarkMode()` used in 13 components +- All 13 components calling `getDarkMode()` — manual ternary colour logic + +## Proposed Fix + +Introduce CSS custom properties as the single source of truth: + +- `common/theme/tokens.ts` — token definitions importable in TypeScript (`import { colorTextStandard } from 'common/theme'`) +- `web/styles/_tokens.scss` — token declarations with `:root` (light values) and `[data-bs-theme='dark']` (dark overrides) + +Both files are already drafted on the `chore/design-system-audit-6606` branch. + +### Migration path + +1. Token files already exist (drafted on audit branch) — merge and stabilise +2. Fix `Icon.tsx` with `currentColor` (QW-1) +3. Migrate all 13 `getDarkMode()` callsites to CSS custom properties +4. Migrate `.dark` SCSS selectors component-by-component +5. Remove orphaned `$*-dark` SCSS variables once all callsites are migrated + +## Acceptance Criteria + +- [ ] `common/theme/tokens.ts` is merged and exported +- [ ] `web/styles/_tokens.scss` defines all semantic tokens under `:root` and `[data-bs-theme='dark']` +- [ ] All 13 `getDarkMode()` callsites are removed +- [ ] All 48 `.dark` SCSS override rules are replaced with token usage +- [ ] Orphaned `$*-dark` variables are deleted +- [ ] Light and dark mode render correctly across all migrated components + +## Storybook Validation + +- `Design System/Colours/Semantic Tokens` — verify token values in both themes +- `Design System/Dark Mode Issues/Theme Token Comparison` — side-by-side light/dark comparison + +## Dependencies + +LR-6 (formal colour palette must be defined before semantic tokens can reference it meaningfully) + +--- +Part of the Design System Audit (#6606) diff --git a/frontend/design-system-issues/LR-3-modal-system-migration.md b/frontend/design-system-issues/LR-3-modal-system-migration.md new file mode 100644 index 000000000000..18e364af2d9c --- /dev/null +++ b/frontend/design-system-issues/LR-3-modal-system-migration.md @@ -0,0 +1,47 @@ +--- +title: "Modal system — migrate from global imperative API" +labels: ["design-system", "large-refactor"] +--- + +## Problem + +The modal system exposes `openModal()`, `openModal2()`, and `openConfirm()` as global functions attached to `window`. This approach has three critical problems: + +1. **Deprecated React APIs** — the implementation uses `ReactDOM.render` and `ReactDOM.unmountComponentAtNode`, both of which were removed in React 18. The app is currently blocked from upgrading React without addressing this. +2. **`openModal2` exists for stacking modals** — its existence is acknowledged in the code as a pattern to avoid, yet it remains in use. +3. **No React context** — modals rendered outside the React tree cannot access context (theme, auth, feature flags, etc.) without prop-drilling workarounds. + +## Files + +- `web/components/modals/` — modal implementations calling the global API +- `common/code/modalService.ts` (or equivalent) — `openModal`, `openModal2`, `openConfirm` definitions +- All callsites of `openModal()`, `openModal2()`, `openConfirm()` across the codebase + +## Proposed Fix + +Migrate to a React context-based modal manager: + +- Introduce a `ModalProvider` at the app root +- Expose a `useModal()` hook that replaces all global function calls +- Support modal stacking natively within the context (eliminating the need for `openModal2`) +- Modals rendered inside the React tree gain access to all context providers + +## Acceptance Criteria + +- [ ] `openModal`, `openModal2`, and `openConfirm` are removed from `window` +- [ ] All modal invocations use the `useModal()` hook +- [ ] Modal stacking works without `openModal2` +- [ ] No usage of `ReactDOM.render` or `ReactDOM.unmountComponentAtNode` remains +- [ ] The app is compatible with React 18 after this change +- [ ] Existing modal behaviour (open, close, confirm, nested) is preserved + +## Storybook Validation + +Not directly applicable — validate via manual testing and E2E tests covering modal open/close/confirm flows. + +## Dependencies + +None — this is a blocker for React 18 compatibility. + +--- +Part of the Design System Audit (#6606) diff --git a/frontend/design-system-issues/LR-4-standardise-tables-lists.md b/frontend/design-system-issues/LR-4-standardise-tables-lists.md new file mode 100644 index 000000000000..7ca707b0a843 --- /dev/null +++ b/frontend/design-system-issues/LR-4-standardise-tables-lists.md @@ -0,0 +1,48 @@ +--- +title: "Standardise table/list components" +labels: ["design-system", "large-refactor"] +--- + +## Problem + +There is no unified table or list component system. Each feature area builds its own ad hoc implementation, resulting in visual inconsistency and duplicated logic: + +- 9 different `TableFilter*` components +- 5+ different `*Row` components (`FeatureRow`, `ProjectFeatureRow`, `FeatureOverrideRow`, `OrganisationUsersTableRow`, etc.) +- 5+ different `*List` components + +There is no shared abstraction for sorting, filtering, pagination, empty states, or loading skeletons. Changes to table behaviour (e.g. keyboard navigation, row hover, selection) must be made in every implementation separately. + +## Files + +- `web/components/` — `TableFilter*`, `*Row`, and `*List` components scattered across feature directories + +## Proposed Fix + +Create a composable `Table` / `List` component system with standardised sub-components: + +- `Table`, `Table.Header`, `Table.Row`, `Table.Cell` +- `List`, `List.Item` +- Shared empty state and loading skeleton slots + +Migrate feature areas one at a time. Do not attempt a single large migration — migrate the simplest feature area first to validate the API, then proceed. + +## Acceptance Criteria + +- [ ] Shared `Table` and `List` components exist with documented sub-component API +- [ ] At least one feature area is fully migrated as a reference implementation +- [ ] Remaining feature areas are migrated incrementally (tracked in sub-tasks) +- [ ] Visual output is identical before and after migration for each area +- [ ] Empty state and loading state are handled consistently + +## Storybook Validation + +- `Design System/Table` — verify all Table variants and states +- `Design System/List` — verify all List variants and states + +## Dependencies + +None. + +--- +Part of the Design System Audit (#6606) diff --git a/frontend/design-system-issues/LR-5-remove-legacy-js-components.md b/frontend/design-system-issues/LR-5-remove-legacy-js-components.md new file mode 100644 index 000000000000..eafe842a83d4 --- /dev/null +++ b/frontend/design-system-issues/LR-5-remove-legacy-js-components.md @@ -0,0 +1,41 @@ +--- +title: "Remove legacy JS class components" +labels: ["design-system", "large-refactor"] +--- + +## Problem + +Several components remain as `.js` class components. These cannot be type-checked, do not support modern React patterns (hooks, context), and block TypeScript strictness improvements across the codebase. One file (`CreateWebhook.js`) coexists with a `.tsx` version of the same component, creating an ambiguous import situation. + +## Files + +- `web/components/base/forms/Input.js` — legacy class component +- `web/components/base/forms/InputGroup.js` — legacy class component +- `web/components/modals/CreateProject.js` — legacy class component +- `web/components/modals/CreateWebhook.js` — legacy duplicate; `.tsx` version exists +- `web/components/modals/Payment.js` — legacy class component +- `web/components/Flex.js` — layout primitive, legacy class component +- `web/components/Column.js` — layout primitive, legacy class component + +## Proposed Fix + +Convert each file to a TypeScript functional component (`.tsx`). Delete `CreateWebhook.js` — the `.tsx` version is the canonical implementation. Verify all import paths resolve to the `.tsx` file after deletion. + +## Acceptance Criteria + +- [ ] All listed `.js` files are converted to `.tsx` functional components +- [ ] `CreateWebhook.js` is deleted; all imports resolve to the `.tsx` version +- [ ] `npm run typecheck` passes with no new errors +- [ ] `npm run lint` passes +- [ ] No runtime regressions in the converted components + +## Storybook Validation + +Verify that `Input`, `InputGroup`, `Flex`, and `Column` stories (if they exist) still render correctly after conversion. + +## Dependencies + +None — each file can be converted independently. + +--- +Part of the Design System Audit (#6606) diff --git a/frontend/design-system-issues/LR-6-formal-colour-palette.md b/frontend/design-system-issues/LR-6-formal-colour-palette.md new file mode 100644 index 000000000000..f8455501001f --- /dev/null +++ b/frontend/design-system-issues/LR-6-formal-colour-palette.md @@ -0,0 +1,50 @@ +--- +title: "Define a formal colour palette" +labels: ["design-system", "large-refactor", "colours"] +--- + +## Problem + +There is no formal, systematic colour palette. Five distinct problems compound each other: + +1. **Inverted tonal scale** — `$primary400` (`#956CFF`) is lighter than `$primary` (`#6837FC`), reversing the conventional lower-number-is-lighter scale and making scale usage unpredictable. +2. **Alpha colour RGB mismatches** — `$primary-alfa-*` uses RGB `(149, 108, 255)` but `$primary` is `(104, 55, 252)`. The same mismatch exists for `$danger` and `$warning` alpha variants. Alpha colours do not derive from their solid counterparts. +3. **30+ orphan hex values** — hardcoded hex values in components that are not in `_variables.scss`. The most prevalent are `#9DA4AE` (52 usages) and `#656D7B` (44 usages). +4. **Missing scale steps** — no `$danger600`, `$success700`, `$info400`, `$warning200` and other mid-range steps, forcing components to reach for the nearest available step or hardcode a value. +5. **No grey scale** — greys are named ad hoc (`$text-icon-grey`, `$bg-light200`, `$footer-grey`) with no systematic neutral scale. + +## Files + +- `web/styles/_variables.scss` — current palette definitions with the above inconsistencies +- All components with hardcoded hex values not referenced in `_variables.scss` + +## Proposed Fix + +Define a formal primitive palette: + +- Numbered tonal scales (50–900) per hue, where a lower number always means a lighter shade +- Alpha variants derived from the same RGB as their solid counterpart +- A systematic grey/neutral scale (e.g. `$grey50` through `$grey900`) replacing ad hoc names +- Every orphan hex value mapped to a palette token or removed + +If Tailwind CSS is adopted (PR #6105 is a POC — not yet committed), the palette can live in `theme.extend.colors`. The palette definition itself is Tailwind-agnostic; this work should not be blocked on or tied to the Tailwind decision. + +## Acceptance Criteria + +- [ ] All hue scales follow the lower-number-is-lighter convention (50–900) +- [ ] All alpha variants are derived from the correct RGB of their solid colour +- [ ] A systematic neutral/grey scale is defined +- [ ] All 30+ orphan hex values are replaced with palette tokens (or documented as intentional one-offs) +- [ ] Missing scale steps are added where components need them +- [ ] `npm run lint` passes (no SCSS errors) + +## Storybook Validation + +- `Design System/Colours/Palette Audit` — four stories covering: tonal scales, alpha variants, neutral scale, and orphan resolution + +## Dependencies + +This is a prerequisite for LR-2 (semantic colour tokens cannot reference a palette that does not exist yet). + +--- +Part of the Design System Audit (#6606) diff --git a/frontend/design-system-issues/LR-7-full-dark-mode-audit.md b/frontend/design-system-issues/LR-7-full-dark-mode-audit.md new file mode 100644 index 000000000000..3ac70c900e17 --- /dev/null +++ b/frontend/design-system-issues/LR-7-full-dark-mode-audit.md @@ -0,0 +1,51 @@ +--- +title: "Full dark mode theme audit" +labels: ["design-system", "large-refactor", "dark-mode"] +--- + +## Problem + +Only 48 `.dark` CSS selectors exist across the entire stylesheet. Many feature areas have zero dark mode coverage. Known problem areas include: + +- **Feature pipeline visualisation** — white circles and grey lines on dark background (invisible) +- **Admin dashboard charts** — light-mode-only colours hardcoded in chart config +- **Integration cards** — no `.dark` overrides, renders with light background +- **Numerous inline styles** — `color`, `background`, and `border` values hardcoded with light-mode hex values, unreachable by any CSS selector + +This is the umbrella issue. All quick-win (QW) and medium-effort (ME) dark mode items from the audit feed into it. + +## Files + +- `web/styles/` — 48 `.dark` rules across 29 files (full list in audit report) +- Any component with inline `style={{ color: '#...' }}` or `style={{ background: '#...' }}` using light-mode-only values + +## Proposed Fix + +Systematic page-by-page dark mode audit: + +1. For each page/feature area: toggle dark mode, take a screenshot, identify contrast failures +2. For each failure: replace hardcoded values with `currentColor`, CSS custom properties, or `.dark` overrides as appropriate +3. Prefer CSS custom properties (LR-2) over new `.dark` selectors — new `.dark` selectors should only be added as a stopgap +4. Document coverage gaps as they are discovered + +This issue tracks the umbrella effort. Sub-tasks per feature area should be filed separately and linked here. + +## Acceptance Criteria + +- [ ] Every top-level page has been audited in dark mode +- [ ] All critical (P0) contrast failures are resolved +- [ ] Feature pipeline visualisation renders correctly in dark mode +- [ ] Admin dashboard charts render correctly in dark mode +- [ ] Integration cards render correctly in dark mode +- [ ] No inline styles with light-mode-only hardcoded colours remain in audited areas + +## Storybook Validation + +- `Design System/Dark Mode Issues/Dark Mode Implementation Patterns` — patterns reference for contributors + +## Dependencies + +All QW and ME dark mode items from the audit contribute to this issue. LR-2 (semantic tokens) will significantly reduce the per-component effort once adopted. + +--- +Part of the Design System Audit (#6606) diff --git a/frontend/design-system-issues/LR-8-standardise-typography.md b/frontend/design-system-issues/LR-8-standardise-typography.md new file mode 100644 index 000000000000..649df3e6c179 --- /dev/null +++ b/frontend/design-system-issues/LR-8-standardise-typography.md @@ -0,0 +1,45 @@ +--- +title: "Standardise typography usage across the codebase" +labels: ["design-system", "large-refactor", "typography"] +--- + +## Problem + +A type scale (h1–h6, body sizes, weight tiers) exists in SCSS but is bypassed in 58+ places. Four related problems: + +1. **Inline `fontSize` values** (58+ occurrences) — components set font sizes via `style={{ fontSize: '...' }}` instead of using the existing SCSS variables or utility classes. +2. **Fragmented weight system** — font weight is applied via two competing conventions: custom classes (`.font-weight-medium`) and Bootstrap utilities (`fw-bold`, `fw-semibold`). Both exist in the codebase simultaneously. +3. **Opacity-based muted text** — "subtle" or secondary text uses `opacity: 0.5` or similar rather than a semantic muted colour, which breaks in dark mode and fails contrast checks. +4. **No `Text` component** — there is nothing preventing future bypass of the type scale. A `Text` component may be warranted, but only if the migration itself reveals the need — not as a speculative abstraction. + +## Files + +- 58+ component files with inline `style={{ fontSize: '...' }}` — full list available via audit +- Components mixing `.font-weight-medium` and `fw-*` Bootstrap utilities +- Components using `opacity` for muted/secondary text + +## Proposed Fix + +1. Replace all 58 inline `fontSize` values with existing SCSS variables or utility classes +2. Pick one weight class system (Bootstrap `fw-*` is preferred — it is already present and tree-shakeable) and migrate all `.font-weight-*` usages +3. Replace opacity-based muted text with `$text-muted` / `colorTextSecondary` tokens +4. Introduce a `Text` component only if the migration demonstrates a repeated need — do not create it speculatively + +## Acceptance Criteria + +- [ ] No inline `fontSize` values remain in the migrated files +- [ ] A single font weight class system is in use (Bootstrap `fw-*`) +- [ ] All `.font-weight-*` custom classes are removed +- [ ] No opacity-based muted text remains — replaced with semantic colour tokens +- [ ] `npm run typecheck` and `npm run lint` pass + +## Storybook Validation + +- `Design System/Typography` — verify type scale renders correctly across all size and weight variants + +## Dependencies + +ME-10 (if that item covers the `$text-muted` token — confirm before starting step 3 to avoid conflicts) + +--- +Part of the Design System Audit (#6606) diff --git a/frontend/design-system-issues/ME-1-consolidate-confirm-remove-modals.md b/frontend/design-system-issues/ME-1-consolidate-confirm-remove-modals.md new file mode 100644 index 000000000000..a9ff3db26e0d --- /dev/null +++ b/frontend/design-system-issues/ME-1-consolidate-confirm-remove-modals.md @@ -0,0 +1,52 @@ +--- +title: "Consolidate 6 identical ConfirmRemove* modals" +labels: ["design-system", "medium-effort"] +--- + +## Problem + +6 deletion confirmation modals follow the exact same "type the name to confirm" pattern but are implemented as separate files with duplicated logic. This results in approximately 500 lines of redundant code and makes future changes to the pattern require 6 separate edits. + +## Files + +- `web/components/modals/ConfirmRemoveFeature.tsx` — duplicate confirmation modal +- `web/components/modals/ConfirmRemoveSegment.tsx` — duplicate confirmation modal +- `web/components/modals/ConfirmRemoveProject.tsx` — duplicate confirmation modal +- `web/components/modals/ConfirmRemoveOrganisation.tsx` — duplicate confirmation modal +- `web/components/modals/ConfirmRemoveEnvironment.tsx` — duplicate confirmation modal +- `web/components/modals/ConfirmRemoveWebhook.tsx` — duplicate confirmation modal + +## Proposed Fix + +Create a single `ConfirmRemoveModal` component with `entityType`, `entityName`, and `onConfirm` props. Replace all 6 existing files with usages of the new unified component. + +Example API: + +```tsx + +``` + +Delete the 6 original files once all call sites are updated. + +## Acceptance Criteria + +- [ ] Single reusable `ConfirmRemoveModal` component exists +- [ ] All 6 use cases (feature, segment, project, organisation, environment, webhook) work identically to before +- [ ] No visual regression across any of the 6 deletion flows +- [ ] Approximately 500 lines of duplicated code removed +- [ ] No existing tests broken + +## Storybook Validation + +Not applicable — this is a refactor of modal logic with no change to visual output. Manual testing of each deletion flow is sufficient. + +## Dependencies + +None. + +--- +Part of the Design System Audit (#6606) diff --git a/frontend/design-system-issues/ME-10-typography-inconsistencies.md b/frontend/design-system-issues/ME-10-typography-inconsistencies.md new file mode 100644 index 000000000000..83637392e9b6 --- /dev/null +++ b/frontend/design-system-issues/ME-10-typography-inconsistencies.md @@ -0,0 +1,53 @@ +--- +title: "Typography inconsistencies — enforce the existing type scale" +labels: ["design-system", "medium-effort", "typography"] +--- + +## Problem + +A type scale and weight system are defined in SCSS but are widely bypassed across the codebase. This results in visually inconsistent text sizing, weight, and colour, and introduces accessibility concerns from opacity-based "muted" text. + +Specific issues found in the audit: + +- **58 hardcoded inline `fontSize` values** in TSX files (13px: 17 instances, 12px: 12 instances, 11px: 7 instances, and more) — none of these reference the existing SCSS variables +- **Inconsistent font weight application** — 4 weight tiers (700, 600, 500, 400) applied ad-hoc, with `.font-weight-medium` competing against Bootstrap's `fw-bold`, `fw-semibold`, and `fw-normal` +- **9 inline `fontWeight` values** in TSX bypassing the class system entirely +- **Opacity-based muted text** — subtle/secondary text uses `opacity: 0.4–0.75` instead of a colour token, which fails WCAG contrast requirements when the background is not guaranteed to be white or dark +- **Minimal utility classes** — only `.text-small`, `.large-para`, `.font-weight-medium`, and `.bold-link` exist; most usage falls back to raw values +- **Semi-bold bug in `SuccessMessage`** — incorrect weight applied + +## Files + +- `web/` (TSX files broadly) — 58 hardcoded `fontSize` and 9 hardcoded `fontWeight` inline styles +- `web/styles/project/_typography.scss` — type scale variables exist but are underused +- `web/styles/project/_variables.scss` — `$text-muted` and `colorTextSecondary` tokens defined but bypassed +- `web/components/SuccessMessage.tsx` — semi-bold weight bug + +## Proposed Fix + +1. **Replace 58 hardcoded `fontSize` values** with existing SCSS variables or CSS utility classes. Define missing utility classes if a size has no corresponding class. +2. **Standardise on one weight class system** — choose between the existing `.font-weight-medium` pattern and Bootstrap's `fw-*` utilities; remove the other (or alias them). Apply consistently. +3. **Replace opacity-based muted text** with the colour token approach (`$text-muted` / `colorTextSecondary`) across all usages. +4. **Fix the semi-bold bug** in `SuccessMessage.tsx`. +5. Optionally expand utility classes to cover the most common size/weight combinations observed in the audit. + +## Acceptance Criteria + +- [ ] No hardcoded `fontSize` inline styles remain in TSX (use SCSS variables or utility classes) +- [ ] No hardcoded `fontWeight` inline styles remain in TSX (use utility classes) +- [ ] One font weight class system in use consistently throughout the codebase +- [ ] Opacity-based muted text replaced with colour token (`$text-muted` / `colorTextSecondary`) +- [ ] `SuccessMessage` semi-bold bug fixed +- [ ] All changes pass `npm run lint` and `npm run typecheck` +- [ ] No visual regression on key pages + +## Storybook Validation + +Design System / Typography — verify the type scale renders correctly at all sizes and weights in both light and dark mode. Muted/secondary text should meet WCAG AA contrast in both modes. + +## Dependencies + +None. + +--- +Part of the Design System Audit (#6606) diff --git a/frontend/design-system-issues/ME-2-button-dark-mode-gaps.md b/frontend/design-system-issues/ME-2-button-dark-mode-gaps.md new file mode 100644 index 000000000000..9fd2e4aae18e --- /dev/null +++ b/frontend/design-system-issues/ME-2-button-dark-mode-gaps.md @@ -0,0 +1,56 @@ +--- +title: "Button variant dark mode gaps" +labels: ["design-system", "medium-effort", "dark-mode"] +--- + +## Problem + +Three button variants — `btn-tertiary`, `btn-danger`, and `btn--transparent` — have no `.dark` mode overrides in the stylesheet, despite the corresponding dark mode variables already being defined. This means these buttons render with light mode colours in dark mode, causing potential contrast and readability issues. + +## Files + +- `web/styles/project/_buttons.scss` — missing `.dark` overrides for `btn-tertiary`, `btn-danger`, and `btn--transparent` +- `web/styles/project/_variables.scss` — dark mode variables already defined at lines 137–144 (`$btn-tertiary-bg-dark`, etc.) but unused + +## Proposed Fix + +Add `.dark` selectors in `_buttons.scss` that reference the existing dark mode variables from `_variables.scss`. No new variables need to be created — the work is purely wiring up what already exists. + +Example pattern to follow: + +```scss +.dark { + .btn-tertiary { + background-color: $btn-tertiary-bg-dark; + color: $btn-tertiary-color-dark; + border-color: $btn-tertiary-border-dark; + } + + .btn-danger { + // add dark overrides + } + + .btn--transparent { + // add dark overrides + } +} +``` + +## Acceptance Criteria + +- [ ] `btn-tertiary` renders correctly in both light and dark mode +- [ ] `btn-danger` renders correctly in both light and dark mode +- [ ] `btn--transparent` renders correctly in both light and dark mode +- [ ] No new SCSS variables introduced — only existing dark mode variables used +- [ ] No other button variants regressed + +## Storybook Validation + +Design System / Buttons / Dark Mode Gaps — toggle dark mode in Storybook and confirm all three variants display with correct colours and contrast. + +## Dependencies + +None. + +--- +Part of the Design System Audit (#6606) diff --git a/frontend/design-system-issues/ME-3-convert-input-to-typescript.md b/frontend/design-system-issues/ME-3-convert-input-to-typescript.md new file mode 100644 index 000000000000..1d3b5a21819a --- /dev/null +++ b/frontend/design-system-issues/ME-3-convert-input-to-typescript.md @@ -0,0 +1,41 @@ +--- +title: "Convert Input.js to TypeScript" +labels: ["design-system", "medium-effort"] +--- + +## Problem + +`web/components/base/forms/Input.js` is a legacy class component at 231 lines. It handles multiple distinct input behaviours — text inputs, passwords (with toggle), search, checkboxes, and radio buttons — all within a single file with no TypeScript types. This makes it difficult to refactor safely, add new variants, or catch prop-related bugs at compile time. + +## Files + +- `web/components/base/forms/Input.js` — 231-line legacy class component with no TypeScript types + +## Proposed Fix + +Convert `Input.js` to a TypeScript functional component (`Input.tsx`). During the conversion, consider splitting out distinct variants via explicit props rather than implicit conditional logic: + +- `type="password"` — includes the show/hide toggle +- `type="search"` — includes the clear/search icon +- `type="checkbox"` and `type="radio"` — may warrant separate typed interfaces + +Ensure full TypeScript types are defined for all props. The conversion should produce identical visual and functional output — this is a type-safety and maintainability improvement, not a redesign. + +## Acceptance Criteria + +- [ ] `Input.js` converted to `Input.tsx` as a functional component +- [ ] Full TypeScript prop types defined (no `any`) +- [ ] Same visual output as before across all input variants (text, password, search, checkbox, radio) +- [ ] No runtime regressions in forms that use this component +- [ ] `npm run typecheck` passes with no new errors + +## Storybook Validation + +Not applicable — this is a type-safety refactor. Manual testing of all input variants in existing forms is sufficient. + +## Dependencies + +None. + +--- +Part of the Design System Audit (#6606) diff --git a/frontend/design-system-issues/ME-4-toast-dark-mode.md b/frontend/design-system-issues/ME-4-toast-dark-mode.md new file mode 100644 index 000000000000..0866d5c0dba8 --- /dev/null +++ b/frontend/design-system-issues/ME-4-toast-dark-mode.md @@ -0,0 +1,34 @@ +--- +title: "Toast notifications — add dark mode support" +labels: ["design-system", "medium-effort", "dark-mode"] +--- + +## Problem + +Toast notifications (`web/project/toast.tsx`) have no dark mode styles. In dark mode, toasts render with light mode colours, making them difficult to read. Additionally, the inline SVGs used for success and danger icons use hardcoded colour values rather than referencing design tokens or the shared `Icon` component, making them impossible to theme correctly. + +## Files + +- `web/project/toast.tsx` — no dark mode styles, hardcoded SVG colours for success/danger icons + +## Proposed Fix + +1. Add `.dark .toast-message` CSS overrides to give toasts an appropriate dark mode appearance with correct contrast. +2. Replace the hardcoded inline SVGs for success and danger icons with the shared `` component (available after ME-7 lands, or in parallel if Icon already supports these). + +## Acceptance Criteria + +- [ ] Toast notifications are readable in dark mode with correct contrast ratios +- [ ] Success and danger toast icons display correctly in both light and dark mode +- [ ] No visual regression in light mode + +## Storybook Validation + +Not applicable — toasts are triggered imperatively. Manual testing in both light and dark mode is required by opening any flow that triggers a success or error toast (e.g. creating a feature, deleting a flag). + +## Dependencies + +ME-7 (Consolidate SVG icon components) — preferred before replacing inline SVGs, but dark mode CSS fixes can land independently. + +--- +Part of the Design System Audit (#6606) diff --git a/frontend/design-system-issues/ME-5-unify-dropdowns.md b/frontend/design-system-issues/ME-5-unify-dropdowns.md new file mode 100644 index 000000000000..5dd63118b057 --- /dev/null +++ b/frontend/design-system-issues/ME-5-unify-dropdowns.md @@ -0,0 +1,47 @@ +--- +title: "Unify dropdown implementations" +labels: ["design-system", "medium-effort"] +--- + +## Problem + +4 different dropdown patterns exist in the codebase with no clear guidance on when to use each. This leads to inconsistent behaviour, duplicated positioning logic, and difficulty maintaining dropdown-related bugs in one place. + +The 4 patterns are: + +1. `base/DropdownMenu.tsx` — icon-triggered action menu (the canonical pattern) +2. `base/forms/ButtonDropdown.tsx` — split button dropdown +3. `navigation/AccountDropdown.tsx` — duplicates `DropdownMenu` positioning logic rather than reusing it +4. `segments/Rule/components/EnvironmentSelectDropdown.tsx` — form-integrated dropdown + +## Files + +- `web/components/base/DropdownMenu.tsx` — canonical action menu +- `web/components/base/forms/ButtonDropdown.tsx` — split button variant +- `web/components/navigation/AccountDropdown.tsx` — duplicates positioning logic from DropdownMenu +- `web/components/segments/Rule/components/EnvironmentSelectDropdown.tsx` — form-integrated variant + +## Proposed Fix + +1. Standardise on `DropdownMenu` for all action menus. +2. Refactor `AccountDropdown` to use `DropdownMenu` as its base, removing the duplicated positioning logic. +3. Document when to use `ButtonDropdown` vs `DropdownMenu` (e.g. in a Storybook story description or inline JSDoc). +4. Assess whether `EnvironmentSelectDropdown` can use an existing base or whether a documented form-dropdown pattern is needed. + +## Acceptance Criteria + +- [ ] `AccountDropdown` refactored to use `DropdownMenu` as its base +- [ ] No duplicated dropdown positioning logic between components +- [ ] Consistent dropdown behaviour (keyboard navigation, close on outside click) across all usages +- [ ] Documentation added (Storybook description or JSDoc) clarifying when to use each dropdown variant + +## Storybook Validation + +Design System / Navigation / Account Dropdown — verify dropdown opens, positions correctly, and closes on outside click in both light and dark mode. + +## Dependencies + +None. + +--- +Part of the Design System Audit (#6606) diff --git a/frontend/design-system-issues/ME-6-checkbox-switch-dark-mode.md b/frontend/design-system-issues/ME-6-checkbox-switch-dark-mode.md new file mode 100644 index 000000000000..f24fc5aec499 --- /dev/null +++ b/frontend/design-system-issues/ME-6-checkbox-switch-dark-mode.md @@ -0,0 +1,41 @@ +--- +title: "Checkbox and switch dark mode states" +labels: ["design-system", "medium-effort", "dark-mode"] +--- + +## Problem + +Two related dark mode gaps exist in form controls: + +1. `Switch.tsx` uses hardcoded colours for its sun and moon icons rather than CSS variables or `currentColor`. In dark mode the icons do not adapt correctly. +2. Form checkboxes and radio buttons (rendered via `Input.js`) have no `.dark` overrides in the SCSS. Their custom styles render with light mode colours in dark mode. + +## Files + +- `web/components/base/Switch.tsx` — hardcoded colours for sun/moon icons +- `web/components/base/forms/Input.js` — no dark mode overrides for checkbox/radio custom styles +- `web/styles/project/_forms.scss` (or equivalent) — location to add `.dark` checkbox/radio overrides + +## Proposed Fix + +1. In `Switch.tsx`, replace hardcoded colour values for the sun and moon icons with `currentColor` or CSS custom properties so they inherit from the surrounding dark mode context. +2. In the relevant SCSS file, add `.dark` overrides for the custom checkbox and radio button styles (border colour, background, checked state fill, focus ring). + +## Acceptance Criteria + +- [ ] `Switch` component sun/moon icons display correctly in both light and dark mode +- [ ] Checkboxes render correctly in dark mode (unchecked, checked, indeterminate, disabled states) +- [ ] Radio buttons render correctly in dark mode (unchecked, checked, disabled states) +- [ ] No visual regression in light mode for any of the above + +## Storybook Validation + +Design System / Forms / Switch — toggle dark mode and verify sun/moon icon colours. +Design System / Forms / Checkbox and Radio — toggle dark mode and verify all states. + +## Dependencies + +None. (ME-3 converts `Input.js` to TypeScript, but dark mode SCSS fixes can land independently of that.) + +--- +Part of the Design System Audit (#6606) diff --git a/frontend/design-system-issues/ME-7-consolidate-svg-icon-components.md b/frontend/design-system-issues/ME-7-consolidate-svg-icon-components.md new file mode 100644 index 000000000000..89cbb2ad0452 --- /dev/null +++ b/frontend/design-system-issues/ME-7-consolidate-svg-icon-components.md @@ -0,0 +1,53 @@ +--- +title: "Consolidate 23 separate SVG icon components" +labels: ["design-system", "medium-effort", "icons"] +--- + +## Problem + +23 SVG icon components exist outside the canonical `` API, spread across 3 directories. These components bypass the shared icon system, have no consistent pattern for props, sizing, or colour, and cannot be themed centrally (e.g. dark mode `currentColor` fix in `Icon.tsx` does not apply to them). + +The 23 components are distributed across: + +- `web/components/svg/` — 19 navigation/sidebar icons +- `web/components/base/icons/` — 2 icons (`GithubIcon`, `GitlabIcon`) +- `web/components/` — 2 icons (`IdentityOverridesIcon`, `SegmentOverridesIcon`) + +## Files + +- `web/components/svg/` — 19 SVG components outside the icon system +- `web/components/base/icons/GithubIcon.tsx` — standalone icon +- `web/components/base/icons/GitlabIcon.tsx` — standalone icon +- `web/components/IdentityOverridesIcon.tsx` — standalone icon +- `web/components/SegmentOverridesIcon.tsx` — standalone icon +- `web/components/Icon.tsx` — the canonical icon component these should integrate with + +## Proposed Fix + +Option A (minimal): Add each of the 23 icons to the `Icon.tsx` switch statement as named entries, keeping the inline SVG approach. + +Option B (preferred, long-term): Extract `Icon.tsx`'s existing inline SVGs into individual files and merge all 23 external icons into the same file-based structure. This makes the icon library easier to extend and avoids a monolithic switch statement. + +Whichever option is chosen, the result must be: +- All 23 icons accessible via `` +- Consistent `currentColor` usage so icons inherit colour from context +- Consistent sizing via the existing size prop + +## Acceptance Criteria + +- [ ] All 23 standalone SVG components accessible via `` +- [ ] Icons inherit colour via `currentColor` (no hardcoded fill/stroke values) +- [ ] Consistent size prop behaviour across all icons +- [ ] Original call sites updated to use `` +- [ ] Original standalone files deleted + +## Storybook Validation + +Design System / Icons / Separate SVG Components — all 23 icons should appear in the icon gallery story. Toggle dark mode and verify all icons adapt via `currentColor`. + +## Dependencies + +None. However, ME-4 (toast dark mode) prefers this to be complete first before replacing toast inline SVGs. + +--- +Part of the Design System Audit (#6606) diff --git a/frontend/design-system-issues/ME-8-storybook-a11y-addon.md b/frontend/design-system-issues/ME-8-storybook-a11y-addon.md new file mode 100644 index 000000000000..1fa3b88339de --- /dev/null +++ b/frontend/design-system-issues/ME-8-storybook-a11y-addon.md @@ -0,0 +1,54 @@ +--- +title: "Add @storybook/addon-a11y for component-level contrast checks" +labels: ["design-system", "medium-effort", "accessibility"] +--- + +## Problem + +Storybook has visual stories for design system components but no automated accessibility checking. Developers can only spot contrast and accessibility issues by eye. The existing axe-core E2E tests (added in #6562) operate at the page level and do not surface component-level violations during development. + +Without this addon, accessibility regressions in individual components are invisible until they reach a full page test — or until a user reports them. + +## Files + +- `.storybook/main.ts` — addon registration +- `.storybook/preview.ts` — optional: configure a11y rules globally (e.g. WCAG 2.1 AA) +- `package.json` — new dev dependency + +## Proposed Fix + +1. Install `@storybook/addon-a11y` as a dev dependency: + +```bash +npm install --save-dev @storybook/addon-a11y +``` + +2. Register the addon in `.storybook/main.ts`: + +```ts +addons: [ + // existing addons... + '@storybook/addon-a11y', +], +``` + +3. Optionally configure WCAG 2.1 AA as the default ruleset in `.storybook/preview.ts`. + +## Acceptance Criteria + +- [ ] `@storybook/addon-a11y` installed and registered +- [ ] An "Accessibility" tab appears in the Storybook addon panel for every story +- [ ] Accessibility violations are flagged automatically in the panel (not just manually checked) +- [ ] No existing Storybook stories broken by the addition +- [ ] `npm run typecheck` and `npm run lint` pass + +## Storybook Validation + +Any story → open the "Accessibility" tab in the addon panel → violations and passes listed automatically. Verify against a known contrast issue (e.g. a story with a light grey label on white) to confirm the addon is catching real violations. + +## Dependencies + +None. Complements but does not depend on ME-9 (expand E2E a11y coverage). + +--- +Part of the Design System Audit (#6606) diff --git a/frontend/design-system-issues/ME-9-expand-a11y-coverage.md b/frontend/design-system-issues/ME-9-expand-a11y-coverage.md new file mode 100644 index 000000000000..6a17a2b2f68e --- /dev/null +++ b/frontend/design-system-issues/ME-9-expand-a11y-coverage.md @@ -0,0 +1,63 @@ +--- +title: "Expand accessibility E2E coverage to all key pages" +labels: ["design-system", "medium-effort", "accessibility"] +--- + +## Problem + +The current axe-core accessibility test suite (added in #6562) only covers 6 scenarios: + +- Features list (light mode) +- Features list (dark mode) +- Project Settings +- Environment Switcher +- Create Feature modal (light mode) +- Create Feature modal (dark mode) + +The following key pages have no accessibility coverage at all: + +- Segments +- Audit Log +- Integrations +- Users & Permissions +- Organisation Settings +- Identities +- Release Pipelines +- Change Requests + +## Files + +- `e2e/tests/accessibility-tests.pw.ts` — existing test file to extend with additional page tests + +## Proposed Fix + +Add light and dark mode contrast tests for each missing page following the existing pattern in `e2e/tests/accessibility-tests.pw.ts`. Each page should have at minimum: + +1. A test that navigates to the page in light mode and runs `checkA11y()` +2. A test that toggles dark mode and runs `checkA11y()` + +Use the same axe-core configuration and violation thresholds already established in the existing tests. + +## Acceptance Criteria + +- [ ] Segments page covered in light and dark mode +- [ ] Audit Log page covered in light and dark mode +- [ ] Integrations page covered in light and dark mode +- [ ] Users & Permissions page covered in light and dark mode +- [ ] Organisation Settings page covered in light and dark mode +- [ ] Identities page covered in light and dark mode +- [ ] Release Pipelines page covered in light and dark mode +- [ ] Change Requests page covered in light and dark mode +- [ ] All new tests pass in CI +- [ ] No existing tests regressed + +## Storybook Validation + +Not applicable — these are E2E page-level tests, not Storybook component tests. + +## Dependencies + +ME-8 (@storybook/addon-a11y) is complementary but independent — this ticket covers E2E coverage, ME-8 covers component-level coverage. + +--- +Part of the Design System Audit (#6606) diff --git a/frontend/design-system-issues/QW-1-icon-currentcolor.md b/frontend/design-system-issues/QW-1-icon-currentcolor.md new file mode 100644 index 000000000000..545d240c0509 --- /dev/null +++ b/frontend/design-system-issues/QW-1-icon-currentcolor.md @@ -0,0 +1,46 @@ +--- +title: "Icon.tsx: Replace #1A2634 defaults with currentColor" +labels: ["design-system", "quick-win", "dark-mode", "icons"] +--- + +## Problem + +~54 SVG icons in `Icon.tsx` default their fill to `#1A2634` (dark navy). On dark mode backgrounds (`#101628`), this makes the icons effectively invisible — dark icon on a dark background with near-zero contrast. + +## Files + +- `web/components/Icon.tsx` — 46 instances of `fill={fill || '#1A2634'}`, plus hardcoded `#1A2634` stroke in 3 paths + +## Proposed Fix + +Replace all instances of: + +```tsx +fill={fill || '#1A2634'} +``` + +with: + +```tsx +fill={fill || 'currentColor'} +``` + +Also update any hardcoded `stroke="#1A2634"` to `stroke={stroke || 'currentColor'}` or inherit from `currentColor`. + +## Acceptance Criteria + +- [ ] All icons are visible in both light and dark mode +- [ ] No hardcoded `#1A2634` colour values remain in `Icon.tsx` +- [ ] Icons that receive an explicit `fill` prop continue to render that colour correctly +- [ ] No visual regression in light mode + +## Storybook Validation + +Design System / Icons / Dark Mode Broken — verify all icons in the story render correctly on a dark background after the fix. + +## Dependencies + +None — this is a prerequisite for QW-2. + +--- +Part of the Design System Audit (#6606) diff --git a/frontend/design-system-issues/QW-2-hardcoded-1a2634-in-components.md b/frontend/design-system-issues/QW-2-hardcoded-1a2634-in-components.md new file mode 100644 index 000000000000..d89c6e192b9f --- /dev/null +++ b/frontend/design-system-issues/QW-2-hardcoded-1a2634-in-components.md @@ -0,0 +1,55 @@ +--- +title: "Remove hardcoded #1A2634 from components outside Icon.tsx" +labels: ["design-system", "quick-win", "dark-mode"] +--- + +## Problem + +Several components outside `Icon.tsx` pass `#1A2634` directly as a `fill` prop to icons or as a `tick` fill to chart elements. These values are invisible in dark mode and bypass the theme system entirely. + +## Files + +- `web/components/StatItem.tsx:43` — `fill='#1A2634'` +- `web/components/Switch.tsx:57` — `fill={checked ? '#656D7B' : '#1A2634'}` +- `web/components/DateSelect.tsx:136` — `fill={isOpen ? '#1A2634' : '#9DA4AE'}` +- `web/components/pages/ScheduledChangesPage.tsx:126` — `fill={'#1A2634'}` +- `web/components/organisation-settings/usage/OrganisationUsage.container.tsx:63` — `tick={{ fill: '#1A2634' }}` +- `web/components/organisation-settings/usage/components/SingleSDKLabelsChart.tsx:62` — `tick={{ fill: '#1A2634' }}` + +Already correct (no action needed): + +- `web/components/CompareIdentities.tsx:214` — uses `getDarkMode()` conditional +- `web/components/RuleConditionValueInput.tsx:150` — uses `getDarkMode()` conditional + +## Proposed Fix + +- For icon `fill` props: after QW-1 ships, remove the explicit `fill` prop entirely so icons inherit `currentColor` from CSS context. +- For cases where a conditional colour is needed (e.g. `Switch.tsx`), replace with appropriate theme tokens. +- For chart tick fills: import `colorTextStandard` from `common/theme` and use it instead of the hardcoded hex. + +```tsx +// Before +tick={{ fill: '#1A2634' }} + +// After +import { colorTextStandard } from 'common/theme' +tick={{ fill: colorTextStandard }} +``` + +## Acceptance Criteria + +- [ ] No hardcoded `#1A2634` fill values remain in any component outside `Icon.tsx` +- [ ] All affected components render correctly in both light and dark mode +- [ ] Chart tick labels are legible in dark mode +- [ ] Theme token usage is consistent with the rest of the codebase + +## Storybook Validation + +Design System / Dark Mode Issues / Hardcoded Colours In Components — verify each listed component renders correctly on both light and dark backgrounds. + +## Dependencies + +Depends on QW-1 (Icon.tsx currentColor fix) being merged first, so that removing explicit `fill` props does not cause regressions. + +--- +Part of the Design System Audit (#6606) diff --git a/frontend/design-system-issues/QW-3-chart-axis-dark-mode.md b/frontend/design-system-issues/QW-3-chart-axis-dark-mode.md new file mode 100644 index 000000000000..397537347b48 --- /dev/null +++ b/frontend/design-system-issues/QW-3-chart-axis-dark-mode.md @@ -0,0 +1,50 @@ +--- +title: "Chart axis labels invisible in dark mode" +labels: ["design-system", "quick-win", "dark-mode"] +--- + +## Problem + +Recharts `XAxis` and `YAxis` components across multiple chart files use hardcoded hex values for `tick` fill colours. In dark mode, these tick labels become invisible because the hardcoded colours do not adapt to the background. + +## Files + +- `web/components/organisation-settings/usage/OrganisationUsage.container.tsx` — lines 56, 63 +- `web/components/organisation-settings/usage/components/SingleSDKLabelsChart.tsx` — lines 53, 62 +- `web/components/pages/admin-dashboard/components/UsageTrendsChart.tsx` +- `web/components/feature-page/FeatureNavTab/FeatureAnalytics.tsx` + +## Proposed Fix + +Import theme colour tokens and use them in place of hardcoded hex values: + +```tsx +import { colorTextStandard, colorTextSecondary } from 'common/theme' + +// Before + + + +// After + + +``` + +The tokens from `common/theme` already resolve to the correct value for the active colour mode, so no additional dark mode conditionals are needed. + +## Acceptance Criteria + +- [ ] All chart axis labels are legible in both light and dark mode +- [ ] No hardcoded hex colour values remain in chart tick configurations +- [ ] Token usage matches `colorTextStandard` and `colorTextSecondary` as appropriate to the label hierarchy + +## Storybook Validation + +Not directly applicable to Recharts charts, but manual verification on the Organisation Usage page and Feature Analytics page in dark mode will confirm the fix. + +## Dependencies + +None. + +--- +Part of the Design System Audit (#6606) diff --git a/frontend/design-system-issues/QW-4-release-pipeline-colours.md b/frontend/design-system-issues/QW-4-release-pipeline-colours.md new file mode 100644 index 000000000000..b02cd4a5994d --- /dev/null +++ b/frontend/design-system-issues/QW-4-release-pipeline-colours.md @@ -0,0 +1,48 @@ +--- +title: "Release pipeline hardcoded colours" +labels: ["design-system", "quick-win", "dark-mode", "colours"] +--- + +## Problem + +Release pipeline components use raw hex values for status indicator colours instead of theme tokens. These colours do not adapt to dark mode and are not tied to the design system token layer, making future theming changes harder to propagate. + +## Files + +- `web/components/release-pipelines/ReleasePipelinesList.tsx:169` — `color: isPublished ? '#6837FC' : '#9DA4AE'` +- `web/components/release-pipelines/ReleasePipelineDetail.tsx:106` — `color: isPublished ? '#6837FC' : '#9DA4AE'` (same pattern) +- `web/components/release-pipelines/StageCard.tsx:8` — `bg-white` Tailwind class hardcoded, no dark mode equivalent applied + +## Proposed Fix + +Replace hardcoded hex values with tokens from `common/theme`: + +```tsx +import { colorBrandPrimary, colorTextTertiary } from 'common/theme' + +// Before +color: isPublished ? '#6837FC' : '#9DA4AE' + +// After +color: isPublished ? colorBrandPrimary : colorTextTertiary +``` + +For `StageCard.tsx`, replace the hardcoded `bg-white` Tailwind class with the appropriate themed background token or a dark-mode-aware Tailwind variant (e.g. `bg-white dark:bg-[var(--color-bg-level-1)]`), consistent with how other card components handle their background. + +## Acceptance Criteria + +- [ ] Published/unpublished status colours in `ReleasePipelinesList` and `ReleasePipelineDetail` use theme tokens +- [ ] `StageCard` background renders correctly in both light and dark mode +- [ ] No hardcoded hex colour values remain in release pipeline components +- [ ] Visual appearance in light mode is unchanged + +## Storybook Validation + +Design System / Release Pipelines — verify status indicators and card backgrounds in both light and dark mode. + +## Dependencies + +None. + +--- +Part of the Design System Audit (#6606) diff --git a/frontend/design-system-issues/QW-5-remove-ionicons.md b/frontend/design-system-issues/QW-5-remove-ionicons.md new file mode 100644 index 000000000000..50936619f12d --- /dev/null +++ b/frontend/design-system-issues/QW-5-remove-ionicons.md @@ -0,0 +1,45 @@ +--- +title: "Remove unused ionicons dependency" +labels: ["design-system", "quick-win"] +--- + +## Problem + +The packages `ionicons` (v7.2.1) and `@ionic/react` (v7.5.3) are listed in `package.json` but are not imported or used anywhere in the codebase. These packages add unnecessary weight to the dependency tree and could be a source of supply chain risk. + +## Files + +- `package.json` — lists `ionicons` and `@ionic/react` as dependencies +- `package-lock.json` — will need to be updated after removal + +## Proposed Fix + +Uninstall both packages: + +```bash +npm uninstall ionicons @ionic/react +``` + +Verify no imports remain after removal: + +```bash +grep -r 'ionicons\|@ionic/react' web/ common/ --include='*.ts' --include='*.tsx' --include='*.js' +``` + +## Acceptance Criteria + +- [ ] `ionicons` and `@ionic/react` are removed from `package.json` and `package-lock.json` +- [ ] No import errors appear after removal +- [ ] `npm run build` completes successfully +- [ ] Bundle size is reduced (confirm with a before/after comparison if possible) + +## Storybook Validation + +Not applicable — this is a dependency cleanup task with no UI impact. + +## Dependencies + +None. + +--- +Part of the Design System Audit (#6606) diff --git a/frontend/design-system-issues/QW-6-hardcoded-icon-fills.md b/frontend/design-system-issues/QW-6-hardcoded-icon-fills.md new file mode 100644 index 000000000000..1a0fbaf5c748 --- /dev/null +++ b/frontend/design-system-issues/QW-6-hardcoded-icon-fills.md @@ -0,0 +1,58 @@ +--- +title: "Fix 9 icons with hardcoded fills that ignore the fill prop" +labels: ["design-system", "quick-win", "dark-mode", "icons"] +--- + +## Problem + +9 icons in `Icon.tsx` have fill colours baked directly into their SVG paths. Because these fills are hardcoded in the markup rather than applied via the `fill` prop, passing a `fill` value to the `` component has no effect. Several of these are invisible in dark mode; others may be intentional brand colours. + +## Files + +- `web/components/Icon.tsx` — the following icon definitions contain hardcoded fills: + +| Icon | Hardcoded Colour | Issue | +|------|-----------------|-------| +| `github` | `#1A2634` | Invisible in dark mode | +| `pr-draft` | `#1A2634` | Invisible in dark mode | +| `google` | Multi-colour brand colours | Intentional — keep as-is | +| `link` | `rgb(104, 55, 252)` | Should use `currentColor` | +| `pr-merged` | `#8957e5` | GitHub purple — intentional? Needs decision | +| `issue-closed` | `#8957e5` | GitHub purple — intentional? Needs decision | +| `issue-linked` | `#238636` | GitHub green — intentional? Needs decision | +| `pr-linked` | `#238636` | GitHub green — intentional? Needs decision | +| `pr-closed` | `#da3633` | GitHub red — intentional? Needs decision | + +## Proposed Fix + +Apply changes based on intent: + +**Clear fixes (no decision needed):** +- `github`: Replace hardcoded `#1A2634` path fills with `fill={fill || 'currentColor'}` +- `pr-draft`: Replace hardcoded `#1A2634` path fills with `fill={fill || 'currentColor'}` +- `link`: Replace `rgb(104, 55, 252)` with `fill={fill || 'currentColor'}` so it inherits brand purple from CSS context + +**Requires team decision:** +- `pr-merged`, `issue-closed`, `issue-linked`, `pr-linked`, `pr-closed`: Decide whether to keep the GitHub brand status colours (which are contextually meaningful) or replace with `currentColor` for theme consistency. If keeping brand colours, document this as intentional in a code comment. + +**Keep as-is:** +- `google`: Multi-colour brand logo — intentional, no change needed. + +## Acceptance Criteria + +- [ ] `github` and `pr-draft` icons are visible in dark mode +- [ ] `link` icon colour is controlled by CSS context / the `fill` prop +- [ ] A decision is recorded (in code comments or in this issue) for each GitHub status icon +- [ ] `google` icon is unchanged +- [ ] No unintended visual regressions in light mode + +## Storybook Validation + +Design System / Icons / Hardcoded Fills — verify each affected icon renders correctly in both light and dark backgrounds after the fix. + +## Dependencies + +Should be done after or alongside QW-1 for consistency, but can be worked independently. + +--- +Part of the Design System Audit (#6606) diff --git a/frontend/design-system-issues/QW-7-a11y-tests-ci.md b/frontend/design-system-issues/QW-7-a11y-tests-ci.md new file mode 100644 index 000000000000..bb37e69b967a --- /dev/null +++ b/frontend/design-system-issues/QW-7-a11y-tests-ci.md @@ -0,0 +1,48 @@ +--- +title: "Wire accessibility E2E tests into CI" +labels: ["design-system", "quick-win", "accessibility"] +--- + +## Problem + +6 axe-core/Playwright accessibility E2E tests exist in the repository but are not included in the CI pipeline. This means contrast ratio regressions and other accessibility violations can be merged to `main` without being caught automatically. + +## Files + +- `e2e/tests/accessibility-tests.pw.ts` — existing axe-core tests (6 tests) +- `e2e/helpers/accessibility.playwright.ts` — `checkA11y()` helper used by the tests +- CI config (GitHub Actions workflow file) — needs an accessibility test job added + +## Proposed Fix + +Add the accessibility tests to the existing Playwright CI job. The tests should be configured to fail only on `critical` and `serious` axe violations, not `moderate` or `minor`, to avoid excessive noise while still blocking regressions. + +Example configuration in the workflow: + +```yaml +- name: Run accessibility tests + run: npm run test -- e2e/tests/accessibility-tests.pw.ts + env: + E2E_RETRIES: 1 +``` + +If the existing CI job runs tests by tag, ensure the accessibility tests carry an appropriate tag (e.g. `@a11y`) so they can be targeted or excluded independently. + +## Acceptance Criteria + +- [ ] CI runs the accessibility tests on every pull request +- [ ] Tests fail on `critical` and `serious` axe violations +- [ ] Contrast ratio regressions block merging +- [ ] CI job name and step are clearly labelled as accessibility tests +- [ ] Existing test run time is not significantly impacted (accessibility tests are fast) + +## Storybook Validation + +Not applicable — this is a CI configuration task. + +## Dependencies + +None — tests already exist and pass locally. + +--- +Part of the Design System Audit (#6606) diff --git a/frontend/design-system-issues/QW-8-semi-bold-bug.md b/frontend/design-system-issues/QW-8-semi-bold-bug.md new file mode 100644 index 000000000000..f904e7973caa --- /dev/null +++ b/frontend/design-system-issues/QW-8-semi-bold-bug.md @@ -0,0 +1,44 @@ +--- +title: "Fix invalid fontWeight: 'semi-bold' in SuccessMessage" +labels: ["design-system", "quick-win", "typography"] +--- + +## Problem + +Both `SuccessMessage.tsx` and `SuccessMessage.js` use `fontWeight: 'semi-bold'` as an inline style. This is not a valid CSS `font-weight` value — the correct string value is `'600'` (a numeric string) or the number `600`. Browsers silently ignore invalid `font-weight` values, meaning the text renders at the default weight (400) instead of the intended semi-bold weight. + +## Files + +- `web/components/messages/SuccessMessage.tsx` — `fontWeight: 'semi-bold'` +- `web/components/SuccessMessage.js` — `fontWeight: 'semi-bold'` + +## Proposed Fix + +Replace the invalid string value with the numeric equivalent: + +```tsx +// Before +fontWeight: 'semi-bold' + +// After +fontWeight: 600 +``` + +If either file is used as the canonical source and the other is a duplicate or legacy version, consider whether `SuccessMessage.js` can be removed in favour of the TypeScript version as a follow-up. + +## Acceptance Criteria + +- [ ] `SuccessMessage` text renders at semi-bold (600) weight in the browser +- [ ] No `fontWeight: 'semi-bold'` string remains in either file +- [ ] No visual regression — the change should only make the intended weight actually apply + +## Storybook Validation + +Design System / Typography / Weight And Style Usage — the Bugs Found section should show `SuccessMessage` rendering at the correct weight after the fix. + +## Dependencies + +None. + +--- +Part of the Design System Audit (#6606) From 47546bd10be212cdcca093f8dccb2f2d5b0ec84e Mon Sep 17 00:00:00 2001 From: Talisson Costa Date: Thu, 5 Mar 2026 23:35:15 -0300 Subject: [PATCH 07/11] docs(design-system): add design system issues summary Part of #6606 Co-Authored-By: Claude Opus 4.6 --- frontend/DESIGN_SYSTEM_ISSUES.md | 459 +++++++++++++++++++++++++++++++ 1 file changed, 459 insertions(+) create mode 100644 frontend/DESIGN_SYSTEM_ISSUES.md diff --git a/frontend/DESIGN_SYSTEM_ISSUES.md b/frontend/DESIGN_SYSTEM_ISSUES.md new file mode 100644 index 000000000000..439b04b4db8a --- /dev/null +++ b/frontend/DESIGN_SYSTEM_ISSUES.md @@ -0,0 +1,459 @@ +# Design System — Actionable Issues + +**Related**: Issue #6606, `DESIGN_SYSTEM_AUDIT.md` (on `chore/design-system-audit-6606` branch), PR #6105 (Tailwind POC — not yet adopted) +**Date**: 2026-03-05 +**Storybook validation**: Run `npm run storybook` to see issues visually. Available stories: +- **Design System/Icons** — All Icons, Dark Mode Broken, Hardcoded Fills, Semantic Defaults, Separate SVG Components, Icon System Summary +- **Design System/Colours** — Semantic Tokens, Current SCSS Variables +- **Design System/Colours/Palette Audit** — Tonal Scale Inconsistency, Alpha Colour Mismatches, Orphan Hex Values, Grey Scale Gaps +- **Design System/Buttons** — All Variants, Dark Mode Gaps, Size Variants +- **Design System/Dark Mode Issues** — Hardcoded Colours In Components, Dark Mode Implementation Patterns, Theme Token Comparison +- **Design System/Typography** — Type Scale, Hardcoded Font Sizes, Proposed Tokens + +Each finding below is a potential GitHub issue. Grouped by size: quick wins first, then medium efforts, then large refactors. + +--- + +## Quick Wins (1–2 hours each) + +### QW-1: Icon.tsx — Replace `#1A2634` defaults with `currentColor` + +**Problem**: ~54 SVG icons in `Icon.tsx` default their `fill` to `#1A2634` (dark navy). This colour is invisible on dark mode backgrounds (`$body-bg-dark: #101628`). + +**Files**: +- `web/components/Icon.tsx` — 46 instances of `fill={fill || '#1A2634'}`, plus `stroke: fill || '#1a2634'` in 3 diff icon paths + +**Affected icons**: checkmark, chevron-down, chevron-left, chevron-right, chevron-up, arrow-left, arrow-right, clock, code, copy, copy-outlined, dash, diff, edit, edit-outlined, email, file-text, flash, flask, height, bell, calendar, layout, layers, list, lock, minus-circle, more-vertical, people, person, pie-chart, refresh, request, setting, settings-2, shield, star, timer, trash-2, bar-chart, award, options-2, open-external-link, features, rocket, expand, radio, and more. + +**Fix**: Replace all `fill={fill || '#1A2634'}` with `fill={fill || 'currentColor'}`. This inherits the text colour from the parent element, working in both light and dark mode automatically. + +**Impact**: Fixes ~54 broken icons in dark mode in one go. Highest ROI fix in the entire audit. + +**Validate**: Storybook → "Design System/Icons/Dark Mode Broken" (toggle dark mode in toolbar). + +--- + +### QW-2: Hardcoded `#1A2634` in components outside Icon.tsx + +**Problem**: Several components pass `#1A2634` directly to icons or chart elements. + +**Files & lines**: +| File | Line | Code | +|------|------|------| +| `web/components/StatItem.tsx` | 43 | `fill='#1A2634'` | +| `web/components/Switch.tsx` | 57 | `fill={checked ? '#656D7B' : '#1A2634'}` | +| `web/components/DateSelect.tsx` | 136 | `fill={isOpen ? '#1A2634' : '#9DA4AE'}` | +| `web/components/pages/ScheduledChangesPage.tsx` | 126 | `fill={'#1A2634'}` | +| `web/components/organisation-settings/usage/OrganisationUsage.container.tsx` | 63 | `tick={{ fill: '#1A2634' }}` | +| `web/components/organisation-settings/usage/components/SingleSDKLabelsChart.tsx` | 62 | `tick={{ fill: '#1A2634' }}` | + +**Fix**: After QW-1, stop passing `fill` entirely (icons will use `currentColor`). For chart ticks, import `colorTextStandard` from `common/theme` and use it directly. + +**Already correct**: `CompareIdentities.tsx:214` and `RuleConditionValueInput.tsx:150` use `getDarkMode()` conditionals. + +**Validate**: Storybook → "Design System/Dark Mode Issues/Hardcoded Colours In Components". + +--- + +### QW-3: Chart axis labels invisible in dark mode + +**Problem**: Recharts `` and `` components use hardcoded tick fill colours. + +**Files**: +- `web/components/organisation-settings/usage/OrganisationUsage.container.tsx` lines 56, 63 +- `web/components/organisation-settings/usage/components/SingleSDKLabelsChart.tsx` lines 53, 62 +- `web/components/pages/admin-dashboard/components/UsageTrendsChart.tsx` +- `web/components/feature-page/FeatureNavTab/FeatureAnalytics.tsx` + +**Fix**: Import `colorTextStandard` and `colorTextSecondary` from `common/theme` and use them for tick fills. This automatically adapts to light/dark mode. + +--- + +### QW-4: Release pipeline hardcoded colours + +**Problem**: Release pipeline status indicators use raw hex values instead of theme variables. + +**Files**: +- `web/components/release-pipelines/ReleasePipelinesList.tsx:169` — `color: isPublished ? '#6837FC' : '#9DA4AE'` +- `web/components/release-pipelines/ReleasePipelineDetail.tsx:106` — same pattern +- `web/components/release-pipelines/StageCard.tsx:8` — `bg-white` hardcoded, no dark mode + +**Fix**: Use tokens from `common/theme` — `colorBrandPrimary` and `colorTextTertiary`. + +--- + +### QW-5: Remove unused `ionicons` dependency + +**Problem**: `ionicons` (v7.2.1) and `@ionic/react` (v7.5.3) are installed in `package.json` but not used anywhere in the codebase. + +**Fix**: `npm uninstall ionicons @ionic/react`. Saves bundle size and removes a confusing dependency. + +--- + +### QW-6: Fix 9 icons with hardcoded fills that ignore the `fill` prop + +**Problem**: 9 icons have fills baked directly into SVG path elements, making the `fill` prop useless: + +| Icon | Colour | Issue | +|------|--------|-------| +| `github` | `#1A2634` | Invisible in dark mode | +| `pr-draft` | `#1A2634` | Invisible in dark mode | +| `google` | Multi-colour brand | Intentional — keep as-is | +| `link` | `rgb(104, 55, 252)` | Should use `$primary` / `currentColor` | +| `pr-merged` | `#8957e5` | GitHub purple — intentional? | +| `issue-closed` | `#8957e5` | Same | +| `issue-linked` | `#238636` | GitHub green — intentional? | +| `pr-linked` | `#238636` | Same | +| `pr-closed` | `#da3633` | GitHub red — intentional? | + +**Fix**: For `github` and `pr-draft`, switch to `fill={fill || 'currentColor'}`. For GitHub status icons, decide: use `currentColor` (adapts to theme) or keep brand colours (intentional). For `link`, use `currentColor`. + +**Validate**: Storybook → "Design System/Icons/Hardcoded Fills". + +--- + +### QW-7: Wire accessibility E2E tests into CI + +**Problem**: 6 axe-core/Playwright E2E tests already exist (`e2e/tests/accessibility-tests.pw.ts`) covering WCAG 2.1 AA compliance — light/dark contrast on Features page, Project Settings, Environment Switcher, and Create Feature modal. But they aren't wired into the CI pipeline, so contrast regressions can ship undetected. + +**Files**: +- `e2e/tests/accessibility-tests.pw.ts` — existing tests +- `e2e/helpers/accessibility.playwright.ts` — `checkA11y()` helper +- CI config (GitHub Actions workflow) — needs accessibility test job + +**Fix**: Add the accessibility tests to the existing Playwright CI job. Tests only fail on critical/serious violations, so they won't be noisy. + +**Impact**: Prevents contrast regressions from shipping. Zero new code needed — just CI config. + +--- + +### QW-8: Fix invalid `fontWeight: 'semi-bold'` in SuccessMessage + +**Problem**: `SuccessMessage.tsx` and `SuccessMessage.js` both use `fontWeight: 'semi-bold'` — this is not a valid CSS value. The browser ignores it entirely, so the text renders at the inherited weight instead of semi-bold. + +**Files**: +- `web/components/messages/SuccessMessage.tsx` +- `web/components/SuccessMessage.js` + +**Fix**: Replace `'semi-bold'` with `600` (the numeric value for semi-bold). + +--- + +## Medium Efforts (half-day to 1 day each) + +### ME-1: Consolidate 6 identical `ConfirmRemove*` modals + +**Problem**: Six deletion confirmation modals follow the exact same "type the name to confirm" pattern with nearly identical code: + +- `modals/ConfirmRemoveFeature.tsx` +- `modals/ConfirmRemoveSegment.tsx` +- `modals/ConfirmRemoveProject.tsx` +- `modals/ConfirmRemoveOrganisation.tsx` +- `modals/ConfirmRemoveEnvironment.tsx` +- `modals/ConfirmRemoveWebhook.tsx` + +**Fix**: Create a single `ConfirmRemoveModal` component that takes `entityType`, `entityName`, and `onConfirm` props. Replace all 6 files with imports of the shared component. + +**Impact**: Removes ~500 lines of duplicated code. + +--- + +### ME-2: Button variant dark mode gaps + +**Problem**: `btn-tertiary`, `btn-danger`, and `btn--transparent` have no dark mode overrides despite variables being defined (`$btn-tertiary-bg-dark`, etc.). + +**Files**: +- `web/styles/project/_buttons.scss` — needs `.dark` overrides for missing variants +- Variables already exist in `_variables.scss` lines 137–144 + +**Fix**: Add `.dark` selectors in `_buttons.scss` that use the existing dark mode variables. Test all button variants in both themes. + +**Validate**: Storybook → "Design System/Buttons/Dark Mode Gaps" (toggle dark mode in toolbar). + +--- + +### ME-3: Convert `Input.js` to TypeScript + +**Problem**: The main form input component `web/components/base/forms/Input.js` is a legacy class component (231 lines) that hasn't been converted to TypeScript. It handles text inputs, passwords, search, checkboxes, and radio buttons in one component. + +**Fix**: Convert to TypeScript functional component. Consider splitting password toggle and search input into separate variants via props rather than internal branching. + +--- + +### ME-4: Toast notifications — add dark mode support + +**Problem**: Toast notifications (`web/project/toast.tsx`) have no dark mode styles. The inline SVGs for success/danger icons use hardcoded colours. + +**Fix**: Add `.dark .toast-message` CSS overrides. Replace hardcoded SVGs with the `Icon` component (after QW-1 is done). + +--- + +### ME-5: Unify dropdown implementations + +**Problem**: 4 different dropdown patterns exist: +1. `base/DropdownMenu.tsx` — icon-triggered action menu +2. `base/forms/ButtonDropdown.tsx` — split button dropdown +3. `navigation/AccountDropdown.tsx` — account menu (duplicates DropdownMenu positioning logic) +4. `segments/Rule/components/EnvironmentSelectDropdown.tsx` — form-integrated dropdown + +**Fix**: Standardise on `DropdownMenu` for action menus. Refactor `AccountDropdown` to use `DropdownMenu` as its base. Document when to use `ButtonDropdown` vs `DropdownMenu`. + +--- + +### ME-6: Checkbox and switch dark mode states + +**Problem**: `Switch.tsx` uses hardcoded colours for sun/moon icons. Form checkboxes/radio buttons in `Input.js` have no dark mode overrides for their custom styles. + +**Fix**: Use CSS variables or `currentColor` for Switch icons. Add `.dark` overrides for checkbox/radio custom styles in SCSS. + +--- + +### ME-7: Consolidate 23 separate SVG icon components + +**Problem**: 23 SVG icon components exist outside of `Icon.tsx` across 3 directories: +- `web/components/svg/` — 19 navigation/sidebar icons (AuditLogIcon, FeaturesIcon, SegmentsIcon, etc.) +- `web/components/base/icons/` — 2 icons (GithubIcon, GitlabIcon) +- `web/components/` — 2 icons (IdentityOverridesIcon, SegmentOverridesIcon) + +These are imported directly by components, bypassing the `` API. There is no shared pattern for props, sizing, or colour handling. + +**Fix**: Either: +1. Add these to the `Icon.tsx` switch statement so all icons use one API, or +2. Extract `Icon.tsx`'s inline SVGs into individual files (part of LR-1) and merge all icons into the same structure. + +Option 2 is better long-term. Short-term, document which components import which icons. + +**Validate**: Storybook → "Design System/Icons/Separate SVG Components". + +--- + +### ME-8: Add `@storybook/addon-a11y` for component-level contrast checks + +**Problem**: Storybook is set up with visual stories for icons, buttons, colours, and dark mode issues, but has no automated accessibility checking. Developers can only spot contrast issues by eye. + +**Fix**: Install `@storybook/addon-a11y` and add to `.storybook/main.ts`. Every story automatically gets a WCAG 2.1 AA accessibility panel showing contrast failures, missing ARIA attributes, and keyboard issues — at component level, not page level. + +**Impact**: Complements the existing E2E axe-core tests (page-level) with a faster component-level feedback loop. Developers see violations while building, not after deployment. + +**Validate**: Storybook → any story → "Accessibility" tab in addon panel. + +--- + +### ME-9: Expand accessibility E2E coverage to all key pages + +**Problem**: The 6 existing axe-core tests only cover: Features page (light + dark), Project Settings, Environment Switcher, Create Feature modal. Many pages have no automated accessibility coverage at all. + +**Missing pages**: Segments, Audit Log, Integrations, Users & Permissions, Organisation Settings, Identities, Release Pipelines, Change Requests. + +**Fix**: Add light and dark mode contrast tests for each missing page, following the existing pattern in `e2e/tests/accessibility-tests.pw.ts`. Focus on critical/serious violations only. + +**Impact**: Catches contrast and ARIA issues across the full app surface area, not just a handful of pages. + +--- + +### ME-10: Typography inconsistencies — enforce the existing type scale + +**Problem**: An h1–h6 scale and body size variables exist in SCSS, but they're widely bypassed: + +**Font sizes** — 58 hardcoded inline `fontSize` values in TSX: + +| Value | Occurrences | Where | +|-------|-------------|-------| +| `fontSize: 13` | 17 | Admin dashboard, tables, various components | +| `fontSize: 12` | 12 | Labels, captions, badges | +| `fontSize: 11` | 7 | Table filters, small labels | +| `fontSize: 10` | 1 | Icon labels | +| Other (`14px`, `16px`, `18px`, `0.875rem`) | ~6 | Scattered | + +**Font weights** — 4 tiers are used in practice (700, 600, 500, 400) but applied inconsistently: +- Mixed systems: custom `.font-weight-medium` (500) alongside Bootstrap `fw-bold`/`fw-semibold`/`fw-normal` +- 9 inline `fontWeight` values in TSX bypassing classes entirely +- `fontWeight: 'semi-bold'` in `SuccessMessage.tsx` and `SuccessMessage.js` — **invalid CSS** (should be 600) + +**"Subtle" text** — achieved via opacity (0.4–0.75) rather than colour tokens, which is an accessibility concern for low-vision users. Classes `.faint`, `.text-muted` exist but compete with ad-hoc opacity values. + +**Utility classes** — minimal and inconsistent. Only `.text-small`, `.large-para`, `.font-weight-medium`, `.bold-link`. No systematic set. + +**Fix**: +1. Replace the 58 hardcoded `fontSize` values with existing SCSS variables or CSS classes +2. Standardise on one weight class system (either custom or Bootstrap, not both) +3. Replace opacity-based muted text with colour-based approach (`$text-muted` / `colorTextSecondary`) +4. Fix the `semi-bold` bug in SuccessMessage + +**Validate**: Storybook → "Design System/Typography". + +--- + +## Large Refactors (multi-day) + +### LR-1: Icon.tsx — break up monolithic component + +**Problem**: `Icon.tsx` is 1,543 lines containing 70+ inline SVG definitions in a single switch statement. It's the single largest component in the codebase. Adding, modifying, or debugging icons requires navigating this massive file. + +One icon (`paste`) is declared in the `IconName` type but has no implementation. + +**Fix**: Extract each icon into its own file under `web/components/icons/`. Create an `IconMap` that lazy-loads icons by name. Keep the `` API but back it with individual files. + +**Prerequisite**: QW-1 (fix `currentColor` defaults first). + +--- + +### LR-2: Introduce semantic colour tokens (CSS custom properties) + +**Problem**: Dark mode is implemented via 3 parallel mechanisms that don't compose: +1. **SCSS `.dark` selectors** (48 rules across 29 files) — compile-time, can't be used in inline styles +2. **`getDarkMode()` runtime calls** (13 components) — manual ternaries, easy to forget +3. **Bootstrap `data-bs-theme`** — set but underused, conflicts with manual `.dark` selectors + +Variables use a `$component-property-dark` suffix convention (`$input-bg-dark`, `$panel-bg-dark`, etc.) requiring duplicate SCSS rules for every property. + +**Fix**: Introduce CSS custom properties via `common/theme/tokens.ts` + `web/styles/_tokens.scss` (already drafted on this branch). These tokens: +- Define light values on `:root` and dark values on `[data-bs-theme='dark']` +- Are importable in TS files: `import { colorTextStandard } from 'common/theme'` +- Eliminate `getDarkMode()` calls entirely +- Gradually replace `.dark` selector pairs + +**Migration path**: +1. Token files already exist (drafted) +2. Fix Icon.tsx with `currentColor` (QW-1) — no tokens needed +3. Migrate `getDarkMode()` callsites (13 files) to use token imports +4. Migrate `.dark` selectors component-by-component +5. Remove orphaned `$*-dark` SCSS variables + +**Validate**: Storybook → "Design System/Colours/Semantic Tokens" (toggle dark mode to see tokens flip). Also see "Design System/Dark Mode Issues/Theme Token Comparison" for before/after code examples. + +--- + +### LR-3: Modal system — migrate from global imperative API + +**Problem**: The modal system uses `openModal()`, `openModal2()`, `openConfirm()` as global functions attached to `window`. It uses deprecated `react-dom` `render`/`unmountComponentAtNode` APIs (removed in React 18). `openModal2` exists for stacking modals, acknowledged in code comments as a pattern to avoid. + +**Fix**: Migrate to a React context-based modal manager. Replace global imperative calls with `useModal()` hook. This is a large effort but unblocks React 18 compatibility. + +--- + +### LR-4: Standardise table/list components + +**Problem**: No unified table/list component system. Each feature area builds its own: +- 9 different `TableFilter*` components +- 5+ different `*Row` components (FeatureRow, ProjectFeatureRow, FeatureOverrideRow, OrganisationUsersTableRow, etc.) +- 5+ different `*List` components + +**Fix**: Create a composable `` / `` component system with standardised `Row`, `Cell`, and `Header` sub-components. Migrate feature areas one at a time. + +--- + +### LR-5: Remove legacy JS class components + +**Problem**: Several components remain as `.js` class components: +- `base/forms/Input.js` +- `base/forms/InputGroup.js` +- `modals/CreateProject.js` +- `modals/CreateWebhook.js` (coexists with `.tsx` version) +- `modals/Payment.js` +- Layout: `Flex.js`, `Column.js` + +**Fix**: Convert each to TypeScript functional components. Remove `CreateWebhook.js` duplicate. + +--- + +### LR-6: Define a formal colour palette + +**Problem**: There is no formal, systematic colour palette. The current state: + +1. **Inconsistent tonal scales**: `$primary400` (`#956CFF`) is *lighter* than `$primary` (`#6837FC`), reversing the typical convention where higher numbers = darker. `$bg-dark500` is darker than `$bg-dark100`, but text/grey scales don't follow a pattern at all. + +2. **Alpha colour RGB mismatches**: The alpha variants use different base RGB values than their solid counterparts: + | Token | Solid hex | Solid RGB | Alpha base RGB | + |-------|-----------|-----------|----------------| + | `$primary` / `$primary-alfa-*` | `#6837FC` | `(104, 55, 252)` | `(149, 108, 255)` | + | `$danger` / `$danger-alfa-*` | `#EF4D56` | `(239, 77, 86)` | `(255, 66, 75)` | + | `$warning` / `$warning-alfa-*` | `#FF9F43` | `(255, 159, 67)` | `(255, 159, 0)` | + +3. **30+ orphan hex values in components**: Colours used directly in TSX/SCSS that don't exist in `_variables.scss`: + - `#9DA4AE` (52 uses) — a grey with no variable + - `#656D7B` (44 uses) — has variable `$text-icon-grey` but most callsites use raw hex + - `#e74c3c`, `#53af41`, `#767d85`, `#5D6D7E`, `#343a40`, `#1c2840`, `#8957e5`, `#238636`, `#da3633` etc. + +4. **Missing scale steps**: No `$danger600`, no `$success700`, no `$info400`, no `$warning200`. Each colour has a different number of tonal variants, making the system unpredictable. + +5. **No grey scale**: Greys are named ad hoc (`$text-icon-grey`, `$text-icon-light-grey`, `$bg-light200`, `$footer-grey`) with no numbered scale. + +**Fix**: Define a formal primitive palette with consistent naming: +- Numbered tonal scales (50–900) for each hue, following the convention: lower numbers = lighter +- Derive alpha variants from the same RGB as the solid colour +- Create a proper grey/neutral scale +- Map every orphan hex to a palette token or remove it + +**Impact**: This is the prerequisite for semantic tokenisation (LR-2). Without a consistent palette, tokens just paper over the mess. + +**Note**: PR #6105 is a Tailwind CSS POC (not yet adopted). If Tailwind is adopted, `theme.extend.colors` could be the home for the palette definition, generating CSS custom properties consumed by both utility classes and the semantic token layer. The palette work itself is Tailwind-agnostic — it needs doing regardless. + +**Validate**: Storybook → "Design System/Colours/Palette Audit" — four stories covering tonal scale inconsistency, alpha mismatches, orphan hex values, and grey scale gaps. + +--- + +### LR-7: Full dark mode theme audit (umbrella) + +**Problem**: Only 48 `.dark` CSS selectors exist across the entire stylesheet. Many component areas have zero dark mode coverage: +- Feature pipeline visualisation (white circles, grey lines on dark background) +- Admin dashboard charts +- Integration cards +- Numerous inline styles with light-mode-only colours + +**Fix**: Systematic page-by-page dark mode audit. For each page: +1. Toggle dark mode +2. Screenshot +3. Identify contrast failures +4. Add `.dark` overrides or switch to `currentColor` / CSS variables + +This is the umbrella issue — all the QW and ME items above contribute to this. + +**Validate**: Storybook → "Design System/Dark Mode Issues/Dark Mode Implementation Patterns" — shows all 3 parallel mechanisms and the proposed solution. + +--- + +### LR-8: Standardise typography usage across the codebase + +**Problem**: The type scale (h1–h6, body sizes, weight tiers) exists in SCSS but is bypassed in 58+ places with inline styles. The weight system is fragmented between custom classes (`.font-weight-medium`) and Bootstrap utilities (`fw-bold`, `fw-semibold`). "Subtle" text uses opacity instead of semantic colour. Without enforcing the existing scale, inconsistencies will keep growing. + +**Fix**: +1. Replace all 58 inline `fontSize` values with existing SCSS variables or utility classes +2. Pick one weight class system and migrate the other (e.g. standardise on Bootstrap `fw-*` and remove `.font-weight-medium`, or vice versa) +3. Replace opacity-based muted text patterns with `$text-muted` / `colorTextSecondary` +4. Optionally introduce a `` component if the migration shows repeated patterns that would benefit from a constrained API — but only if the need is demonstrated, not upfront + +**Prerequisite**: ME-10 (audit and consolidate the type scale first). + +--- + +## Summary + +| Category | Count | Estimated total effort | +|----------|-------|----------------------| +| Quick wins | 8 | 1–2 days | +| Medium efforts | 10 | 5–10 days | +| Large refactors | 8 | 4–7 weeks | + +**Recommended order**: QW-1 → QW-5 → QW-6 → QW-7 → QW-2 → QW-3 → ME-8 → ME-2 → ME-4 → ME-10 → ME-9 → ME-7 → ME-1 → LR-1 → LR-6 → LR-2 → LR-8 → rest. + +**Key dependency chains**: +- QW-1 (icon currentColor) → QW-2 (component hardcoded colours) → LR-1 (break up Icon.tsx) +- LR-6 (formal palette) → LR-2 (semantic tokens) +- ME-10 (typography tokens) → LR-8 (Text/Heading components) +- QW-7 (CI a11y tests) → ME-9 (expand a11y coverage) + +--- + +## Appendix: Icon System Inventory + +| Category | Count | Details | +|----------|-------|---------| +| Icons in `Icon.tsx` switch | 70 | All inline SVGs, 1,543 lines | +| Icons declared but not implemented | 1 | `paste` — in type, no case | +| Separate SVG components | 23 | Across `svg/`, `base/icons/`, root | +| Integration SVG files | 37 | In `/static/images/integrations/` | +| Icons defaulting to `#1A2634` | ~54 | Invisible in dark mode | +| Icons with hardcoded fills | 9 | Cannot be overridden via props | +| Icons using `currentColor` | 0 | None | +| Unused icon dependency | `ionicons` v7.2.1 | Installed but never imported | From 07d65615d7e9539d8cd91d625fa73d6122df3161 Mon Sep 17 00:00:00 2001 From: Talisson Costa Date: Mon, 9 Mar 2026 10:02:29 -0300 Subject: [PATCH 08/11] docs: sync issue reports with PR findings and add missing issue files Updates QW-1 to reflect expanded scope in PR #6870 (57 fills, not 41). Adds PR links and scope changes to QW-4, QW-5, QW-8, QW-10. Creates missing issue files for LR-1 through LR-9, ME-9, ME-11. Adds audit verification report and compliance audit summary. Co-Authored-By: Claude Opus 4.6 --- frontend/DESIGN_SYSTEM_COMPLIANCE_AUDIT.md | 252 ++++++++++++++++++ .../AUDIT_VERIFICATION.md | 113 ++++++++ .../LR-1-icon-refactor.md | 103 +++++++ .../design-system-issues/LR-3-modal-system.md | 144 ++++++++++ .../LR-5-js-to-typescript.md | 119 +++++++++ .../LR-6-colour-primitives.md | 147 ++++++++++ .../LR-7-dark-mode-full-pass.md | 135 ++++++++++ .../LR-8-typography-tokens.md | 156 +++++++++++ .../LR-9-remove-global-component-registry.md | 119 +++++++++ .../ME-11-offgrid-spacing.md | 120 +++++++++ .../ME-9-storybook-a11y-addon.md | 117 ++++++++ .../QW-1-icon-currentcolor.md | 62 +++-- .../QW-10-decouple-button-from-store.md | 58 ++++ .../QW-4-sidebarlink-bugs.md | 51 ++++ ...QW-5-remove-duplicate-legacy-components.md | 110 ++++++++ .../QW-8-successmessage-fontweight.md | 41 +++ 16 files changed, 1822 insertions(+), 25 deletions(-) create mode 100644 frontend/DESIGN_SYSTEM_COMPLIANCE_AUDIT.md create mode 100644 frontend/design-system-issues/AUDIT_VERIFICATION.md create mode 100644 frontend/design-system-issues/LR-1-icon-refactor.md create mode 100644 frontend/design-system-issues/LR-3-modal-system.md create mode 100644 frontend/design-system-issues/LR-5-js-to-typescript.md create mode 100644 frontend/design-system-issues/LR-6-colour-primitives.md create mode 100644 frontend/design-system-issues/LR-7-dark-mode-full-pass.md create mode 100644 frontend/design-system-issues/LR-8-typography-tokens.md create mode 100644 frontend/design-system-issues/LR-9-remove-global-component-registry.md create mode 100644 frontend/design-system-issues/ME-11-offgrid-spacing.md create mode 100644 frontend/design-system-issues/ME-9-storybook-a11y-addon.md create mode 100644 frontend/design-system-issues/QW-10-decouple-button-from-store.md create mode 100644 frontend/design-system-issues/QW-4-sidebarlink-bugs.md create mode 100644 frontend/design-system-issues/QW-5-remove-duplicate-legacy-components.md create mode 100644 frontend/design-system-issues/QW-8-successmessage-fontweight.md diff --git a/frontend/DESIGN_SYSTEM_COMPLIANCE_AUDIT.md b/frontend/DESIGN_SYSTEM_COMPLIANCE_AUDIT.md new file mode 100644 index 000000000000..9b8695073b06 --- /dev/null +++ b/frontend/DESIGN_SYSTEM_COMPLIANCE_AUDIT.md @@ -0,0 +1,252 @@ +# Design System Compliance Audit + +**Date**: 2026-03-06 +**Scope**: 525 component files across `web/components/` +**Prior work**: `DESIGN_SYSTEM_AUDIT.md` (2026-02-27), `DESIGN_SYSTEM_ISSUES.md` (2026-03-05), 28 issue files in `design-system-issues/` + +--- + +## Executive Summary + +**Components reviewed:** 525 files across 11 UI categories +**Issues found:** 85 tracked (21 P0 · 34 P1 · 30 P2) +**Quick wins completed:** 0 of 8 +**Overall compliance score:** 47 / 100 + +A comprehensive audit was completed one week ago and 28 individual issue files were created. As of today, **none of the quick wins have been addressed**. The highest-ROI fixes — Icon.tsx dark mode, toast dark mode, and two known bugs — remain open and are blocking dark mode quality across the entire UI. + +--- + +## Compliance Status by Category + +### Naming Consistency + +| Issue | Components Affected | Status | +|-------|---------------------|--------| +| 50 remaining `.js` files alongside `.tsx` counterparts | `Flex.js`, `Column.js`, `CreateProject.js`, `Payment.js`, `OrganisationSelect.js`, `ProjectSelect.js`, `FlagSelect.js`, and 43 others | Open | +| Duplicate coexisting files | `CreateWebhook.js` + `CreateWebhook.tsx`, `ErrorMessage.js` + `ErrorMessage.tsx`, `SuccessMessage.js` + `SuccessMessage.tsx` | Open | +| `InlineModal` named `displayName = 'Popover'` | `web/components/InlineModal.tsx` | Open | +| `inactiveClassName={activeClassName}` copy-paste bug | `web/components/navigation/SidebarLink.tsx:31` | Open — confirmed | + +**Score: 4 / 10** — 10.5% of component files are unconverted JS. Three literal naming bugs are confirmed. + +--- + +### Token Coverage + +#### Colours + +| Category | Defined tokens | Hardcoded values found | Status | +|----------|---------------|------------------------|--------| +| Brand colours | ✅ `$primary`, `$secondary`, etc. | 323 instances in TSX alone | Open | +| Grey scale | ⚠️ Ad hoc only | `#9DA4AE` (52 uses), `#656D7B` (44 uses) | Open | +| Alpha variants | ⚠️ Wrong base RGB | `rgba(149,108,255,…)` vs `#6837fc` actual | Open | +| Orphan hex | — | 30+ values with no token home | Open | + +**Score: 3 / 10** — 280+ hardcoded hex instances in TSX. The grey scale has no formal primitives. Alpha variants use an incorrect base RGB that doesn't match their solid counterpart. + +#### Typography + +| Category | Status | +|----------|--------| +| Font size scale | Defined but bypassed in 6+ files | +| Font weight tokens | ❌ No general-purpose weight tokens (`$font-weight-*` missing) | +| Line-height tokens | Partially defined; 4 off-scale values in use | +| Inline style overrides | 50+ in admin dashboard alone | + +**Score: 4 / 10** — No `$font-weight-semibold` token exists; `fontWeight: 600` appears hardcoded in 3 files. One confirmed invalid CSS value: `fontWeight: 'semi-bold'` in `SuccessMessage.tsx:44` (browser ignores it silently). + +#### Spacing + +| Category | Status | +|----------|--------| +| Base grid (8px) | Defined (`$spacer`) but widely ignored | +| Off-grid offenders | `5px` (10+ occurrences), `6px` (3+ files), `3px`, `15px`, `19px` | +| Chip margin inconsistency | `.chip-sm` uses `5px`, `.chip` uses `8px` | + +**Score: 5 / 10** — `5px` is the single largest off-grid offender. A bulk replace of `5px → 4px` would be the highest-impact spacing fix. + +--- + +### Component Completeness + +#### Buttons — `web/components/base/forms/Button.tsx` + +| Variant | Light mode | Dark mode | Score | +|---------|-----------|-----------|-------| +| primary | ✅ | ✅ | 10/10 | +| secondary | ✅ | ✅ | 10/10 | +| tertiary | ✅ | ❌ No override | 5/10 | +| danger | ✅ | ❌ Resolves to primary colour on hover | 3/10 | +| btn--transparent | ✅ | ❌ `rgba(0,0,0,0.1)` invisible on dark | 3/10 | +| btn--outline | ✅ | ⚠️ Hardcodes `background: white` | 6/10 | + +Focus rings are explicitly removed via `btn:focus-visible { box-shadow: none }` — **WCAG 2.4.11 failure**. + +#### Icons — `web/components/Icon.tsx` + `web/components/svg/` + +| Issue | Count | Status | +|-------|-------|--------| +| `fill={fill \|\| '#1A2634'}` defaults | **41 instances** | Open — not fixed | +| `fill='#000000'` on expand icon | 1 | Open | +| `fill='rgb(104,55,252)'` hardcoded (link icon) | 1 | Open | +| SVG components not using `currentColor` | 19 of 19 | Open | +| Three separate icon systems | 1 | Open | + +**Score: 1 / 10** — This is the single highest-priority item in the codebase. 41 icons are invisible in dark mode; the fix is a one-line change per icon. + +#### Modals — `web/components/modals/` + +| Issue | Status | +|-------|--------| +| 14 near-identical confirmation modals | Open | +| `openModal`/`closeModal` on `global` (deprecated React 18 API) | Open | +| `openModal2` anti-pattern acknowledged but in use | Open | +| `CreateProject.js` (class component, JS) | Open | +| `Payment.js` (class component, JS) | Open | + +**Score: 4 / 10** — Core infrastructure uses a React 16-era imperative API. 14 confirmation modals each implement their own layout. + +#### Notifications/Alerts + +| Component | States | Variants | Dark mode | Score | +|-----------|--------|----------|-----------|-------| +| Toast | success, danger | 2 | ❌ No `.dark` block | 4/10 | +| ErrorMessage | — | 1 | ✅ | 7/10 | +| SuccessMessage | — | 1 | ✅ | 6/10 (fontWeight bug) | +| InfoMessage | — | 1 | ⚠️ Partial | 6/10 | +| WarningMessage | — | 1 | ❌ Icon invisible | 4/10 | + +**Score: 5 / 10** — Toast has zero dark mode styles. `SuccessMessage` has an invalid CSS value (`fontWeight: 'semi-bold'`) that browsers silently ignore. + +#### Forms/Inputs + +| Issue | Severity | +|-------|----------| +| `input:read-only` hardcodes `#777`, no dark override | P0 | +| Checkbox focus/hover/disabled states missing in dark | P1 | +| Switch `:focus` not overridden in dark | P1 | +| Textarea border invisible in dark (`border-color: $input-bg-dark`) | P1 | +| Focus ring rgba uses wrong base (`rgba(51,102,255,…)` not `$primary`) | P2 | + +**Score: 5 / 10** + +#### Tables — `PanelSearch.tsx` + `web/components/tables/` + +Reasonably well-structured. Filter components compose from a shared `TableFilter` base. `PanelSearch` is monolithic (20+ props) but functional. + +**Score: 7 / 10** + +#### Tabs — `web/components/navigation/TabMenu/` + +Well-consolidated. Minor prop bloat (feature-specific `isRoles` prop). + +**Score: 8 / 10** + +#### Layout — `web/components/base/grid/` + +`Panel.tsx` is a class component. `Flex.js` and `Column.js` are the oldest-style components in the codebase (class, `module.exports`, `propTypes`). `AccordionCard` imports `@material-ui/core` — the only Material UI usage, adding a heavy dependency for a single collapse animation. + +**Score: 4 / 10** + +--- + +### Accessibility (WCAG 2.1 AA) + +| Check | Result | +|-------|--------| +| Secondary text contrast (both themes) | **FAIL** — `#656d7b` on white = 4.48:1; on dark = ~4.2:1. Both below 4.5:1. | +| Input border visibility | **FAIL** — 16% opacity border on white background | +| Button focus rings | **FAIL** — explicitly removed in `_buttons.scss` | +| Disabled element contrast | **FAIL** — `opacity: 0.32` on already-low-contrast elements | +| 54+ icons in dark mode | **FAIL** — invisible (`#1A2634` on `#101628`) | +| `WarningMessage` icon in dark mode | **FAIL** — icon fill invisible | + +**Accessibility score: 3 / 10** — Multiple systemic WCAG AA failures, including in base tokens. + +--- + +### Storybook / Documentation Coverage + +| Category | Stories exist | A11y addon | Score | +|----------|--------------|------------|-------| +| Icons | ✅ | ❌ | 6/10 | +| Colours | ✅ | — | 7/10 | +| Buttons | ✅ | ❌ | 6/10 | +| Dark Mode Issues | ✅ | ❌ | 6/10 | +| Typography | ✅ | — | 7/10 | +| Forms/Inputs | ❌ | — | 0/10 | +| Modals | ❌ | — | 0/10 | +| Tables | ❌ | — | 0/10 | +| Tooltips | ❌ | — | 0/10 | + +**Score: 4 / 10** — 6 of 9 audited categories have no Storybook stories. The accessibility addon (`@storybook/addon-a11y`) is not installed, so the existing stories cannot surface WCAG issues automatically. + +--- + +## Priority Actions + +### Unblocking (do this week) + +These 5 items require less than 2 hours each and unlock dark mode quality across the entire UI. + +1. **QW-1 — Icon.tsx default fills** `web/components/Icon.tsx` + Replace 41 instances of `fill={fill || '#1A2634'}` with `fill={fill || 'currentColor'}`. Fixes dark mode for ~54 icons in one file change. See `design-system-issues/QW-1-icon-currentcolor.md`. + +2. **QW-8 — SuccessMessage fontWeight bug** `web/components/messages/SuccessMessage.tsx:44` + Change `fontWeight: 'semi-bold'` to `fontWeight: 600`. The string value is invalid CSS and silently ignored by browsers. + +3. **ME-4 — Toast dark mode** `web/styles/components/_toast.scss` + Add `.dark` block using existing `$success-solid-dark-alert` and `$danger-solid-dark-alert` tokens. See `design-system-issues/ME-4-toast-dark-mode.md`. + +4. **Fix SidebarLink bugs** `web/components/navigation/SidebarLink.tsx` + Line 31: `inactiveClassName={activeClassName}` should be `inactiveClassName={inactiveClassName}`. Line 42: remove hardcoded `fill={'#767D85'}`. These two bugs make inactive nav items render as active. + +5. **Remove duplicate legacy components** + Delete `web/components/ErrorMessage.js` and `web/components/SuccessMessage.js`. Both have TypeScript replacements in `web/components/messages/`. + +### Medium term (this sprint) + +6. **ME-2 — Button dark mode gaps** — Add `.dark` overrides for `btn-tertiary`, `btn-danger`, `btn--transparent` in `_buttons.scss`. See `design-system-issues/ME-2-button-dark-mode-gaps.md`. +7. **QW-3 — Chart axis dark mode** — Use `colorTextStandard`/`colorTextSecondary` from `common/theme` for Recharts tick fills. See `design-system-issues/QW-3-chart-axis-dark-mode.md`. +8. **ME-6 — Checkbox/switch dark mode focus states** — Complete missing `:focus`, `:hover`, `:disabled` dark overrides in `_input.scss` and `_switch.scss`. +9. **ME-9 — Expand accessibility coverage** — Install `@storybook/addon-a11y` and add it to the Storybook config. See `design-system-issues/ME-9-expand-a11y-coverage.md`. +10. **Standardise off-grid spacing** — Bulk replace `5px → 4px` in SCSS (highest-frequency offender, 10+ occurrences). + +### Design system roadmap (next quarter) + +These are tracked in the `LR-*` issue files in `design-system-issues/`: + +- **LR-1** — Refactor `Icon.tsx`: extract inline SVGs to individual files, integrate `svg/` directory, retire IonIcon +- **LR-2** — Implement semantic colour token architecture (3-layer: primitives → semantic → component) +- **LR-3** — Migrate modal system from global imperative API to React context manager +- **LR-5** — Convert remaining 50 `.js` components to TypeScript +- **LR-6** — Define formal colour primitive palette (`$neutral-0` to `$neutral-950`, `$purple-*`) +- **LR-7** — Full dark mode audit pass across all SCSS files +- **LR-8** — Introduce `$font-weight-*` tokens; standardise typography scale + +--- + +## Quick Reference: Score Summary + +| Category | Score | +|----------|-------| +| Naming consistency | 4 / 10 | +| Colour token coverage | 3 / 10 | +| Typography tokens | 4 / 10 | +| Spacing tokens | 5 / 10 | +| Button component | 5 / 10 | +| Icon component | 1 / 10 | +| Modal system | 4 / 10 | +| Notifications/Alerts | 5 / 10 | +| Forms/Inputs | 5 / 10 | +| Tables | 7 / 10 | +| Tabs | 8 / 10 | +| Layout | 4 / 10 | +| Accessibility (WCAG) | 3 / 10 | +| Storybook coverage | 4 / 10 | +| **Overall** | **47 / 100** | + +--- + +*For detailed findings per issue, see the `design-system-issues/` directory. For colour token architecture, see `COLOUR_TOKEN_PLAN.md`. For the full component-by-component breakdown, see `DESIGN_SYSTEM_AUDIT.md`.* diff --git a/frontend/design-system-issues/AUDIT_VERIFICATION.md b/frontend/design-system-issues/AUDIT_VERIFICATION.md new file mode 100644 index 000000000000..6a781a28ad26 --- /dev/null +++ b/frontend/design-system-issues/AUDIT_VERIFICATION.md @@ -0,0 +1,113 @@ +# Audit Verification Report + +All claims in the Dark Mode Audit and Light Mode Audit boards have been verified against the codebase. + +--- + +## Dark Mode Audit — All 6 claims VALID + +### 1. Surface layers nearly identical + +- `$body-bg-dark: #101628` +- `$panel-bg-dark: #15192b` +- `$input-bg-dark: #161d30` +- Only 5–18 hex values apart — no visual separation between layers + +### 2. Secondary text barely meets AA + +- `$text-muted: #656d7b` on `#15192b` background +- Contrast ratio: ~4.2:1 +- WCAG AA requires 4.5:1 for normal text — **this fails** +- Note: the audit board says "barely meets" but it actually fails AA + +### 3. Form inputs blend into panels + +- Input background: `#161d30` +- Panel background: `#15192b` +- Difference: 5 hex values +- `$input-border-color: rgba(101, 109, 123, 0.16)` — 16% opacity border provides almost no distinction + +### 4. Charts use hardcoded light colours + +- 3 of 4 chart files affected +- `OrganisationUsage.container.tsx`: `stroke='#EFF1F4'` (light grid lines — invisible on dark bg) +- `SingleSDKLabelsChart.tsx`: `fill='#1A2634'` (dark text on dark bg — invisible) +- These colours are not token-driven and do not respond to theme changes + +### 5. Modals and cards same background as panels + +- Both resolve to `#15192b` +- No elevation or shadow difference +- Cards, modals, and panels are visually indistinguishable + +### 6. 50+ orphan hex colours + +- 280 hardcoded hex instances across TSX files +- 52 unique hex values +- Top offenders: + - `#9DA4AE` — 52 occurrences + - `#656D7B` — 44 occurrences + - `#6837FC` — 21 occurrences +- None of these go through tokens or CSS custom properties + +--- + +## Light Mode Audit — All 6 claims VALID + +### 1. All-white surfaces, no depth + +- `$bg-light100: #ffffff` +- `$panel-bg: white` +- `$input-bg: #fff` +- All resolve to the same white — page, panel, and input backgrounds are identical + +### 2. Secondary text below AA + +- `$text-muted: #656d7b` on `#ffffff` background +- Contrast ratio: 4.48:1 +- WCAG AA requires 4.5:1 — **fails by 0.02** + +### 3. Input borders nearly invisible + +- `$input-border-color: $basic-alpha-16: rgba(101, 109, 123, 0.16)` +- 16% opacity on a white background +- Effective rendered colour is barely distinguishable from the background + +### 4. Subtle hover states + +- Many interactive elements use text-colour-only hover changes +- No background colour shift on hover +- Difficult to perceive, especially for users with low vision + +### 5. Focus rings = brand purple or disabled + +- `btn:focus-visible { box-shadow: none }` in `_buttons.scss` +- Focus ring explicitly removed on buttons +- Where present elsewhere, focus uses brand purple (`#6837FC`) which may not have sufficient contrast on all backgrounds + +### 6. Disabled states at 32% opacity + +- `opacity: 0.32` pattern confirmed for disabled elements +- Low opacity on already-low-contrast elements compounds the problem + +--- + +## WCAG / Implementation Claims — VALID + +| Claim | Verified value | Source | +|-------|---------------|--------| +| `.dark` CSS selector rules | 40 rules | SCSS files across `web/styles/` | +| `getDarkMode()` runtime calls | 46 instances | 8 files in `web/components/` | +| `data-bs-theme` attribute underused | Set in `darkMode.ts`, only 2 CSS rules reference it | `_tokens.scss` | +| Hardcoded hex values in TSX | 280 instances, 52 unique | Grep across `web/components/` | +| Icon.tsx hardcoded fill | 46 instances of `fill={fill \|\| '#1A2634'}` | `web/components/Icon.tsx` | + +--- + +## Summary + +Every claim in both audit boards is supported by the codebase. The audits are accurate. + +### One suggested correction + +The Dark Mode Audit board claim #2 says secondary text "barely meets AA". It actually **fails** AA (~4.2:1 vs the 4.5:1 requirement). Consider updating the wording to "fails AA" for consistency with the Light Mode board, which correctly says "below AA". diff --git a/frontend/design-system-issues/LR-1-icon-refactor.md b/frontend/design-system-issues/LR-1-icon-refactor.md new file mode 100644 index 000000000000..e5c8c03f77c5 --- /dev/null +++ b/frontend/design-system-issues/LR-1-icon-refactor.md @@ -0,0 +1,103 @@ +--- +title: "Refactor Icon.tsx: extract inline SVGs, retire IonIcon" +labels: ["design-system", "large-refactor"] +status: DRAFT +--- + +## Problem + +`web/components/Icon.tsx` contains ~60 inline SVG definitions inside a single +`switch` statement, making the file extremely large and impossible to tree-shake. +Three separate icon systems coexist across the codebase: + +1. **Icon.tsx inline SVGs** — 60+ icons defined as `case` branches in a monolithic + switch. 41 of these use `fill={fill || '#1A2634'}`, which renders them + invisible in dark mode. +2. **`web/components/svg/` directory** — 19 standalone SVG components + (`UpgradeIcon.tsx`, `FeaturesIcon.tsx`, `UsersIcon.tsx`, etc.) with their own + hardcoded fills (none use `currentColor`). +3. **IonIcon** — legacy icon set referenced in older components. + +This triple system causes: + +- **No tree-shaking** — importing `` pulls in all 60 SVG + definitions because they live in one switch statement. +- **Inconsistent API** — callers must know whether to use ``, + ``, or an IonIcon class. No single source of truth for available + icons. +- **Dark mode breakage** — 41 icons default to `#1A2634` (near-black), invisible + on the dark background (`#101628`). The `svg/` components also hardcode fills. +- **Maintenance burden** — adding a new icon means editing a 700+ line file and + adding another `case` branch. + +## Files + +- `web/components/Icon.tsx` — monolithic icon switch (~60 cases) +- `web/components/svg/` — 19 standalone SVG components +- All consumers of `` across the codebase (46+ files) + +## Proposed Fix + +### Step 1 — Extract each SVG to its own file + +Create `web/components/icons/` directory. For each `case` in `Icon.tsx`, extract +the SVG markup into a standalone component file: + +``` +web/components/icons/ + ArrowLeftIcon.tsx + ArrowRightIcon.tsx + SearchIcon.tsx + ... + index.ts ← barrel export + IconName type +``` + +Each extracted icon must: +- Accept `fill`, `width`, `height`, and spread `...rest` props +- Default `fill` to `currentColor` (not `#1A2634`) +- Use a consistent `FC>` type signature + +### Step 2 — Migrate `svg/` directory icons into the new system + +Move the 19 components from `web/components/svg/` into `web/components/icons/`, +normalising their API to match the new convention. Replace hardcoded fills with +`currentColor`. + +### Step 3 — Create a new `Icon` wrapper (optional) + +Keep the `` API if desired, but implement it as a lazy +lookup into the barrel export rather than a switch statement. This preserves the +existing call-site API whilst enabling tree-shaking via dynamic imports or a +static map. + +### Step 4 — Deprecate and remove IonIcon references + +Grep for IonIcon usages, replace each with the equivalent new icon component, +then remove the IonIcon dependency. + +### Step 5 — Update all import paths + +Update all consumers to import from `components/icons/` (or continue using the +wrapper `` component if kept). + +## Acceptance Criteria + +- [ ] No inline SVG definitions remain in `Icon.tsx` switch statement +- [ ] Every icon defaults to `fill="currentColor"` — no hardcoded hex fills +- [ ] `web/components/svg/` directory is empty or removed +- [ ] IonIcon is no longer referenced anywhere in the codebase +- [ ] `npm run typecheck` passes with no new errors +- [ ] `npm run build` completes; bundle size does not increase +- [ ] All icons are visible in both light and dark mode (verified in Storybook) +- [ ] Barrel export provides the `IconName` type for type-safe usage + +## Dependencies + +- Blocked by **QW-1** (Icon.tsx `currentColor` quick win) — apply that fix first, + then this refactor builds on it +- Enables better Storybook coverage — individual icon files can each have a story +- Related to **LR-7** (dark mode full pass) — this resolves the icon portion of + dark mode breakage + +--- +Part of the Design System Audit (#6606) diff --git a/frontend/design-system-issues/LR-3-modal-system.md b/frontend/design-system-issues/LR-3-modal-system.md new file mode 100644 index 000000000000..567f055e3981 --- /dev/null +++ b/frontend/design-system-issues/LR-3-modal-system.md @@ -0,0 +1,144 @@ +--- +title: "Migrate modal system from global imperative API to React context" +labels: ["design-system", "large-refactor"] +status: DRAFT +--- + +## Problem + +The modal system relies on a deprecated React 16-era imperative API that +attaches `openModal`, `openModal2`, `openConfirm`, and `closeModal` to the +`global`/`window` object. This is used across 46+ files. + +### Specific issues + +1. **Deprecated `ReactDOM.render` usage** — `web/components/modals/base/Modal.tsx` + calls `render()` and `unmountComponentAtNode()` from `react-dom`, which are + removed in React 18. This is the primary blocker for a React 18 upgrade. + +2. **`openModal2` anti-pattern** — a second modal API was added alongside the + original `openModal` to work around limitations, resulting in two competing + global functions for the same task. + +3. **14 near-identical confirmation modals** — each implements its own layout, + button arrangement, and copy: + - `ConfirmRemoveFeature.tsx` + - `ConfirmRemoveSegment.tsx` + - `ConfirmRemoveProject.tsx` + - `ConfirmRemoveOrganisation.tsx` + - `ConfirmRemoveEnvironment.tsx` + - `ConfirmRemoveWebhook.tsx` + - `ConfirmRemoveAuditWebhook.tsx` + - `ConfirmRemoveTrait.tsx` + - `ConfirmDeleteAccount.tsx` + - `ConfirmDeleteRole.tsx` + - `ConfirmToggleFeature.tsx` + - `ConfirmToggleEnvFeature.tsx` + - `ConfirmHideFlags.tsx` + - `ConfirmCloneSegment.tsx` + + These could be a single `ConfirmModal` component with configurable title, + message, and action props. + +4. **Legacy JS modals** — `CreateProject.js`, `CreateWebhook.js`, and + `Payment.js` are class components in plain JavaScript, using `propTypes` and + relying on the global registry. + +5. **No context for modal state** — because modals are rendered imperatively into + a detached DOM node, they cannot access React context (e.g. Redux store) + without the `` wrapper that `Modal.tsx` manually injects. + +## Files + +- `web/components/modals/base/Modal.tsx` — imperative `openModal`/`closeModal` + implementation using `ReactDOM.render` +- `web/components/modals/base/ModalDefault.tsx` — default modal wrapper +- `web/components/modals/base/ModalConfirm.tsx` — confirmation modal base +- `web/components/modals/Confirm*.tsx` — 14 confirmation modal variants +- `web/components/modals/CreateProject.js` — legacy JS class component +- `web/components/modals/CreateWebhook.js` — legacy JS class component (duplicate + of `CreateWebhook.tsx`) +- `web/components/modals/Payment.js` — legacy JS class component +- 46+ files across `web/components/` that call `openModal` or `openModal2` + +## Proposed Fix + +### Step 1 — Create ModalProvider context + +```tsx +// web/components/modals/ModalProvider.tsx +const ModalContext = createContext(null) + +export const ModalProvider: FC = ({ children }) => { + const [modals, setModals] = useState([]) + // open, close, confirm methods + return ( + + {children} + {modals.map(modal => )} + + ) +} + +export const useModal = () => useContext(ModalContext) +``` + +### Step 2 — Consolidate confirmation modals + +Create a single `ConfirmModal` component that accepts: +- `title: string` +- `message: ReactNode` +- `confirmText?: string` (default: "Confirm") +- `cancelText?: string` (default: "Cancel") +- `onConfirm: () => void | Promise` +- `variant?: 'danger' | 'warning' | 'default'` + +Replace all 14 `Confirm*.tsx` files with call sites that use: +```tsx +const { confirm } = useModal() +confirm({ + title: 'Remove Feature', + message: `Are you sure you want to remove "${name}"?`, + variant: 'danger', + onConfirm: handleDelete, +}) +``` + +### Step 3 — Migrate call sites from global to hook + +For each of the 46+ files that call `openModal`/`openModal2`: +1. Import `useModal` +2. Replace `openModal(...)` with `modal.open(...)` +3. Remove any `global.openModal` references + +### Step 4 — Remove legacy imperative API + +Once all call sites are migrated: +1. Remove `ReactDOM.render`/`unmountComponentAtNode` calls from `Modal.tsx` +2. Remove `openModal`, `openModal2`, `closeModal` from `global`/`window` +3. Delete the legacy JS modal files (`CreateProject.js`, `CreateWebhook.js`, + `Payment.js`) + +## Acceptance Criteria + +- [ ] `ModalProvider` context exists and is mounted at the app root +- [ ] A single `ConfirmModal` component replaces all 14 confirmation variants +- [ ] No `openModal`, `openModal2`, or `closeModal` on `global`/`window` +- [ ] No `ReactDOM.render` or `unmountComponentAtNode` calls remain in modal code +- [ ] All modals have access to React context (Redux store, theme) without + manual `` wrapping +- [ ] `CreateProject.js`, `CreateWebhook.js`, and `Payment.js` are deleted or + converted to TypeScript +- [ ] `npm run typecheck` and `npm run build` pass +- [ ] E2E tests for modal flows (create feature, delete segment, etc.) pass + +## Dependencies + +- Related to **LR-9** (Remove global component registry) — that issue covers the + broader `window.*` pattern; this issue focuses specifically on the modal globals +- Related to **LR-5** (JS to TypeScript) — the 3 legacy JS modals should be + converted as part of this work +- Prerequisite for React 18 upgrade — `ReactDOM.render` must be removed first + +--- +Part of the Design System Audit (#6606) diff --git a/frontend/design-system-issues/LR-5-js-to-typescript.md b/frontend/design-system-issues/LR-5-js-to-typescript.md new file mode 100644 index 000000000000..9706bff457c6 --- /dev/null +++ b/frontend/design-system-issues/LR-5-js-to-typescript.md @@ -0,0 +1,119 @@ +--- +title: "Convert remaining .js components to TypeScript" +labels: ["design-system", "large-refactor", "tech-debt"] +status: DRAFT +--- + +## Problem + +50 `.js` files remain in `web/components/` alongside their `.tsx` counterparts. +Many are class components using `propTypes`, `module.exports`, and the global +component registry (`window.Button`, `window.Row`, etc.). + +### Specific issues + +1. **3 confirmed duplicate coexisting files** — both a `.js` and `.tsx` version + exist for the same component, creating ambiguity about which is actually used: + - `web/components/modals/CreateWebhook.js` + `CreateWebhook.tsx` + - `web/components/ErrorMessage.js` + `messages/ErrorMessage.tsx` + - `web/components/SuccessMessage.js` + `messages/SuccessMessage.tsx` + +2. **Class components with propTypes** — files like `Flex.js`, `Column.js`, + `Input.js`, `InputGroup.js`, `Tabs.js`, `Payment.js`, `CreateProject.js` use + the class component pattern with `propTypes` instead of TypeScript interfaces. + +3. **`module.exports` usage** — older files use CommonJS exports rather than ES + module syntax, preventing proper static analysis and tree-shaking. + +4. **Global registry dependency** — many `.js` files rely on `window.Button`, + `window.Row`, etc. from `project-components.js` instead of explicit imports + (see LR-9). These cannot be converted to `.tsx` without first adding imports. + +### Full list of .js files in web/components/ + +**Top level (30 files):** +`AdminAPIKeys.js`, `App.js`, `AsideProjectButton.js`, `AsideTitleLink.js`, +`Blocked.js`, `CodeHelp.js`, `Collapsible.js`, `CompareEnvironments.js`, +`CompareFeatures.js`, `ErrorMessage.js`, `FlagOwnerGroups.js`, `FlagOwners.js`, +`FlagSelect.js`, `Headway.js`, `Highlight.js`, `HistoryIcon.js`, +`Maintenance.js`, `OrganisationSelect.js`, `Paging.js`, +`PasswordRequirements.js`, `ProjectSelect.js`, `RebrandBanner.js`, +`SamlForm.js`, `SegmentOverrides.js`, `ServerSideSDKKeys.js`, +`SuccessMessage.js`, `Token.js`, `TryIt.js`, `TwoFactor.js`, `ValueEditor.js` + +**Base components (5 files):** +`base/forms/Input.js`, `base/forms/InputGroup.js`, `base/forms/Tabs.js`, +`base/grid/Column.js`, `base/grid/Flex.js` + +**Modals (5 files):** +`modals/CreateProject.js`, `modals/CreateWebhook.js`, `modals/Payment.js`, +`modals/create-experiment/index.js`, `modals/create-feature/index.js` + +**Pages (7 files):** +`pages/AccountSettingsPage.js`, `pages/ComingSoonPage.js`, `pages/ComparePage.js`, +`pages/ConfirmEmailPage.js`, `pages/InvitePage.js`, `pages/NotFoundErrorPage.js`, +`pages/PasswordResetPage.js`, `pages/UserIdPage.js` + +**Other (3 files):** +`SimpleTwoFactor/index.js`, `SimpleTwoFactor/prompt.js` + +## Files + +- All 50 `.js` files listed above in `web/components/` +- `web/project/project-components.js` — global registry (see LR-9) + +## Proposed Fix + +### Phase 1 — Delete confirmed duplicates + +Delete the 3 `.js` files that have `.tsx` replacements already in use: +- Delete `web/components/modals/CreateWebhook.js` (keep `CreateWebhook.tsx`) +- Delete `web/components/ErrorMessage.js` (keep `messages/ErrorMessage.tsx`) +- Delete `web/components/SuccessMessage.js` (keep `messages/SuccessMessage.tsx`) + +Verify no import path resolves to the deleted `.js` file. + +### Phase 2 — Convert simple functional components + +Start with files that are already functional components or trivial class +components. For each file: +1. Rename `.js` → `.tsx` +2. Replace `propTypes` with a TypeScript `interface` or `type` +3. Replace `module.exports` with `export default` +4. Add explicit imports for any globally-registered components +5. Run `npx eslint --fix ` and `npm run typecheck` + +### Phase 3 — Convert class components + +For files using `class extends Component`: +1. Convert to functional component with hooks where straightforward +2. If the class component has complex lifecycle methods, convert to hooks + (`useEffect`, `useCallback`, etc.) +3. Add proper TypeScript interfaces for props and state + +### Phase 4 — Convert page and modal components + +These tend to be larger and more complex. Convert after the simpler components +are done, as patterns established in Phase 2-3 can be reused. + +## Acceptance Criteria + +- [ ] Zero `.js` files remain in `web/components/` +- [ ] No `propTypes` imports remain in converted files +- [ ] No `module.exports` usage remains in converted files +- [ ] All converted files have TypeScript interfaces for their props +- [ ] `npm run typecheck` passes with no new errors +- [ ] `npm run build` completes successfully +- [ ] No duplicate `.js` + `.tsx` files exist for the same component + +## Dependencies + +- **LR-9** (Remove global component registry) should be completed first — without + it, `.js` files that use `window.Button` etc. cannot be converted because + TypeScript will flag the missing imports +- Related to **LR-3** (Modal system migration) — the 3 legacy JS modals + (`CreateProject.js`, `CreateWebhook.js`, `Payment.js`) will be addressed there +- Enables better `npm run typecheck` coverage across the codebase + +--- +Part of the Design System Audit (#6606) diff --git a/frontend/design-system-issues/LR-6-colour-primitives.md b/frontend/design-system-issues/LR-6-colour-primitives.md new file mode 100644 index 000000000000..7a76fba3104c --- /dev/null +++ b/frontend/design-system-issues/LR-6-colour-primitives.md @@ -0,0 +1,147 @@ +--- +title: "Define formal colour primitive palette" +labels: ["design-system", "large-refactor", "tokens"] +status: DRAFT +--- + +## Problem + +The codebase has no systematic colour palette. Greys are defined ad hoc in +`_variables.scss` and hardcoded directly in components. There is no `$neutral-*` +or `$purple-*` scale that other tokens can reference. + +### Specific issues + +1. **Ad hoc grey definitions** — `_variables.scss` defines greys as one-off + variables with no scale relationship: + - `$text-icon-grey: #656d7b` + - `$text-icon-light-grey: rgba(157, 164, 174, 1)` (equivalent to `#9DA4AE`) + - `$body-color: #1a2634` + - `$header-color: #1e0d26` + - `$bg-dark500: #101628` through `$bg-dark100: #2d3443` + +2. **Highest-frequency hardcoded values** — these hex values appear dozens of + times across the codebase with no token: + - `#9DA4AE` — 59 occurrences across 35 files (light grey text/icons) + - `#656D7B` — 75 occurrences across 48 files (secondary text) + - `#1A2634` — used as body colour and icon default fill + +3. **Brand purple has no scale** — `_variables.scss` defines: + - `$primary: #6837fc` + - `$primary400: #906af6` + - `$primary600: #4e25db` + - `$primary700: #3919b7` + - `$primary800: #2a2054` + - `$primary900: #1E0D26` + + But these don't follow a consistent naming convention and `$primary900` + (`#1E0D26`) is actually a near-black, not a dark purple — it's the same + value as `$header-color`. + +4. **`_primitives.scss` exists but is incomplete** — the file was drafted with + `$slate-*` and `$purple-*` scales that `_tokens.scss` already references, but + the primitives are not yet used to replace ad hoc values in `_variables.scss` + or in component code. + +5. **Background dark scale is informal** — `$bg-dark500` through `$bg-dark100` + use a loose numbering system that doesn't align with the standard + 50/100/200/.../950 convention. + +## Files + +- `web/styles/_variables.scss` — current ad hoc colour definitions (~60 lines of + colour variables) +- `web/styles/_primitives.scss` — drafted primitive palette (incomplete) +- `web/styles/_tokens.scss` — semantic tokens that reference primitives + +## Proposed Fix + +### Step 1 — Define the neutral (slate) scale + +Create a complete `$slate-*` scale in `_primitives.scss`: + +```scss +// Neutral / Slate +$slate-0: #ffffff; +$slate-50: #fafafb; +$slate-100: #eff1f4; +$slate-200: #e0e3e9; +$slate-300: #9da4ae; // maps to existing #9DA4AE (59 uses) +$slate-400: #8a919b; +$slate-500: #656d7b; // maps to existing #656D7B (75 uses) +$slate-600: #1a2634; // maps to existing $body-color +$slate-800: #2d3443; // maps to existing $bg-dark100 +$slate-850: #202839; // maps to existing $bg-dark200 +$slate-900: #15192b; // maps to existing $bg-dark400 +$slate-950: #101628; // maps to existing $bg-dark500 +``` + +(Exact values to be confirmed by visual comparison; the above maps known hex +values to the nearest scale step.) + +### Step 2 — Define the purple (brand) scale + +```scss +// Brand / Purple +$purple-400: #906af6; // maps to existing $primary400 +$purple-600: #6837fc; // maps to existing $primary +$purple-700: #4e25db; // maps to existing $primary600 +$purple-800: #3919b7; // maps to existing $primary700 +``` + +Remove the misleading `$primary900: #1E0D26` (it's a neutral, not a purple). + +### Step 3 — Define feedback scales + +```scss +// Feedback +$red-500: #ef4d56; // maps to existing $danger +$red-400: #f57c78; // maps to existing $danger400 +$green-500: #27ab95; // maps to existing $success +$green-400: #56ccad; // maps to existing $success400 +$green-600: #13787b; // maps to existing $success600 +$orange-500: #ff9f43; // maps to existing $warning +$blue-500: #0aaddf; // maps to existing $info +``` + +### Step 4 — Map existing variables to primitives + +Update `_variables.scss` to reference primitives instead of hardcoded hex: + +```scss +// Before +$body-color: #1a2634; +$text-icon-grey: #656d7b; + +// After +$body-color: $slate-600; +$text-icon-grey: $slate-500; +``` + +### Step 5 — Document the mapping + +Add a comment block at the top of `_primitives.scss` listing every old hex value +and its new primitive equivalent, so reviewers can verify the mapping is correct. + +## Acceptance Criteria + +- [ ] `_primitives.scss` defines complete `$slate-0` through `$slate-950` scale +- [ ] `_primitives.scss` defines `$purple-*` scale for brand colours +- [ ] `_primitives.scss` defines `$red-*`, `$green-*`, `$orange-*`, `$blue-*` + for feedback colours +- [ ] `_variables.scss` references primitives instead of hardcoded hex values +- [ ] `$primary900: #1E0D26` is removed or remapped to a neutral +- [ ] `_tokens.scss` compiles without errors using the new primitive references +- [ ] `npm run build` passes with no visual changes in light or dark mode +- [ ] Mapping documentation exists in `_primitives.scss` comments + +## Dependencies + +- This is a **prerequisite for LR-2** (Semantic colour tokens) — the semantic + layer references primitives +- Enables **LR-7** (Full dark mode pass) — primitives make it straightforward to + define dark overrides +- No blocking dependencies — this can start immediately + +--- +Part of the Design System Audit (#6606) diff --git a/frontend/design-system-issues/LR-7-dark-mode-full-pass.md b/frontend/design-system-issues/LR-7-dark-mode-full-pass.md new file mode 100644 index 000000000000..b25cb8cb04db --- /dev/null +++ b/frontend/design-system-issues/LR-7-dark-mode-full-pass.md @@ -0,0 +1,135 @@ +--- +title: "Full dark mode audit pass across all SCSS files" +labels: ["design-system", "large-refactor", "dark-mode"] +status: DRAFT +--- + +## Problem + +Dark mode is implemented through three parallel mechanisms that overlap +inconsistently, leaving large gaps in coverage. + +### Three parallel dark mode mechanisms + +1. **`.dark` CSS class on ``** — toggled by `web/project/darkMode.ts` via + `document.body.classList.add('dark')`. Used by 41 `.dark {}` blocks across 29 + SCSS files. This is the primary mechanism for component-level dark overrides. + +2. **`data-bs-theme="dark"` attribute on ``** — also set by `darkMode.ts` + via `document.documentElement.setAttribute('data-bs-theme', 'dark')`. Despite + being set, only `_tokens.scss` and Bootstrap's own variables respond to it. + Almost no custom SCSS uses this selector. + +3. **`getDarkMode()` runtime checks in JavaScript** — 10 files call + `getDarkMode()` from `web/project/darkMode.ts` to conditionally apply inline + styles or class names at render time: + - `web/components/tags/Tag.tsx` + - `web/components/CompareIdentities.tsx` + - `web/components/CompareEnvironments.js` + - `web/components/segments/Rule/components/RuleConditionValueInput.tsx` + - `web/components/base/select/multi-select/InlineMultiValue.tsx` + - `web/components/base/select/multi-select/CustomOption.tsx` + - `web/components/feature-page/FeatureNavTab/CodeReferences/components/RepoSectionHeader.tsx` + - `web/components/DarkModeSwitch.tsx` + + This approach re-renders components when dark mode changes only if the + component re-mounts, creating flash-of-wrong-theme bugs. + +### Coverage gaps + +- **41 `.dark` blocks exist across 29 files** — but the codebase has 60+ SCSS + files, meaning roughly half have no dark overrides at all. +- **Toast** — zero dark mode styles (`_toast.scss` has no `.dark` block) +- **Buttons** — `btn-tertiary`, `btn-danger`, `btn--transparent` have no dark + overrides. `btn--outline` hardcodes `background: white`. +- **Forms** — `input:read-only` hardcodes `#777` with no dark override. Textarea + border uses `border-color: $input-bg-dark` making it invisible. Checkbox + focus/hover/disabled states missing in dark. +- **Icons** — 41 icons default to `fill: #1A2634`, invisible on dark background + `#101628`. + +## Files + +- `web/project/darkMode.ts` — dark mode toggle implementation (sets both `.dark` + class and `data-bs-theme` attribute) +- `web/styles/_tokens.scss` — semantic tokens with `.dark` overrides (drafted) +- `web/styles/_variables.scss` — colour variables including dark variants +- All SCSS files under `web/styles/` (60+ files) +- 10 TSX/JS files that call `getDarkMode()` + +## Proposed Fix + +### Step 1 — Unify on a single mechanism + +Choose CSS custom properties + `.dark` class as the single mechanism. +`data-bs-theme` should be kept only for Bootstrap compatibility but not used in +custom SCSS. `getDarkMode()` runtime checks should be eliminated. + +### Step 2 — Audit every SCSS file for dark coverage + +For each SCSS file under `web/styles/`: +1. Check if it defines colours, backgrounds, borders, or shadows +2. If yes, verify a `.dark` override exists +3. If no `.dark` override, add one using semantic tokens from `_tokens.scss` + +Priority order (by user impact): +1. `_toast.scss` — no dark styles at all +2. `_buttons.scss` — tertiary, danger, transparent variants +3. `_input.scss` — read-only, focus ring, textarea border +4. `_switch.scss` — focus/hover/disabled states +5. `_chip.scss` — inconsistent dark handling +6. Remaining files + +### Step 3 — Replace getDarkMode() with CSS tokens + +For each of the 10 files calling `getDarkMode()`: +1. Replace the conditional inline style with a CSS custom property reference +2. If the style is too complex for a single token, create a component-scoped + CSS class with `.dark` override + +Example migration: +```tsx +// Before +const color = getDarkMode() ? '#e1e1e1' : '#656d7b' +... + +// After +... +``` + +### Step 4 — Remove getDarkMode() export + +Once all call sites are migrated, remove `getDarkMode()` from `darkMode.ts`. +Keep only `setDarkMode()` for the toggle switch. + +### Step 5 — Validate with Storybook + +Add a dark mode decorator to Storybook that toggles the `.dark` class on the +preview body. Verify every existing story renders correctly in both themes. + +## Acceptance Criteria + +- [ ] Every SCSS file that defines colour/background/border values has a + corresponding `.dark` override +- [ ] Zero `getDarkMode()` calls remain in component code (only `setDarkMode()` + in `DarkModeSwitch.tsx` and `darkMode.ts`) +- [ ] Toast, buttons (tertiary/danger/transparent), and form inputs all have + complete dark mode styles +- [ ] `data-bs-theme` attribute is only used for Bootstrap variable overrides, + not in custom SCSS selectors +- [ ] No hardcoded light-only colours (`#1A2634`, `white`, `#777`) appear without + a `.dark` counterpart +- [ ] `npm run build` passes; no visual regressions in light mode +- [ ] Storybook dark mode decorator works for all existing stories + +## Dependencies + +- **LR-6** (Colour primitives) and **LR-2** (Semantic tokens) should be completed + first — dark overrides are much simpler when tokens exist +- **QW-1** (Icon currentColor fix) should be applied first — it resolves the icon + portion of dark mode independently +- Enables better accessibility scores — several WCAG AA contrast failures are + dark-mode-specific + +--- +Part of the Design System Audit (#6606) diff --git a/frontend/design-system-issues/LR-8-typography-tokens.md b/frontend/design-system-issues/LR-8-typography-tokens.md new file mode 100644 index 000000000000..ba5a08090ee9 --- /dev/null +++ b/frontend/design-system-issues/LR-8-typography-tokens.md @@ -0,0 +1,156 @@ +--- +title: "Define typography tokens and standardise type scale" +labels: ["design-system", "large-refactor", "tokens"] +status: DRAFT +--- + +## Problem + +The codebase lacks formal typography tokens. Font weights, sizes, and +line-heights are hardcoded inline or scattered across SCSS files with no +centralised scale. + +### Specific issues + +1. **No `$font-weight-*` tokens** — there is no `$font-weight-semibold`, + `$font-weight-bold`, or `$font-weight-regular` variable. Instead, + `fontWeight: 600` is hardcoded in multiple files: + - `web/components/navigation/AccountDropdown.tsx` + - `web/components/pages/admin-dashboard/components/ReleasePipelineStatsTable.tsx` + - `web/components/pages/admin-dashboard/components/OrganisationUsageTable.tsx` + +2. **Invalid CSS value** — `web/components/messages/SuccessMessage.tsx:44` + contains `fontWeight: 'semi-bold'` which is not a valid CSS value. Browsers + silently ignore it, so the intended semibold weight is never applied. + +3. **50+ inline style overrides** — the admin dashboard pages alone contain 50+ + instances of inline `style={{ fontWeight: ..., fontSize: ... }}` that bypass + the SCSS type scale entirely. + +4. **h1-h6 scale exists but is bypassed** — `web/styles/project/_type.scss` + defines heading styles, but components frequently override them with inline + styles or ad hoc classes. + +5. **Off-scale line-heights** — at least 4 line-height values in use that don't + align with the base grid (`1.3`, `1.15`, `18px`, `22px`). + +6. **No responsive typography** — font sizes are fixed pixel values with no + fluid scaling or breakpoint adjustments. + +## Files + +- `web/styles/_variables.scss` — where `$font-weight-*` tokens should be defined +- `web/styles/_tokens.scss` — where CSS custom property typography tokens should + live +- `web/styles/project/_type.scss` — existing heading and body type scale +- `web/components/navigation/AccountDropdown.tsx` — hardcoded `fontWeight: 600` +- `web/components/pages/admin-dashboard/components/ReleasePipelineStatsTable.tsx` + — hardcoded `fontWeight: 600` +- `web/components/pages/admin-dashboard/components/OrganisationUsageTable.tsx` — + hardcoded `fontWeight: 600` +- `web/components/messages/SuccessMessage.tsx` — invalid `fontWeight: 'semi-bold'` + +## Proposed Fix + +### Step 1 — Define font weight tokens + +Add to `_variables.scss`: + +```scss +$font-weight-regular: 400; +$font-weight-medium: 500; +$font-weight-semibold: 600; +$font-weight-bold: 700; +``` + +### Step 2 — Define CSS custom property typography tokens + +Add to `_tokens.scss`: + +```scss +:root { + // Font weights + --font-weight-regular: #{$font-weight-regular}; + --font-weight-medium: #{$font-weight-medium}; + --font-weight-semibold: #{$font-weight-semibold}; + --font-weight-bold: #{$font-weight-bold}; + + // Font sizes + --font-size-xs: 0.75rem; // 12px + --font-size-sm: 0.8125rem; // 13px + --font-size-base: 0.875rem; // 14px + --font-size-md: 1rem; // 16px + --font-size-lg: 1.125rem; // 18px + --font-size-xl: 1.25rem; // 20px + --font-size-2xl: 1.5rem; // 24px + --font-size-3xl: 2rem; // 32px + + // Line heights + --line-height-tight: 1.25; + --line-height-normal: 1.5; + --line-height-loose: 1.75; +} +``` + +### Step 3 — Add TypeScript exports + +Add to `common/theme/tokens.ts`: + +```ts +export const fontWeightRegular = 'var(--font-weight-regular, 400)' +export const fontWeightMedium = 'var(--font-weight-medium, 500)' +export const fontWeightSemibold = 'var(--font-weight-semibold, 600)' +export const fontWeightBold = 'var(--font-weight-bold, 700)' +``` + +### Step 4 — Replace hardcoded values + +For each file with `fontWeight: 600`: +```tsx +// Before +style={{ fontWeight: 600 }} + +// After +import { fontWeightSemibold } from 'common/theme' +style={{ fontWeight: fontWeightSemibold }} +``` + +Or preferably, use a CSS class: +```scss +.text-semibold { font-weight: var(--font-weight-semibold); } +``` + +### Step 5 — Fix the invalid CSS value + +In `SuccessMessage.tsx:44`, change `fontWeight: 'semi-bold'` to +`fontWeight: 'var(--font-weight-semibold, 600)'` (or use a CSS class). + +### Step 6 — Standardise line-heights + +Replace off-scale line-height values (`1.3`, `1.15`, `18px`, `22px`) with the +nearest token from the defined scale. + +## Acceptance Criteria + +- [ ] `$font-weight-regular`, `$font-weight-medium`, `$font-weight-semibold`, + and `$font-weight-bold` are defined in `_variables.scss` +- [ ] CSS custom properties for font weights, sizes, and line-heights are defined + in `_tokens.scss` +- [ ] TypeScript exports exist in `common/theme/tokens.ts` +- [ ] Zero hardcoded `fontWeight: 600` or `fontWeight: 'bold'` in TSX files — + all use tokens +- [ ] `fontWeight: 'semi-bold'` bug in `SuccessMessage.tsx` is fixed +- [ ] Off-scale line-height values are replaced with token references +- [ ] `npm run typecheck` and `npm run build` pass +- [ ] No visual regressions — typography looks identical before and after + +## Dependencies + +- Related to **LR-2** (Semantic colour tokens) — follows the same 3-layer token + architecture pattern +- Related to **LR-6** (Colour primitives) — the primitive layer concept applies + to typography as well +- Can proceed independently — no hard blockers + +--- +Part of the Design System Audit (#6606) diff --git a/frontend/design-system-issues/LR-9-remove-global-component-registry.md b/frontend/design-system-issues/LR-9-remove-global-component-registry.md new file mode 100644 index 000000000000..31b220a82312 --- /dev/null +++ b/frontend/design-system-issues/LR-9-remove-global-component-registry.md @@ -0,0 +1,119 @@ +--- +title: "Remove global component registry in project-components.js" +labels: ["design-system", "large-refactor", "tech-debt"] +status: DRAFT +--- + +## Problem + +`web/project/project-components.js` attaches ~15 components and providers to +`window` (and `global`), making them available implicitly across the entire +codebase without any import statement: + +```js +window.Button = Button +window.Row = Row +window.Select = Select +window.Input = Input +window.InputGroup = InputGroup +window.FormGroup = FormGroup +window.Panel = Panel +window.PanelSearch = PanelSearch +window.CodeHelp = CodeHelp +window.Loader = class extends PureComponent { ... } // defined inline +window.Tooltip = Tooltip +global.ToggleChip = ToggleChip +global.Select = class extends PureComponent { ... } // wrapper defined inline +``` + +This was a common pre-module-era shortcut, but it causes several concrete +problems today: + +- **TypeScript blindness** — the TS compiler has no knowledge of `window.Button` + so any `.js` file that uses it cannot be migrated to `.tsx` without first + adding an explicit import. This is the root blocker for completing LR-5. +- **ESLint no-undef suppression** — usages like bare `Button` or `Row` in `.js` + files would trigger `no-undef` if the global polyfill weren't there. This + masks real missing-import bugs (e.g. `Button` in `SuccessMessage.js`). +- **Tree-shaking defeated** — bundlers cannot eliminate unused globals. Every + component in `project-components.js` ships in every bundle regardless of + whether it's used on that page. +- **Storybook incompatibility** — the global registry is not set up in the + Storybook environment, so `.js` components that rely on `window.Button` etc. + cannot be rendered in isolation without special mocking. +- **Hidden coupling** — nothing in a consuming file signals its dependency on + `project-components.js` being loaded first. This makes the dependency graph + invisible to tooling and to new contributors. + +## Files + +- `web/project/project-components.js` — the registry itself +- All `.js` components that use globals without importing them (see LR-5 for + the list; examples include `SuccessMessage.js`, `CodeHelp.js`, `Payment.js`) + +## Proposed Fix + +### Step 1 — Audit all global usages + +```bash +grep -rn "window\.Button\|window\.Row\|window\.Select\|window\.Input\|window\.Panel\|window\.Loader\|window\.Tooltip\|window\.Paging\|window\.FormGroup\|window\.CodeHelp\|window\.PanelSearch\|global\.ToggleChip\|global\.Select" web/ common/ --include='*.js' --include='*.ts' --include='*.tsx' +``` + +Also grep for bare usages (no `window.` prefix) in `.js` files: + +```bash +grep -rn "\bButton\b\|\bRow\b\|\bSelect\b\|\bLoader\b" web/components --include='*.js' +``` + +### Step 2 — Add explicit imports to each consuming file + +For each `.js` file that uses a global component, add the correct import. +Example for `SuccessMessage.js`: + +```js +// Before — relies on window.Button +
`, ``, ``, ``, ``, `` with shared empty state and loading skeleton slots. -**Fix**: Introduce CSS custom properties via `common/theme/tokens.ts` + `web/styles/_tokens.scss` (already drafted on this branch). These tokens: -- Define light values on `:root` and dark values on `[data-bs-theme='dark']` -- Are importable in TS files: `import { colorTextStandard } from 'common/theme'` -- Eliminate `getDarkMode()` calls entirely -- Gradually replace `.dark` selector pairs +Not a near-term priority. Documented here for when the team is ready to standardise table/list patterns. When started, build one reference implementation first and migrate incrementally. -**Migration path**: -1. Token files already exist (drafted) -2. Fix Icon.tsx with `currentColor` (QW-1) — no tokens needed -3. Migrate `getDarkMode()` callsites (13 files) to use token imports -4. Migrate `.dark` selectors component-by-component -5. Remove orphaned `$*-dark` SCSS variables +### NP-5: TypeScript-first components -**Validate**: Storybook → "Design System/Colours/Semantic Tokens" (toggle dark mode to see tokens flip). Also see "Design System/Dark Mode Issues/Theme Token Comparison" for before/after code examples. +**Current state**: 7 components remain as `.js` class components (`Input.js`, `InputGroup.js`, `CreateProject.js`, `CreateWebhook.js`, `Payment.js`, `Flex.js`, `Column.js`). ---- +**New pattern**: All components in TypeScript with full prop types. No new `.js` files. -### LR-3: Modal system — migrate from global imperative API +**Adoption strategy**: +1. Delete `CreateWebhook.js` (`.tsx` version already exists) +2. When modifying a `.js` component, convert it to `.tsx` as part of the PR +3. Prioritise `Input.js` (ME-3) since it's a widely-used form primitive -**Problem**: The modal system uses `openModal()`, `openModal2()`, `openConfirm()` as global functions attached to `window`. It uses deprecated `react-dom` `render`/`unmountComponentAtNode` APIs (removed in React 18). `openModal2` exists for stacking modals, acknowledged in code comments as a pattern to avoid. +**See also**: [#5746](https://github.com/Flagsmith/flagsmith/issues/5746) (TS migration epic) — aligns with this pattern. `Payment.js` already tracked as [#6319](https://github.com/Flagsmith/flagsmith/issues/6319). -**Fix**: Migrate to a React context-based modal manager. Replace global imperative calls with `useModal()` hook. This is a large effort but unblocks React 18 compatibility. +### NP-6: Formal colour primitive palette 🔄 In progress ---- +**Current state**: Ad hoc grey definitions, inverted tonal scales, alpha variants using different RGB than solid counterparts, 30+ orphan hex values. -### LR-4: Standardise table/list components +**New pattern**: `_primitives.scss` with `$slate-*`, `$purple-*`, `$red-*`, `$green-*`, `$orange-*`, `$blue-*` scales following the lower-number-is-lighter convention. Already drafted on `chore/design-system-tokens`. -**Problem**: No unified table/list component system. Each feature area builds its own: -- 9 different `TableFilter*` components -- 5+ different `*Row` components (FeatureRow, ProjectFeatureRow, FeatureOverrideRow, OrganisationUsersTableRow, etc.) -- 5+ different `*List` components +**Adoption strategy**: +1. Ship `_primitives.scss` with the complete scale +2. Map existing `_variables.scss` definitions to reference primitives +3. When touching a file with orphan hex values, replace with the nearest primitive +4. Enables NP-2 (semantic tokens reference primitives) -**Fix**: Create a composable `
` / `` component system with standardised `Row`, `Cell`, and `Header` sub-components. Migrate feature areas one at a time. +### NP-7: Fix dark mode gaps ---- - -### LR-5: Remove legacy JS class components - -**Problem**: Several components remain as `.js` class components: -- `base/forms/Input.js` -- `base/forms/InputGroup.js` -- `modals/CreateProject.js` -- `modals/CreateWebhook.js` (coexists with `.tsx` version) -- `modals/Payment.js` -- Layout: `Flex.js`, `Column.js` +**Current state**: Only 48 `.dark` CSS selectors across the entire stylesheet. Large areas have zero dark mode support: feature pipeline visualisation, admin dashboard charts, integration cards, and dozens of components with inline light-mode-only hex values. The QW dark mode fixes (QW-1, QW-3, QW-12, QW-13, QW-14) cover specific components but leave most of the surface area untouched. -**Fix**: Convert each to TypeScript functional components. Remove `CreateWebhook.js` duplicate. +**Work required**: After NP-2 (semantic tokens) lands, go feature-by-feature and replace hardcoded colour values with tokens: +- 48 `.dark` SCSS selector pairs → replace with token usage +- 13 `getDarkMode()` call sites → replace with token imports +- 280+ hardcoded hex values in TSX → replace with tokens +- Feature pipeline, integration cards, admin dashboard — full dark mode pass ---- - -### LR-6: Define a formal colour palette +**Adoption strategy**: +1. QW dark mode fixes land first (specific, high-impact components) +2. NP-2 (semantic tokens) lands — the replacement vocabulary exists +3. Go page by page: Features, Segments, Audit Log, Integrations, Users & Permissions, Organisation Settings, Identities, Release Pipelines, Change Requests +4. Each page becomes a sub-task — replace hardcoded values with tokens, verify in both themes -**Problem**: There is no formal, systematic colour palette. The current state: +**Blocked by**: NP-2 (semantic colour tokens) — tokens must exist before we can use them -1. **Inconsistent tonal scales**: `$primary400` (`#956CFF`) is *lighter* than `$primary` (`#6837FC`), reversing the typical convention where higher numbers = darker. `$bg-dark500` is darker than `$bg-dark100`, but text/grey scales don't follow a pattern at all. +### NP-8: Typography consistency — deferred to Tailwind adoption -2. **Alpha colour RGB mismatches**: The alpha variants use different base RGB values than their solid counterparts: - | Token | Solid hex | Solid RGB | Alpha base RGB | - |-------|-----------|-----------|----------------| - | `$primary` / `$primary-alfa-*` | `#6837FC` | `(104, 55, 252)` | `(149, 108, 255)` | - | `$danger` / `$danger-alfa-*` | `#EF4D56` | `(239, 77, 86)` | `(255, 66, 75)` | - | `$warning` / `$warning-alfa-*` | `#FF9F43` | `(255, 159, 67)` | `(255, 159, 0)` | +**Current state**: Type scale exists in SCSS but is bypassed in 58+ places (13px × 17, 12px × 12, 11px × 7). Fragmented weight system (`.font-weight-medium` vs Bootstrap `fw-*`). 9 inline `fontWeight` values. Opacity-based muted text failing WCAG contrast. -3. **30+ orphan hex values in components**: Colours used directly in TSX/SCSS that don't exist in `_variables.scss`: - - `#9DA4AE` (52 uses) — a grey with no variable - - `#656D7B` (44 uses) — has variable `$text-icon-grey` but most callsites use raw hex - - `#e74c3c`, `#53af41`, `#767d85`, `#5D6D7E`, `#343a40`, `#1c2840`, `#8957e5`, `#238636`, `#da3633` etc. +**Decision**: Don't build custom typography tokens. Tailwind ships a complete type scale (`text-xs`, `text-sm`, `text-base`, etc.) and weight utilities (`font-medium`, `font-semibold`, etc.) out of the box. Building a parallel system now means migrating twice. -4. **Missing scale steps**: No `$danger600`, no `$success700`, no `$info400`, no `$warning200`. Each colour has a different number of tonal variants, making the system unpredictable. +**What to do now**: Avoid adding new hardcoded `fontSize`/`fontWeight` inline styles. When Tailwind lands, replace the 58 hardcoded values with utility classes. -5. **No grey scale**: Greys are named ad hoc (`$text-icon-grey`, `$text-icon-light-grey`, `$bg-light200`, `$footer-grey`) with no numbered scale. +**Exception**: Opacity-based muted text (`opacity: 0.4–0.75`) should be replaced with `colorTextSecondary` / `$text-muted` tokens now — that's a colour/accessibility concern, not typography, and is covered by NP-2. -**Fix**: Define a formal primitive palette with consistent naming: -- Numbered tonal scales (50–900) for each hue, following the convention: lower numbers = lighter -- Derive alpha variants from the same RGB as the solid colour -- Create a proper grey/neutral scale -- Map every orphan hex to a palette token or remove it +**See also**: [#6414](https://github.com/Flagsmith/flagsmith/issues/6414) (inconsistent header font colour) under [#5921](https://github.com/Flagsmith/flagsmith/issues/5921) -**Impact**: This is the prerequisite for semantic tokenisation (LR-2). Without a consistent palette, tokens just paper over the mess. +### NP-9: Explicit imports (remove global component registry) -**Note**: PR #6105 is a Tailwind CSS POC (not yet adopted). If Tailwind is adopted, `theme.extend.colors` could be the home for the palette definition, generating CSS custom properties consumed by both utility classes and the semantic token layer. The palette work itself is Tailwind-agnostic — it needs doing regardless. +**Current state**: `project-components.js` attaches ~15 components to `window` (`Button`, `Row`, `Select`, etc.). Defeats tree-shaking, blocks TypeScript migration, invisible dependencies. -**Validate**: Storybook → "Design System/Colours/Palette Audit" — four stories covering tonal scale inconsistency, alpha mismatches, orphan hex values, and grey scale gaps. +**New pattern**: Explicit imports in every file. No `window.*` globals. ---- +**Adoption strategy**: +1. When converting a `.js` file to `.tsx` (NP-5), add explicit imports +2. After adding imports, remove that component's `window.*` assignment +3. Registry shrinks naturally as files are converted -### LR-7: Full dark mode theme audit (umbrella) +### NP-10: Form building blocks -**Problem**: Only 48 `.dark` CSS selectors exist across the entire stylesheet. Many component areas have zero dark mode coverage: -- Feature pipeline visualisation (white circles, grey lines on dark background) -- Admin dashboard charts -- Integration cards -- Numerous inline styles with light-mode-only colours +**Current state**: `Input.js` is a 231-line legacy class component handling text, password, search, checkbox, and radio inputs in a single file. No TypeScript types. `InputGroup.js` wraps it with label + error display. Validation is entirely manual — inline truthiness checks, no schemas, no validation library. Utils like `isValidEmail()`, `isValidURL()`, `isValidNumber()` are scattered in `utils.tsx`. -**Fix**: Systematic page-by-page dark mode audit. For each page: -1. Toggle dark mode -2. Screenshot -3. Identify contrast failures -4. Add `.dark` overrides or switch to `currentColor` / CSS variables +**New pattern**: Typed form primitives as separate, single-responsibility components: `TextInput`, `SearchInput`, `PasswordInput`, `Checkbox`, `Radio`. Each with built-in validation support (`required`, `pattern`, `validate` props) — no heavy library needed, just a consistent API. `Input.js` is not refactored — it's replaced gradually. -This is the umbrella issue — all the QW and ME items above contribute to this. +**Adoption strategy**: +1. Build the first primitives (`TextInput`, `Checkbox`) with validation API +2. New forms use the new components +3. When touching a form that uses `Input.js`, migrate that usage +4. `Input.js` shrinks naturally until it can be deleted -**Validate**: Storybook → "Design System/Dark Mode Issues/Dark Mode Implementation Patterns" — shows all 3 parallel mechanisms and the proposed solution. +**See also**: [#5746](https://github.com/Flagsmith/flagsmith/issues/5746) (TS migration epic) --- -### LR-8: Standardise typography usage across the codebase - -**Problem**: The type scale (h1–h6, body sizes, weight tiers) exists in SCSS but is bypassed in 58+ places with inline styles. The weight system is fragmented between custom classes (`.font-weight-medium`) and Bootstrap utilities (`fw-bold`, `fw-semibold`). "Subtle" text uses opacity instead of semantic colour. Without enforcing the existing scale, inconsistencies will keep growing. - -**Fix**: -1. Replace all 58 inline `fontSize` values with existing SCSS variables or utility classes -2. Pick one weight class system and migrate the other (e.g. standardise on Bootstrap `fw-*` and remove `.font-weight-medium`, or vice versa) -3. Replace opacity-based muted text patterns with `$text-muted` / `colorTextSecondary` -4. Optionally introduce a `` component if the migration shows repeated patterns that would benefit from a constrained API — but only if the need is demonstrated, not upfront - -**Prerequisite**: ME-10 (audit and consolidate the type scale first). +## Summary ---- +| Category | Count | Status | +|----------|-------|--------| +| Quick wins | 10 | GitHub issues created, 3 with PRs in review | +| Medium efforts | 6 (1 done) | Documented for evaluation | +| New patterns | 10 (2 in progress) | Documented for evaluation — introduce once, adopt incrementally | +| **Total** | **27** | | -## Summary +### Key dependency chains -| Category | Count | Estimated total effort | -|----------|-------|----------------------| -| Quick wins | 8 | 1–2 days | -| Medium efforts | 10 | 5–10 days | -| Large refactors | 8 | 4–7 weeks | +``` +QW-1 (currentColor) → NP-1 (unified icon system) +NP-6 (colour primitives) → NP-2 (semantic tokens) → NP-7 (fix dark mode gaps, page by page) using tokens +NP-8 + ME-11 (typography + spacing) → deferred to Tailwind adoption +QW-7 (a11y POC) → ME-9 (expand a11y coverage) +NP-9 (explicit imports) → NP-5 (TypeScript-first components) +NP-3 (modal system) → cleaner modal API + reduced duplication (React compat handled by PR #6764) +``` -**Recommended order**: QW-1 → QW-5 → QW-6 → QW-7 → QW-2 → QW-3 → ME-8 → ME-2 → ME-4 → ME-10 → ME-9 → ME-7 → ME-1 → LR-1 → LR-6 → LR-2 → LR-8 → rest. +### Recommended priority order -**Key dependency chains**: -- QW-1 (icon currentColor) → QW-2 (component hardcoded colours) → LR-1 (break up Icon.tsx) -- LR-6 (formal palette) → LR-2 (semantic tokens) -- ME-10 (typography tokens) → LR-8 (Text/Heading components) -- QW-7 (CI a11y tests) → ME-9 (expand a11y coverage) +1. **Immediate** (QW with PRs): QW-1, QW-8, QW-9 — already in review +2. **Next sprint**: QW-3, QW-10, QW-11, QW-12, QW-13, QW-14 +3. **Foundation patterns** (in progress): NP-6 + NP-2 (colour primitives + semantic tokens) +4. **After tokens land**: ME-4, ME-10, ME-1 +5. **Introduce remaining patterns**: NP-1, NP-3, NP-4, NP-5, NP-8, NP-9 — each introduced once, then adopted progressively --- ## Appendix: Icon System Inventory -| Category | Count | Details | -|----------|-------|---------| -| Icons in `Icon.tsx` switch | 70 | All inline SVGs, 1,543 lines | -| Icons declared but not implemented | 1 | `paste` — in type, no case | -| Separate SVG components | 23 | Across `svg/`, `base/icons/`, root | -| Integration SVG files | 37 | In `/static/images/integrations/` | -| Icons defaulting to `#1A2634` | ~54 | Invisible in dark mode | -| Icons with hardcoded fills | 9 | Cannot be overridden via props | -| Icons using `currentColor` | 0 | None | -| Unused icon dependency | `ionicons` v7.2.1 | Installed but never imported | +| Category | Count | +|----------|-------| +| Icons in `Icon.tsx` switch | 70 | +| Icons declared but not implemented | 1 (`paste`) | +| Separate SVG components (`svg/`, `base/icons/`) | 23 | +| Integration SVG files (`/static/images/integrations/`) | 37 | +| Files importing IonIcon | 40+ | +| Icons that defaulted to `#1A2634` (pre QW-1) | ~54 | +| Icons with hardcoded SVG fills (pre QW-1) | 9 | + +## Appendix: Highest-Frequency Orphan Hex Values + +| Hex | Occurrences | Files | Current variable | +|-----|-------------|-------|-----------------| +| `#656D7B` | 75 | 48 | `$text-icon-grey` (but most callsites use raw hex) | +| `#9DA4AE` | 59 | 35 | None | +| `#1A2634` | ~54 (in Icon.tsx) | 1 | `$body-color` | +| `#6837FC` | scattered | pipeline files | `$primary` | + +## Appendix: Dark Mode Coverage + +| Mechanism | Count | Files | +|-----------|-------|-------| +| `.dark` CSS selectors | 48 rules | 29 SCSS files | +| `getDarkMode()` runtime calls | 13 | 13 TSX files | +| `data-bs-theme` | 1 (root) | Set but underused | +| Components with zero dark mode support | Many | Toasts, pipeline viz, integration cards, admin charts | diff --git a/frontend/design-system-issues/LR-6-formal-colour-palette.md b/frontend/design-system-issues/LR-6-formal-colour-palette.md index f8455501001f..b76fed246d69 100644 --- a/frontend/design-system-issues/LR-6-formal-colour-palette.md +++ b/frontend/design-system-issues/LR-6-formal-colour-palette.md @@ -27,7 +27,7 @@ Define a formal primitive palette: - A systematic grey/neutral scale (e.g. `$grey50` through `$grey900`) replacing ad hoc names - Every orphan hex value mapped to a palette token or removed -If Tailwind CSS is adopted (PR #6105 is a POC — not yet committed), the palette can live in `theme.extend.colors`. The palette definition itself is Tailwind-agnostic; this work should not be blocked on or tied to the Tailwind decision. +The palette definition should be implemented as SCSS variables and CSS custom properties. If a utility-class framework is adopted in the future, the palette can be mapped to it at that point. ## Acceptance Criteria diff --git a/frontend/design-system-issues/QW-4-release-pipeline-colours.md b/frontend/design-system-issues/QW-4-release-pipeline-colours.md index b02cd4a5994d..e7375f52ad77 100644 --- a/frontend/design-system-issues/QW-4-release-pipeline-colours.md +++ b/frontend/design-system-issues/QW-4-release-pipeline-colours.md @@ -11,7 +11,7 @@ Release pipeline components use raw hex values for status indicator colours inst - `web/components/release-pipelines/ReleasePipelinesList.tsx:169` — `color: isPublished ? '#6837FC' : '#9DA4AE'` - `web/components/release-pipelines/ReleasePipelineDetail.tsx:106` — `color: isPublished ? '#6837FC' : '#9DA4AE'` (same pattern) -- `web/components/release-pipelines/StageCard.tsx:8` — `bg-white` Tailwind class hardcoded, no dark mode equivalent applied +- `web/components/release-pipelines/StageCard.tsx:8` — `bg-white` Bootstrap class hardcoded, no dark mode equivalent applied ## Proposed Fix @@ -27,7 +27,7 @@ color: isPublished ? '#6837FC' : '#9DA4AE' color: isPublished ? colorBrandPrimary : colorTextTertiary ``` -For `StageCard.tsx`, replace the hardcoded `bg-white` Tailwind class with the appropriate themed background token or a dark-mode-aware Tailwind variant (e.g. `bg-white dark:bg-[var(--color-bg-level-1)]`), consistent with how other card components handle their background. +For `StageCard.tsx`, replace the hardcoded `bg-white` class with the appropriate themed background token (e.g. a CSS custom property via `var(--color-bg-level-1)`), consistent with how other card components handle their background in dark mode. ## Acceptance Criteria diff --git a/frontend/design-system-issues/QW-7-a11y-tests-ci.md b/frontend/design-system-issues/QW-7-a11y-tests-ci.md index bb37e69b967a..d5f8091310cb 100644 --- a/frontend/design-system-issues/QW-7-a11y-tests-ci.md +++ b/frontend/design-system-issues/QW-7-a11y-tests-ci.md @@ -1,44 +1,39 @@ --- -title: "Wire accessibility E2E tests into CI" -labels: ["design-system", "quick-win", "accessibility"] +title: "POC: Evaluate wiring accessibility E2E tests into CI" +labels: ["design-system", "quick-win", "accessibility", "spike"] --- -## Problem +## Objective -6 axe-core/Playwright accessibility E2E tests exist in the repository but are not included in the CI pipeline. This means contrast ratio regressions and other accessibility violations can be merged to `main` without being caught automatically. +Evaluate the feasibility of integrating the existing axe-core/Playwright accessibility tests into the CI pipeline. This is a spike/POC — not an implementation commitment. -## Files +## Context -- `e2e/tests/accessibility-tests.pw.ts` — existing axe-core tests (6 tests) -- `e2e/helpers/accessibility.playwright.ts` — `checkA11y()` helper used by the tests -- CI config (GitHub Actions workflow file) — needs an accessibility test job added - -## Proposed Fix +6 axe-core/Playwright accessibility E2E tests exist in the repository (`e2e/tests/accessibility-tests.pw.ts`) but are not included in the CI pipeline. Before committing to full integration, we need to understand: -Add the accessibility tests to the existing Playwright CI job. The tests should be configured to fail only on `critical` and `serious` axe violations, not `moderate` or `minor`, to avoid excessive noise while still blocking regressions. +1. **CI impact** — How much time do the a11y tests add to the pipeline? +2. **Noise level** — How many existing violations surface? Are they actionable or overwhelming? +3. **Severity filtering** — Can we configure axe to fail only on `critical`/`serious` violations? +4. **Infrastructure** — Do the tests need Docker services running? What's the dependency footprint? -Example configuration in the workflow: +## Files to Investigate -```yaml -- name: Run accessibility tests - run: npm run test -- e2e/tests/accessibility-tests.pw.ts - env: - E2E_RETRIES: 1 -``` - -If the existing CI job runs tests by tag, ensure the accessibility tests carry an appropriate tag (e.g. `@a11y`) so they can be targeted or excluded independently. +- `e2e/tests/accessibility-tests.pw.ts` — existing axe-core tests (6 tests) +- `e2e/helpers/accessibility.playwright.ts` — `checkA11y()` helper +- `.github/workflows/` — existing CI workflow files +- `e2e/playwright.config.ts` — test configuration -## Acceptance Criteria +## POC Tasks -- [ ] CI runs the accessibility tests on every pull request -- [ ] Tests fail on `critical` and `serious` axe violations -- [ ] Contrast ratio regressions block merging -- [ ] CI job name and step are clearly labelled as accessibility tests -- [ ] Existing test run time is not significantly impacted (accessibility tests are fast) +- [ ] Run the a11y tests locally and document pass/fail results +- [ ] Measure execution time +- [ ] List all current violations by severity level +- [ ] Draft a CI workflow addition (without merging) +- [ ] Document findings and recommendation (proceed / defer / adjust scope) -## Storybook Validation +## Success Criteria -Not applicable — this is a CI configuration task. +A short write-up answering: Should we wire these tests into CI now, and if so, with what configuration? ## Dependencies diff --git a/frontend/design-system-issues/REPORT_DRAFT.md b/frontend/design-system-issues/REPORT_DRAFT.md new file mode 100644 index 000000000000..ed4823378174 --- /dev/null +++ b/frontend/design-system-issues/REPORT_DRAFT.md @@ -0,0 +1,136 @@ +## Audit Results + +Full report and Storybook evidence available on branch `chore/design-system-audit-6606` ([`frontend/DESIGN_SYSTEM_ISSUES.md`](https://github.com/Flagsmith/flagsmith/blob/chore/design-system-audit-6606/frontend/DESIGN_SYSTEM_ISSUES.md)). Run `npm run storybook` on that branch for visual evidence. + +Work is tracked under [#6882 — Resolve UI inconsistencies and consolidation](https://github.com/Flagsmith/flagsmith/issues/6882). + +--- + +### Scope + +Audited 525 component files across `web/components/`, `web/styles/`, and `common/`. Combined code scanning (SCSS variables, inline styles, dark mode selectors) with visual review in both light and dark mode. Screenshots captured in [Penpot](https://design.penpot.app/#/workspace?team-id=72ad1239-4f5c-8115-8007-a72d5b669ca5&file-id=72ad1239-4f5c-8115-8007-a72dd0aef2fe&page-id=72ad1239-4f5c-8115-8007-a72dd0aef2ff). + +--- + +### Findings + +#### Dark Mode + +The most critical category. Large portions of the UI are broken or unreadable in dark mode. + +- **3 parallel theming mechanisms** that don't compose: `.dark` SCSS selectors (48 rules across 29 files), `getDarkMode()` runtime calls (13 components), and `data-bs-theme` attribute (set but underused) +- **~54 icons invisible in dark mode** — `Icon.tsx` defaulted fills to `#1A2634` (near-black), which is invisible on the dark background (`#101628`). Contrast ratio ~1.1:1 +- **Button variants missing dark overrides** — `btn-tertiary`, `btn-danger`, `btn--transparent` have no `.dark` selectors despite dark variables being defined +- **Toast notifications** — zero dark mode styles, hardcoded SVG icon colours +- **Checkbox and switch** — `Switch.tsx` hardcodes sun/moon icon colours, form checkboxes/radio have no `.dark` overrides +- **Chart axis labels** — Recharts components use hardcoded tick fill colours, invisible in dark mode +- **Feature pipeline, integration cards, admin dashboard** — no dark mode coverage at all +- **SidebarLink** — hover state references non-existent CSS classes, broken in dark mode + +#### Colours + +No formal colour system. Values are scattered and inconsistent. + +- **280+ hardcoded hex values** in TSX files not tied to any token or variable +- **Highest-frequency orphan values**: `#656D7B` (75 occurrences, 48 files), `#9DA4AE` (59 occurrences, 35 files), `#1A2634` (~54 in Icon.tsx) +- **No systematic grey/neutral scale** — greys named ad hoc (`$text-icon-grey`, `$text-icon-light-grey`, `$bg-light200`, `$footer-grey`) +- **Inverted tonal scales** — `$primary400` (`#956CFF`) is lighter than `$primary` (`#6837FC`), reversing the lower-number-is-lighter convention +- **Alpha colour RGB mismatches** — `$primary-alfa-*` uses RGB `(149, 108, 255)` but `$primary` is `(104, 55, 252)`. Same mismatch for `$danger` and `$warning` alpha variants +- **Missing scale steps** — no `$danger600`, `$success700`, `$info400`, `$warning200` +- **Release pipelines** — 19 hardcoded hex values across 8 files + +#### Typography + +A type scale exists in SCSS but is widely bypassed. + +- **58 hardcoded inline `fontSize` values** in TSX: `13px` (17×), `12px` (12×), `11px` (7×), and more +- **9 inline `fontWeight` values** bypassing the class system +- **Competing weight systems** — custom `.font-weight-medium` alongside Bootstrap `fw-bold`/`fw-semibold`/`fw-normal` +- **Invalid CSS** — `fontWeight: 'semi-bold'` in `SuccessMessage` (browsers ignore it) +- **Opacity-based muted text** — `opacity: 0.4–0.75` instead of colour tokens, fails WCAG contrast when background isn't guaranteed + +See also: [#6414](https://github.com/Flagsmith/flagsmith/issues/6414) (inconsistent header font colour) + +#### Spacing + +- **8px base grid** defined via `$spacer` but widely broken +- **`5px`** — 10+ occurrences across buttons, panels, tags, lists, chips +- **`6px`** — 3+ files +- **`3px`** — some intentional (centring), some not +- **`15px`, `19px`** — one-offs in tabs and icons + +#### Icons + +Three separate icon systems with no unified API. + +- **`Icon.tsx`** — 1,543 lines, 70+ inline SVGs in a single switch statement. No tree-shaking possible +- **`web/components/svg/`** — 19 standalone SVG components with their own hardcoded fills +- **IonIcon** — 40+ files import from `ionicons`/`@ionic/react` +- **1 icon declared but not implemented** — `paste` exists in the `IconName` type but has no case +- **9 icons with fills baked into SVG elements** — `fill` prop has no effect + +#### Components + +Duplication and inconsistency across common patterns. + +- **14 near-identical confirmation modals** — each implements its own layout, buttons, and copy for the "type name to confirm" pattern +- **4 dropdown patterns** — `DropdownMenu`, `ButtonDropdown`, `AccountDropdown` (duplicates positioning logic), `EnvironmentSelectDropdown` +- **`Input.js`** — 231-line legacy class component handling text, password, search, checkbox, and radio in one file. No TypeScript types +- **`ErrorMessage.js`** — legacy class component coexists with TypeScript replacement (`messages/ErrorMessage.tsx`). 37 files import the legacy version +- **`Button`** — imports Redux store to check a `hasFeature` prop that is always true (dead code) +- **Modal system** — `openModal()`, `openModal2()`, `openConfirm()` as globals on `window`. Uses deprecated `ReactDOM.render` (being resolved in [PR #6764](https://github.com/Flagsmith/flagsmith/pull/6764)) +- **Global component registry** — `project-components.js` attaches ~15 components to `window`, defeating tree-shaking and blocking TS migration + +#### Accessibility + +- **Secondary text fails WCAG AA** — `#656D7B` on white gives 4.48:1 (needs 4.5:1 for normal text). Worse in dark mode +- **6 axe-core E2E tests exist** but are not wired into CI — contrast regressions can ship undetected +- **Only 6 pages covered** by a11y tests — Segments, Audit Log, Integrations, Users & Permissions, Organisation Settings, Identities, Release Pipelines, and Change Requests have no coverage + +--- + +### What's Being Addressed + +**Quick wins** — 10 GitHub issues created under [#6882](https://github.com/Flagsmith/flagsmith/issues/6882): + +| Issue | What | +|-------|------| +| [#6869](https://github.com/Flagsmith/flagsmith/issues/6869) | Icons: replace hardcoded fills with `currentColor` ([PR #6870](https://github.com/Flagsmith/flagsmith/pull/6870)) | +| [#6889](https://github.com/Flagsmith/flagsmith/issues/6889) | Chart axis colours in dark mode | +| [#6890](https://github.com/Flagsmith/flagsmith/issues/6890) | POC: evaluate wiring a11y tests into CI | +| [#6872](https://github.com/Flagsmith/flagsmith/issues/6872) | Fix invalid `fontWeight: 'semi-bold'` ([PR #6873](https://github.com/Flagsmith/flagsmith/pull/6873)) | +| [#6868](https://github.com/Flagsmith/flagsmith/issues/6868) | SidebarLink hover state in dark mode ([PR #6871](https://github.com/Flagsmith/flagsmith/pull/6871)) | +| [#6866](https://github.com/Flagsmith/flagsmith/issues/6866) | Decouple Button from Redux store | +| [#6891](https://github.com/Flagsmith/flagsmith/issues/6891) | Remove legacy ErrorMessage.js | +| [#6892](https://github.com/Flagsmith/flagsmith/issues/6892) | Button dark mode gaps (tertiary, danger, transparent) | +| [#6893](https://github.com/Flagsmith/flagsmith/issues/6893) | Toast dark mode support | +| [#6894](https://github.com/Flagsmith/flagsmith/issues/6894) | Checkbox and switch dark mode states | + +**Foundation work in progress**: +- Semantic colour tokens + colour primitive palette on `chore/design-system-tokens` branch +- `@storybook/addon-a11y` installed on audit branch — every story has a WCAG accessibility panel + +**Related efforts**: +- [#5746](https://github.com/Flagsmith/flagsmith/issues/5746) — TS migration epic (overlaps with legacy `.js` component findings) +- [#5921](https://github.com/Flagsmith/flagsmith/issues/5921) — UI improvements ([#6414](https://github.com/Flagsmith/flagsmith/issues/6414) overlaps with typography findings) +- [PR #6764](https://github.com/Flagsmith/flagsmith/pull/6764) — React 19 migration (resolves deprecated modal API) + +--- + +### What Needs Team Discussion + +These findings require architectural decisions before work can start: + +1. **Dark mode strategy** — Continue with `.dark` SCSS selectors, or move to CSS custom properties (semantic tokens) as the single theming mechanism? The token approach is drafted but needs team buy-in before full adoption. + +2. **Icon system** — Three systems coexist (Icon.tsx switch, svg/ directory, IonIcon). What's the target? Individual icon files with barrel export? Keep the `` API? + +3. **Modal system** — PR #6764 fixes the React compat issue, but the global imperative API (`openModal`/`openModal2` on `window`) and 14 duplicate confirmation modals remain. Worth introducing a context-based pattern? + +4. **Form primitives** — `Input.js` handles 5 input types in one 231-line file. Introduce new typed building blocks (`TextInput`, `Checkbox`, `Radio`, etc.) and replace gradually? + +5. **Typography and spacing** — 58 hardcoded `fontSize` values and off-grid spacing. Build custom tokens now, or wait for Tailwind adoption (which ships type scale and spacing utilities out of the box)? + +6. **Table/list standardisation** — 9 `TableFilter*` components, 5+ row components, 5+ list components with no shared abstractions. Worth building a composable system, or too ambitious for now? + +7. **Global component registry** — `project-components.js` attaches ~15 components to `window`. Removing it unblocks tree-shaking and TS migration, but requires adding explicit imports to every consuming `.js` file first. From 6164dc8a35c8821815bda7c94d2151f2f4a592a0 Mon Sep 17 00:00:00 2001 From: Talisson Costa Date: Mon, 9 Mar 2026 19:05:11 -0300 Subject: [PATCH 10/11] ci: add Chromatic visual regression workflow for Storybook Publishes Storybook to Chromatic on frontend PRs for easy preview. Co-Authored-By: Claude Opus 4.6 --- .github/workflows/frontend-chromatic.yml | 45 ++++++++++++++++++++++++ 1 file changed, 45 insertions(+) create mode 100644 .github/workflows/frontend-chromatic.yml diff --git a/.github/workflows/frontend-chromatic.yml b/.github/workflows/frontend-chromatic.yml new file mode 100644 index 000000000000..8a2eb69fefa9 --- /dev/null +++ b/.github/workflows/frontend-chromatic.yml @@ -0,0 +1,45 @@ +name: Frontend Chromatic + +on: + pull_request: + types: [opened, synchronize, reopened, ready_for_review] + paths: + - frontend/** + - .github/workflows/frontend-chromatic.yml + +permissions: + contents: read + +jobs: + chromatic: + name: Chromatic + runs-on: ubuntu-latest + # if: github.event.pull_request.draft == false + + defaults: + run: + working-directory: frontend + + steps: + - uses: actions/checkout@v5 + with: + fetch-depth: 0 + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version-file: frontend/.nvmrc + cache: npm + cache-dependency-path: frontend/package-lock.json + + - name: Install dependencies + run: npm ci --legacy-peer-deps + + - name: Publish to Chromatic + uses: chromaui/action@latest + with: + workingDir: frontend + projectToken: ${{ secrets.CHROMATIC_PROJECT_TOKEN }} + exitZeroOnChanges: true + exitOnceUploaded: true + onlyChanged: true From 73f0e857709b5d2615953df681d24cdac5478a16 Mon Sep 17 00:00:00 2001 From: Talisson Costa Date: Mon, 9 Mar 2026 19:06:56 -0300 Subject: [PATCH 11/11] deps: add chromatic for Storybook visual review Co-Authored-By: Claude Opus 4.6 --- frontend/package-lock.json | 25 +++++++++++++++++++++++++ frontend/package.json | 1 + 2 files changed, 26 insertions(+) diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 7c4d9ca90f31..c406525b88ff 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -149,6 +149,7 @@ "@typescript-eslint/eslint-plugin": "5.4.0", "@typescript-eslint/parser": "5.4.0", "archiver": "^7.0.1", + "chromatic": "^15.2.0", "eslint": "^8.0.1", "eslint-config-prettier": "^8.3.0", "eslint-plugin-import": "2.27.5", @@ -10168,6 +10169,30 @@ "node": ">= 6" } }, + "node_modules/chromatic": { + "version": "15.2.0", + "resolved": "https://registry.npmjs.org/chromatic/-/chromatic-15.2.0.tgz", + "integrity": "sha512-c9tDfE62aiPVPnVab8jQLz+9c9II/CUFZ6T2Kk3hi2hSL+HLkRwX3zjwRYW1z9Shn57R/ORBEpQ3ftufp8EgWA==", + "dev": true, + "license": "MIT", + "bin": { + "chroma": "dist/bin.js", + "chromatic": "dist/bin.js", + "chromatic-cli": "dist/bin.js" + }, + "peerDependencies": { + "@chromatic-com/cypress": "^0.*.* || ^1.0.0", + "@chromatic-com/playwright": "^0.*.* || ^1.0.0" + }, + "peerDependenciesMeta": { + "@chromatic-com/cypress": { + "optional": true + }, + "@chromatic-com/playwright": { + "optional": true + } + } + }, "node_modules/chrome-trace-event": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/chrome-trace-event/-/chrome-trace-event-1.0.4.tgz", diff --git a/frontend/package.json b/frontend/package.json index 6d86728a0f44..e05def9629c2 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -179,6 +179,7 @@ "@typescript-eslint/eslint-plugin": "5.4.0", "@typescript-eslint/parser": "5.4.0", "archiver": "^7.0.1", + "chromatic": "^15.2.0", "eslint": "^8.0.1", "eslint-config-prettier": "^8.3.0", "eslint-plugin-import": "2.27.5",