Skip to content

Commit 80dfe91

Browse files
feat(visualization): redesign color settings popover (#4486)
1 parent acbb426 commit 80dfe91

47 files changed

Lines changed: 1911 additions & 324 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

plans/2026-05-13-redesign-color-settings-popover.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
---
22
name: redesign-color-settings-popover
33
issue: ""
4-
state: progress
4+
state: complete
55
version: <next>
66
date: 2026-05-13
77
git_commit: 7708617904d4310f0896fc9b9f666235d6eab2f6
@@ -12,6 +12,8 @@ tags: [plan, visualization, metricsBar, colorSettingsPopover, daisyui, restyle]
1212

1313
# Redesign the Color settings popover
1414

15+
> **Superseded** by `plans/color_metric_popover/2026-06-11-restyle-color-metric-popover.md` — the popover is being migrated to a new mockup-based layout. Phases 1–2 of this plan were implemented and remain in place until the new plan lands.
16+
1517
## Goal
1618

1719
The Color settings popover currently looks broken (`RestyleImages/BadMenu.png`): a 150 px-wide slider rail floats in a 640 px container leaving the number inputs orphaned at the edges, two full-width red-outline reset buttons dominate the visual hierarchy, and the color picker rows sit sparsely in a popover much wider than they need to be. The Label settings panel (`features/labelSettings/components/labelSettingsPanel/labelSettingsPanel.component.html`) is the visual reference — calm, scannable, no oversized destructive buttons. Bring the Color popover to that standard by re-arranging the existing sub-components and demoting the reset buttons. No sub-component refactors.
Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
---
2+
name: restyle-color-metric-popover
3+
issue: ""
4+
state: complete
5+
version: 1
6+
date: 2026-06-11
7+
tags: [plan, visualization, metricsBar, colorSettingsPopover, markedPackages, restyle]
8+
---
9+
10+
# Restyle the Color settings popover to the new mockup
11+
12+
Mockup: `plans/color_metric_popover/restyle_color_metric.png`
13+
Supersedes: `plans/2026-05-13-redesign-color-settings-popover.md`
14+
15+
## Goal
16+
17+
Migrate the Color settings popover to the mockup's single-column layout: header with metric name, slider row, distribution chart, segmented gradient mode, bands list with node counts, a folder-overrides section (backed by markedPackages), and an invert/reset footer.
18+
19+
Decisions (clarified with user):
20+
21+
- **Styling**: structure only — adopt the mockup's layout/sections/controls but use existing DaisyUI/Tailwind theme tokens (no cream/mono theme).
22+
- **Gradient modes**: keep all 4 `ColorMode` values as a segmented control (Absolute / Focused / Weighted / Relative). No behavior change.
23+
- **Folder overrides**: reuse the markedPackages state (shared with sidebar). Swatch click recolors with arbitrary colors, × unmarks, "Pin a folder color…" opens a folder search that marks a new package.
24+
- **Scope**: phased — restyle first, folder overrides second. The 2026-05-22 metrics-bar distributions plan stays separate.
25+
26+
## Tasks
27+
28+
### 1. Restyle existing controls into the mockup layout
29+
30+
- Fixed narrow popover width (~420px), single column; drop the `w-[640px]`/`w-80` dynamic width logic
31+
- Header row: swatch dot + "Color by <metric>" + reset-thresholds icon button (right) — replaces the inline "Reset thresholds" button
32+
- Slider row: min input — slider rail with value labels above — max input
33+
- DISTRIBUTION section: section label, "<metric> × quantile %" caption, quantile diagram resized to fit the column (replace hardcoded 550×250 SVG size)
34+
- GRADIENT MODE: segmented control (DaisyUI join) for all 4 modes
35+
- BANDS section: one row per band — swatch + value range + node count; swatch opens the existing color picker (replaces the labelled picker rows); keep the "selected" row
36+
- Footer: Invert colors toggle (left) + Reset colors button (right)
37+
- Delta mode / unary metric: keep the simplified narrow variant (delta pickers + selected + footer), as today
38+
39+
### 2. Folder overrides section (markedPackages)
40+
41+
- Selector providing marked packages with descendant file counts
42+
- Section: "FOLDER OVERRIDES" label + "<n> pinned" + helper text ("These folders use a fixed color instead of the metric. Click a swatch to recolor.")
43+
- Row: swatch (click → color picker, dispatch `markPackages` with new color) + path + file count + × (`unmarkPackage`)
44+
- "Pin a folder color…": inline folder search (autocomplete over folder paths) that marks the chosen folder
45+
- Section always shows the pin button; list renders only when packages exist
46+
47+
### 3. Tests + changelog
48+
49+
- Unit tests for new selector(s), band counts, and folder-override interactions
50+
- `Changed` entry in `visualization/CHANGELOG.md`
51+
52+
## Steps
53+
54+
- [x] Complete Task 1: restyle existing controls into the mockup layout
55+
- [x] Complete Task 2: folder overrides section
56+
- [x] Complete Task 3: tests + changelog
57+
58+
## Review Feedback Addressed
59+
60+
1. **Invert/Reset placement**: Moved the Invert colors toggle + Reset colors row from the popover footer to between the Bands section and Folder Overrides.
61+
2. **Reset scope**: `Reset colors` now also resets the gradient mode (`dynamicSettings.colorMode`) — outside delta mode only, since the control is not visible there.
62+
3. **Picker light-dismiss bug**: Choosing a color closed the popover because the mat-menu picker rendered in a CDK overlay on `document.body`, outside the native `[popover]`, triggering light dismiss. Replaced with `cc-inline-color-picker`, which renders the `color-chrome` panel inside the popover DOM (fixed-positioned so it escapes the scrollable override list).
63+
4. **Override list density**: Rows sit in a `gap-0.5` container capped at `max-h-36` (~5 rows) with `overflow-y-auto`; full path shown as tooltip on each row and each search suggestion.
64+
5. **Folder search auto-close**: The search closes on blur when empty (typed terms keep it open); the suggestion list prevents the input blur on mousedown so clicking a suggestion with an empty term still pins it.
65+
6. **Invert/Reset row sizing**: The row looked oversized next to the dense column — "Invert colors" now uses `text-sm` and the Reset colors button renders as `btn-sm` via a new opt-in `small` input on `cc-reset-settings-button` (other popovers keep the full-size default).
66+
7. **Sub-agent review findings (all 12 fixed)**: inline picker closes on `pointerdown` instead of `click` so a drag released outside the panel keeps the picked color (pending `(onChange)` value is emitted if the close races the 100ms `onChangeComplete` debounce); panel also closes on container scroll, window resize, and popover close; Escape cancels the folder search without light-dismissing the popover (`preventDefault`); `mousedown` preventDefault moved from the suggestion `<ul>` (broke scrollbar dragging) to the buttons; helper copy corrected (marking colors tint folder floors, not buildings) with an "n files" tooltip on counts; both selectors skip excluded nodes; count attribution rewritten as an O(leaves×depth) path walk-up; `rounded-btn` (removed in DaisyUI 5) replaced with `rounded-md`; nested pins avoid their marked parent's color (reducer would drop them); gradient radio group got `role="radiogroup"` and a per-popover `name`; reset tooltip is delta-aware; new tests for all of the above plus the header threshold reset and the diagram size inputs. Components were also rewired through feature stores/services (`FolderOverridesStore/Service`, `MapColorsStore.setMapColors`) to satisfy the dependency-cruiser rule `feature-only-stores-can-import-ngrx-store`.
67+
68+
## Notes
69+
70+
- Band counts: `colorCategoryCounts$` (`ui/codeMap/codeMap.render.service.ts`) already tracks positive/neutral/negative node counts — reuse or derive a selector from `metricDataSelector` + `colorRangeSelector`
71+
- Marked-package recoloring is no longer limited to the 5 `markingColors` — verify rendering and sidebar handle arbitrary hex colors
72+
- Header reset icon resets thresholds only; footer "Reset colors" keeps its current behavior (colors + invert checkbox)
73+
- Verified headless via Electron + Playwright (`xvfb-run`): popover layout, segmented gradient mode, band counts, pin/recolor/unpin folder overrides (pinned `/root` → count 8 across both sample maps), invert toggle, threshold reset. Delta-mode variant covered by unit tests only.
74+
- New files: `colorBandRow.component.*`, `folderOverrides.component.*`, `markedPackagesWithCounts.selector.*`, `markableFolderPaths.selector.*` (all under `features/metricsBar/`); diagram got `diagramWidth`/`diagramHeight`/`showAxisLabels` inputs
75+
- `npm run build`, full `npm test` (367 suites), and `npm run format:check` all pass
67.8 KB
Loading

visualization/CHANGELOG.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,13 +13,14 @@ and this project adheres to [Semantic Versioning](http://semver.org/)
1313
- **Label Size slider**: New slider in the Label Settings panel scales floating label text (name, metric value, and "+N more" badge) between 0.75× and 2.5×. The setting is preserved in scenarios.
1414
- **File Explorer sidebar**: The file tree was redesigned as a dedicated left-side sidebar drawer that overlays the codemap. The drawer adds a Shown/Flattened/Hidden chip row at the top, a sort control, and click-to-edit popovers for the active flatten and exclude rules.
1515
- **Collapsible File Explorer**: The sidebar can now be collapsed via the `«` button in the header. Collapsed mode shows a small floating search box (with the kebab Flatten/Exclude menu) pinned at the top-left, freeing the full viewport for the codemap. Re-expand with the folder-tree button.
16+
- **Folder overrides in the Color settings popover**: Marked packages can now be managed directly from the Color popover. A new "Folder Overrides" section lists each pinned folder with its file count, lets you recolor it via a swatch click (any color, no longer limited to the five marking colors), unpin it, and pin new folders through an inline folder search.
1617

1718
### Changed
1819

1920
- **Inspector sidebar**: The attribute sidebar was redesigned as the "Inspector" — a right-side drawer (`features/sidebarInspector/`) that slides in when a building is selected and closes via ✕ (which also deselects the building) or by deselecting. It shows the selected node's path with a copy-to-clipboard button and badges for the node type (file/folder) and a folder's file count, the current Area/Height/Color/Edge metric mapping with the selected node's value per mapped metric (buildings show their own values, folders the aggregate of the contained files; the color block carries an `inverted` marker), and all node metrics with severity-colored bars sized by the value's share of the whole map — a building with 100 rloc in a 1,000-rloc map fills 10% (severity respects higher-is-better metric directions). A `map | range` toggle in the METRICS header switches the bars between comparing against the whole map and comparing against the min/max range of all files. The header and metric mapping stay pinned at the top; only the metrics list scrolls. Metrics with empty (zero) values and no delta are greyed out and grouped in a collapsible "Empty metrics" section below the valued metrics; in comparison mode a metric that dropped to zero stays in the main list so its delta remains visible. Delta values and delta file counts are still shown in comparison mode. The Σ/median attribute-type toggle was removed.
2021
- **Floating metrics bar**: Replaced the top-of-page ribbon with a single rounded card that floats just above the bottom bar (`features/metricsBar/`). Each metric segment (Scenario, Area, Height, Color, Edges, Labels) opens a native popover for search and settings; the bar uses DaisyUI primitives and is free of `@angular/material`.
2122
- **Metric settings popovers**: Restyled the Area, Height, Color, and Edge settings popovers to match the Label settings panel — same DaisyUI spacing (`gap-2.5 py-2 px-5`) and a full `btn btn-outline btn-error` reset button that stretches to the popover width.
22-
- **Color settings popover layout**: Re-laid out the Color settings popover so the threshold slider rail aligns with the number inputs, the `Reset thresholds` button sits inline at the end of the slider row, the popover narrows to `w-80` in delta or unary-metric mode, and the bottom row reads `Invert Colors` (left) → `Reset colors` (right).
23+
- **Color settings popover layout**: Redesigned the Color settings popover as a single narrow column: a header with the metric name and a reset-thresholds icon, the threshold slider, a compact "Distribution" quantile chart, the gradient mode as a segmented control (Absolute / Focused / Weighted / Relative), a "Bands" list showing each color band with its building count and a click-to-recolor swatch, followed by an Invert colors toggle and the `Reset colors` button (which now also resets the gradient mode) above the folder overrides. The popover narrows further in delta or unary-metric mode.
2324
- **Faster blacklist operations**: Adding or removing flatten and exclude patterns is significantly faster on large codebases — the matching engines are now cached and only rebuilt when the blacklist itself changes.
2425

2526
### Fixed 🐞
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
<div class="flex items-center gap-2">
2+
<cc-inline-color-picker
3+
[hexColor]="color()"
4+
[ariaLabel]="'Recolor ' + mapColorFor()"
5+
(colorChange)="handleColorChange($event)"
6+
></cc-inline-color-picker>
7+
<span class="min-w-0 flex-1 truncate text-sm">
8+
{{ mapColorFor() | mapColorLabel: colorRange() : nodeMetricRange() : colorMetric() }}
9+
</span>
10+
@if (count() !== null) {
11+
<span class="text-xs opacity-60 tabular-nums">{{ count() }}</span>
12+
}
13+
</div>
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
import { TestBed } from "@angular/core/testing"
2+
import { State } from "@ngrx/store"
3+
import { MockStore, provideMockStore } from "@ngrx/store/testing"
4+
import { render, screen } from "@testing-library/angular"
5+
import { setMapColors } from "../../../../state/store/appSettings/mapColors/mapColors.actions"
6+
import { defaultState } from "../../../../state/store/state.manager"
7+
import { ColorBandRowComponent } from "./colorBandRow.component"
8+
9+
describe("ColorBandRowComponent", () => {
10+
async function setup(inputs: { mapColorFor: string; count?: number | null }) {
11+
const renderResult = await render(ColorBandRowComponent, {
12+
inputs,
13+
providers: [provideMockStore({ initialState: defaultState }), { provide: State, useValue: { getValue: () => defaultState } }]
14+
})
15+
return { component: renderResult.fixture.componentInstance }
16+
}
17+
18+
it("should render the band count when one is provided", async () => {
19+
// Arrange & Act
20+
await setup({ mapColorFor: "positive", count: 312 })
21+
22+
// Assert
23+
expect(screen.getByText("312")).not.toBeNull()
24+
})
25+
26+
it("should not render a count when none is provided", async () => {
27+
// Arrange & Act
28+
await setup({ mapColorFor: "selected" })
29+
30+
// Assert
31+
expect(screen.getByText("selected")).not.toBeNull()
32+
expect(screen.queryByText("null")).toBeNull()
33+
})
34+
35+
it("should resolve its color from the map colors", async () => {
36+
// Arrange & Act
37+
const { component } = await setup({ mapColorFor: "positive" })
38+
39+
// Assert
40+
expect(component.color()).toBe(defaultState.appSettings.mapColors.positive)
41+
})
42+
43+
it("should dispatch setMapColors for its map color when the picker emits", async () => {
44+
// Arrange
45+
const { component } = await setup({ mapColorFor: "neutral" })
46+
const store = TestBed.inject(MockStore)
47+
const dispatchSpy = jest.spyOn(store, "dispatch")
48+
49+
// Act
50+
component.handleColorChange("#123456")
51+
52+
// Assert
53+
expect(dispatchSpy).toHaveBeenCalledWith(setMapColors({ value: { neutral: "#123456" } }))
54+
})
55+
})
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
import { ChangeDetectionStrategy, Component, computed, input } from "@angular/core"
2+
import { toSignal } from "@angular/core/rxjs-interop"
3+
import { MapColors } from "../../../../codeCharta.model"
4+
import { defaultMapColors } from "../../../../state/store/appSettings/mapColors/mapColors.reducer"
5+
import { MapColorLabelPipe } from "../../../../ui/colorPickerForMapColor/mapColorLabel.pipe"
6+
import { ColorMetricService } from "../../services/colorMetric.service"
7+
import { ColorRangeService } from "../../services/colorRange.service"
8+
import { MapColorsService } from "../../services/mapColors.service"
9+
import { SelectedColorMetricDataService } from "../../services/selectedColorMetricData.service"
10+
import { InlineColorPickerComponent } from "./inlineColorPicker.component"
11+
12+
export type BandMapColor = keyof Omit<MapColors, "labelColorAndAlpha" | "markingColors" | "isColorRangeInverted" | "areDeltaColorsInverted">
13+
14+
@Component({
15+
selector: "cc-color-band-row",
16+
templateUrl: "./colorBandRow.component.html",
17+
changeDetection: ChangeDetectionStrategy.OnPush,
18+
imports: [InlineColorPickerComponent, MapColorLabelPipe]
19+
})
20+
export class ColorBandRowComponent {
21+
constructor(
22+
private readonly colorMetricService: ColorMetricService,
23+
private readonly colorRangeService: ColorRangeService,
24+
private readonly selectedColorMetricDataService: SelectedColorMetricDataService,
25+
private readonly mapColorsService: MapColorsService
26+
) {}
27+
28+
readonly mapColorFor = input.required<BandMapColor>()
29+
readonly count = input<number | null>(null)
30+
31+
readonly colorMetric = toSignal(this.colorMetricService.colorMetric$(), { initialValue: "" })
32+
readonly colorRange = toSignal(this.colorRangeService.colorRange$(), { initialValue: { from: 0, to: 0 } })
33+
readonly nodeMetricRange = toSignal(this.selectedColorMetricDataService.selectedColorMetricData$(), {
34+
initialValue: { values: [] as number[], minValue: 0, maxValue: 0 }
35+
})
36+
private readonly mapColors = toSignal(this.mapColorsService.mapColors$(), { initialValue: defaultMapColors })
37+
38+
readonly color = computed(() => this.mapColors()[this.mapColorFor()] as string)
39+
40+
handleColorChange(newHexColor: string) {
41+
this.mapColorsService.setMapColors({ [this.mapColorFor()]: newHexColor })
42+
}
43+
}
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
<div class="flex flex-col gap-1">
2+
<span class="text-xs uppercase tracking-wider opacity-60">Bands</span>
3+
@if (!isDeltaState()) {
4+
<cc-color-band-row mapColorFor="positive" [count]="colorCategoryCounts().positive"></cc-color-band-row>
5+
<cc-color-band-row mapColorFor="neutral" [count]="colorCategoryCounts().neutral"></cc-color-band-row>
6+
<cc-color-band-row mapColorFor="negative" [count]="colorCategoryCounts().negative"></cc-color-band-row>
7+
} @else {
8+
<cc-color-band-row mapColorFor="positiveDelta"></cc-color-band-row>
9+
<cc-color-band-row mapColorFor="negativeDelta"></cc-color-band-row>
10+
}
11+
<cc-color-band-row mapColorFor="selected"></cc-color-band-row>
12+
</div>

0 commit comments

Comments
 (0)