Skip to content

Commit 7b3d4d9

Browse files
committed
Add column color mode (#16)
A toolbar toggle that tints every data column with a distinct, theme-aware background hue, spread by golden-angle rotation so adjacent columns stay far apart on the wheel for any column count. The hue is injected per column as a CSS variable on the stable AG Grid col-id; lightness, chroma and alpha live in CSS and adapt to light, dark and high-contrast themes. The tint is a translucent OKLCH overlay, so foreground text, frozen rows, selection, find matches and duplicate highlights keep painting clearly on top. Persisted globally like zoom, so it is remembered across files and sessions.
1 parent b411134 commit 7b3d4d9

10 files changed

Lines changed: 155 additions & 3 deletions

File tree

media/webview.css

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -472,6 +472,43 @@ button .codicon {
472472
:is(.ag-theme-alpine-dark, .ag-theme-alpine) .ag-floating-top .ag-row {
473473
background-color: var(--vscode-list-inactiveSelectionBackground, #37373d) !important;
474474
}
475+
476+
/* ── Column Color Mode ──────────────────────────────────────────────────────
477+
Toggled by the toolbar "Color columns" button (features/color-mode.ts), which
478+
adds `cm-on` to #grid-container and injects one `--cm-h` hue per column. Each
479+
column cell is tinted with a translucent overlay so the theme's own foreground
480+
text keeps its contrast and the tint reads as "this column's color".
481+
482+
Why these rules are NOT !important: every existing highlight — range selection,
483+
find match/active, duplicate rows, the focused cell, the frozen-row band — is
484+
!important and therefore wins the cascade over this normal-weight tint, so all
485+
of them keep painting clearly on top. The tint is also translucent (mixed with
486+
`transparent`), so the real row background (striping, hover, the frozen-row
487+
tint) shows through underneath. The '#' row-number gutter (col-id "row-index")
488+
is never matched, only data columns (col-id "col_*").
489+
490+
Lightness (--cm-l), chroma (--cm-c) and alpha adapt to the active theme so the
491+
color is visible without fighting the text on light, dark or high-contrast
492+
themes. Hue varies per column; L/C/alpha do not. color-mix()/oklch() are
493+
supported by VS Code's Chromium; on a very old build the declaration is simply
494+
ignored and the toggle is a harmless no-op. */
495+
body.vscode-light #grid-container.cm-on {
496+
--cm-l: 0.55; --cm-c: 0.15; --cm-body-a: 15%; --cm-head-a: 30%;
497+
}
498+
body.vscode-dark #grid-container.cm-on,
499+
body.vscode-high-contrast #grid-container.cm-on {
500+
--cm-l: 0.74; --cm-c: 0.15; --cm-body-a: 20%; --cm-head-a: 36%;
501+
}
502+
/* High contrast: keep tints faint — HC users expect minimal background noise. */
503+
body.vscode-high-contrast #grid-container.cm-on {
504+
--cm-body-a: 10%; --cm-head-a: 20%;
505+
}
506+
#grid-container.cm-on .ag-cell[col-id^="col_"] {
507+
background-color: color-mix(in oklab, oklch(var(--cm-l) var(--cm-c) var(--cm-h)) var(--cm-body-a), transparent);
508+
}
509+
#grid-container.cm-on .ag-header-cell[col-id^="col_"] {
510+
background-color: color-mix(in oklab, oklch(var(--cm-l) var(--cm-c) var(--cm-h)) var(--cm-head-a), transparent);
511+
}
475512
/* Frozen (pinned-left) column header: a 📌 marker before the column name,
476513
mirroring the frozen-row pin. The always-pinned '#' gutter (.row-index-header)
477514
is excluded so only user-frozen columns get the marker. */

src/csvEditorProvider.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -177,6 +177,7 @@ export class CsvEditorProvider implements vscode.CustomEditorProvider<CsvDocumen
177177

178178
const fileName = path.basename(document.uri.fsPath);
179179
const zoomIndex = this.context.globalState.get<number>('csvGridEditor.zoomIndex', 4);
180+
const colorMode = this.context.globalState.get<boolean>('csvGridEditor.colorMode', false);
180181

181182
webviewPanel.webview.html = getWebviewContent(
182183
webviewPanel.webview,
@@ -188,7 +189,8 @@ export class CsvEditorProvider implements vscode.CustomEditorProvider<CsvDocumen
188189
fileName,
189190
document.isChunked,
190191
process.platform === 'darwin',
191-
zoomIndex
192+
zoomIndex,
193+
colorMode
192194
);
193195

194196
// F3: File System Watcher — auto-reload on external changes (non-preview only)
@@ -244,6 +246,9 @@ export class CsvEditorProvider implements vscode.CustomEditorProvider<CsvDocumen
244246
} else if (msg.type === 'zoomChanged') {
245247
this.context.globalState.update('csvGridEditor.zoomIndex', msg.zoomIndex);
246248

249+
} else if (msg.type === 'colorModeChanged') {
250+
this.context.globalState.update('csvGridEditor.colorMode', msg.colorMode);
251+
247252
} else if (msg.type === 'edit' && !document.isPreview) {
248253
document.content = msg.text;
249254
this._onDidChangeCustomDocument.fire({ document });

src/webview.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,8 @@ export function getWebviewContent(
1010
fileName: string = '',
1111
isChunked: boolean = false,
1212
isMac: boolean = false,
13-
zoomIndex: number = 4
13+
zoomIndex: number = 4,
14+
colorMode: boolean = false
1415
): string {
1516
const nonce = getNonce();
1617
const mod = isMac ? '⌘' : 'Ctrl+';
@@ -113,6 +114,7 @@ export function getWebviewContent(
113114
<div class="separator"></div>
114115
<button id="btn-profile" title="Column Profile"><i class="codicon codicon-graph"></i></button>
115116
<button id="btn-columns" title="Show / hide columns"><i class="codicon codicon-checklist"></i></button>
117+
<button id="btn-colormode" title="Color columns — distinct, theme-aware tint per column"><i class="codicon codicon-symbol-color"></i></button>
116118
<div class="separator"></div>
117119
<button id="btn-find-replace" title="Find &amp; Replace (${mod}F)"><i class="codicon codicon-search"></i></button>
118120
<div class="separator"></div>
@@ -266,6 +268,7 @@ export function getWebviewContent(
266268
const FILENAME = '${fileName.replace(/'/g, "\\'")}';
267269
const IS_CHUNKED = ${isChunked ? 'true' : 'false'};
268270
const INITIAL_ZOOM_INDEX = ${zoomIndex};
271+
const INITIAL_COLOR_MODE = ${colorMode ? 'true' : 'false'};
269272
</script>
270273
271274
<!-- Bundled webview logic -->

src/webview/features/color-mode.ts

Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
import { state, getNumCols } from '../state';
2+
3+
/**
4+
* Column color mode. A toolbar toggle that gives every data column a distinct,
5+
* theme-adaptive background tint (header stronger, body fainter) so columns are
6+
* easy to tell apart, similar to Rainbow CSV but readable on any VS Code theme.
7+
*
8+
* How the colors work (see also media/webview.css → "Column Color Mode"):
9+
* - Each column gets one hue via golden-angle rotation (137.5°), which keeps
10+
* adjacent columns far apart on the wheel for any column count, even after a
11+
* column is inserted or deleted. Only the per-column hue is injected here as
12+
* a CSS variable (--cm-h) on that column's cells via their stable AG Grid
13+
* `col-id`; the lightness, chroma and alpha live in CSS and adapt to the
14+
* active theme (light / dark / high-contrast).
15+
* - The tint is a translucent overlay (color-mix in OKLCH) painted over the
16+
* real row background, so the theme's own foreground text keeps its contrast
17+
* and frozen rows, selection, find matches and duplicate highlights — all of
18+
* which are !important and higher in the cascade — keep painting clearly on
19+
* top. The row-number gutter (col-id "row-index") is never tinted.
20+
*
21+
* The toggle is persisted globally (VS Code globalState) exactly like zoom, so
22+
* it is remembered across every CSV file and every session.
23+
*/
24+
25+
const HUE_STYLE_ID = 'cm-col-hues';
26+
// 360 * (1 - 1/phi). Irrational vs 360°, so successive hues land in the largest
27+
// remaining gap and never visibly repeat.
28+
const GOLDEN_ANGLE = 137.50776405003785;
29+
// Start offset so column 0 isn't pure red.
30+
const START_HUE = 25;
31+
32+
function persistColorMode(): void {
33+
vscodeApi.postMessage({ type: 'colorModeChanged', colorMode: state.colorMode });
34+
}
35+
36+
function updateButton(): void {
37+
document.getElementById('btn-colormode')?.classList.toggle('btn-active', state.colorMode);
38+
}
39+
40+
// Toggles the `cm-on` class on the grid container (which gates all tint CSS) and,
41+
// when on, (re)generates the per-column hue rules for the current column count.
42+
// Safe to call before the grid has data (numCols 0 → empty rule set, class only).
43+
// Called on setup, on toggle, and at the end of every buildGrid (column add/delete,
44+
// delimiter change, paging) so freshly built columns pick up their hue.
45+
export function applyColorMode(): void {
46+
const container = document.getElementById('grid-container');
47+
if (!container) return;
48+
container.classList.toggle('cm-on', state.colorMode);
49+
50+
let styleEl = document.getElementById(HUE_STYLE_ID) as HTMLStyleElement | null;
51+
if (!state.colorMode) {
52+
styleEl?.remove();
53+
return;
54+
}
55+
if (!styleEl) {
56+
styleEl = document.createElement('style');
57+
styleEl.id = HUE_STYLE_ID;
58+
document.head.appendChild(styleEl);
59+
}
60+
const numCols = getNumCols(state.data);
61+
let css = '';
62+
for (let c = 0; c < numCols; c++) {
63+
const hue = ((START_HUE + c * GOLDEN_ANGLE) % 360).toFixed(2);
64+
css += `#grid-container.cm-on [col-id="col_${c}"]{--cm-h:${hue};}`;
65+
}
66+
styleEl.textContent = css;
67+
}
68+
69+
function toggleColorMode(): void {
70+
state.colorMode = !state.colorMode;
71+
updateButton();
72+
applyColorMode();
73+
persistColorMode();
74+
}
75+
76+
export function setupColorMode(): void {
77+
state.colorMode = !!INITIAL_COLOR_MODE;
78+
updateButton();
79+
applyColorMode();
80+
document.getElementById('btn-colormode')?.addEventListener('click', toggleColorMode);
81+
}

src/webview/features/theme.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,12 @@ export function isDarkTheme(): boolean {
1313
export function applyGridTheme(): void {
1414
const container = document.getElementById('grid-container');
1515
if (!container) return;
16-
container.className = isDarkTheme() ? 'ag-theme-alpine-dark' : 'ag-theme-alpine';
16+
// Swap only the AG Grid theme class. Toggling via classList (rather than
17+
// overwriting className) preserves other state classes on the container —
18+
// notably `cm-on` from the column color mode — so switching VS Code theme,
19+
// or a buildGrid rebuild, doesn't silently drop the coloring.
20+
container.classList.remove('ag-theme-alpine', 'ag-theme-alpine-dark');
21+
container.classList.add(isDarkTheme() ? 'ag-theme-alpine-dark' : 'ag-theme-alpine');
1722
}
1823

1924
export function setupTheme(): void {

src/webview/globals.d.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ declare const DELIMITER: string;
66
declare const FILENAME: string;
77
declare const IS_CHUNKED: boolean;
88
declare const INITIAL_ZOOM_INDEX: number;
9+
declare const INITIAL_COLOR_MODE: boolean;
910

1011
declare const agGrid: {
1112
createGrid(container: HTMLElement, options: unknown): any;

src/webview/grid/builder.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import { getFindCellClassRules } from '../features/find-replace';
99
import { attachHeaderContextMenus } from '../features/freeze-columns';
1010
import { applyZoom } from '../features/zoom';
1111
import { applyGridTheme } from '../features/theme';
12+
import { applyColorMode } from '../features/color-mode';
1213
import {
1314
getRangeCellClassRules,
1415
onCellMouseDownHandler,
@@ -336,6 +337,10 @@ export function buildGrid(): void {
336337

337338
state.gridApi = agGrid.createGrid(container, gridOptions);
338339
updateButtons();
340+
// Re-apply column color mode for the freshly built columns. buildGrid rebuilds
341+
// the column set (insert/delete, delimiter change, paging), so the per-column
342+
// hue rules must be regenerated to match the current column count.
343+
applyColorMode();
339344

340345
// Double-click on resize handle → auto-size that column. Wired once: the
341346
// container survives rebuilds, so re-adding would accumulate listeners.

src/webview/grid/refresh.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { state, getNumCols } from '../state';
2+
import { applyColorMode } from '../features/color-mode';
23

34
// Splits a freshly-built rowData array into the scrollable body and the frozen
45
// reference rows (AG Grid renders the latter in a fixed pinned-top band). Frozen
@@ -81,6 +82,10 @@ export function refreshGrid(): void {
8182
// refreshGrid only swaps rowData, so the row/column counters in the toolbar
8283
// and status bar would otherwise go stale after a delete/insert/paste/undo.
8384
updateCountsDisplay();
85+
// Keep the per-column color hues in sync with the live column count. Undo/redo
86+
// of a column insert/delete reaches here (not buildGrid), so the hue rules must
87+
// be regenerated for the current numCols, not left at the pre-undo count.
88+
applyColorMode();
8489
}
8590

8691
// Recomputes the "<n> rows × <n> columns" toolbar text and the "<n> records"

src/webview/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import { setupPaste } from './features/paste';
1515
import { setupRangeSelect } from './features/range-select';
1616
import { setupRenameColumn } from './features/rename-column';
1717
import { setupColumnChooser } from './features/column-chooser';
18+
import { setupColorMode } from './features/color-mode';
1819
import { setupKeyboard } from './keyboard';
1920
import { setupMessaging } from './messaging';
2021

@@ -35,6 +36,7 @@ setupPaste();
3536
setupRangeSelect();
3637
setupRenameColumn();
3738
setupColumnChooser();
39+
setupColorMode();
3840
setupKeyboard();
3941
setupMessaging();
4042

src/webview/state.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,14 @@ export const state = {
1414
ZOOM_STEPS: [60, 70, 80, 90, 100, 110, 125, 150, 175, 200],
1515
zoomIndex: 4,
1616
isAutoFitted: false,
17+
18+
// Column color mode — when on, every data column gets a distinct, theme-adaptive
19+
// background tint so columns are easier to tell apart. Persisted globally via
20+
// VS Code globalState (csvGridEditor.colorMode), exactly like zoomIndex, so the
21+
// toggle is remembered across every CSV file and every session. The actual
22+
// colors are pure CSS (features/color-mode.ts + media/webview.css). In-memory
23+
// mirror of the persisted flag.
24+
colorMode: false,
1725
autoFitCache: null as any,
1826
autoFitCacheZoom: -1,
1927

0 commit comments

Comments
 (0)