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

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 8 additions & 8 deletions apps/eds-color-palette-generator/ABOUT_PAGE.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ The page explains:

### 2. Interactive Bell Curve Visualization

The `BellCurveVisualization` component (`src/components/BellCurveVisualization.tsx`) provides:
The `BellCurveVisualization` component (`src/components/docs/BellCurveVisualization.tsx`) provides:

- Visual representation of the Gaussian function
- Interactive controls for mean and standard deviation
Expand All @@ -33,7 +33,7 @@ The `BellCurveVisualization` component (`src/components/BellCurveVisualization.t

### 3. Interactive Chroma Distribution Demo

The `ChromaDistributionDemo` component (`src/components/ChromaDistributionDemo.tsx`) provides:
The `ChromaDistributionDemo` component (`src/components/docs/ChromaDistributionDemo.tsx`) provides:

- Color picker to select any base color
- Interactive controls for Gaussian parameters (mean and standard deviation)
Expand All @@ -43,7 +43,7 @@ The `ChromaDistributionDemo` component (`src/components/ChromaDistributionDemo.t

### 4. Color Step Pairings and Contrast Requirements

The `ContrastRequirementsTable` component (`src/components/ContrastRequirementsTable.tsx`) provides:
The `ContrastRequirementsTable` component (`src/components/docs/ContrastRequirementsTable.tsx`) provides:

- Comprehensive list of all color steps with contrast requirements
- Direct reference to configuration file for always up-to-date information
Expand All @@ -65,7 +65,7 @@ The `ContrastRequirementsTable` component (`src/components/ContrastRequirementsT

### BellCurveVisualization

**File:** `src/components/BellCurveVisualization.tsx`
**File:** `src/components/docs/BellCurveVisualization.tsx`

A client-side component that visualizes the Gaussian function used to calculate chroma multipliers.

Expand All @@ -84,7 +84,7 @@ A client-side component that visualizes the Gaussian function used to calculate

### ChromaDistributionDemo

**File:** `src/components/ChromaDistributionDemo.tsx`
**File:** `src/components/docs/ChromaDistributionDemo.tsx`

A client-side component that demonstrates how chroma varies across a color scale.

Expand All @@ -104,7 +104,7 @@ A client-side component that demonstrates how chroma varies across a color scale

### ContrastRequirementsTable

**File:** `src/components/ContrastRequirementsTable.tsx`
**File:** `src/components/docs/ContrastRequirementsTable.tsx`

A client-side component that displays color step pairings and their contrast requirements, directly referencing the configuration file.

Expand All @@ -122,10 +122,10 @@ A client-side component that displays color step pairings and their contrast req

The About page is accessible from the main page via:

- An "About" link in the header (added to `HeaderPanel` component)
- An "About" link in the header (added to the `HeaderPanel` component, now at `/old`)
- Direct URL navigation to `/about`

A "Back to generator" link is provided on the About page for easy navigation back to the main tool.
A "Back to generator" link is provided on the About page, pointing at the archived Gaussian generator at `/old` (the tool this page documents).

## Technical Implementation

Expand Down
13 changes: 10 additions & 3 deletions apps/eds-color-palette-generator/README.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,13 @@
# EDS Color Palette Generator

An accessible color palette generator for the Equinor Design System. This tool creates harmonious color scales using Gaussian distribution and the OKLCH color space, ensuring consistent, accessible colors across different lightness levels.
Internal tooling for building accessible colour themes and palettes for the Equinor Design System.

The project hosts two tools in one Next.js App Router app:

- **Theme Builder** (`/`, the primary tool) — build accessible colour themes and palettes, preview them on example components, and check contrast. Palette state is shareable via the URL.
- **Colour Palette Generator** (`/old`, archived) — the original Gaussian colour-scale generator. It creates harmonious colour scales using Gaussian distribution and the OKLCH colour space, ensuring consistent, accessible colours across different lightness levels. Still fully functional but no longer the default entry point.

> The `/themebuilder` route now redirects to `/`.

## Features

Expand All @@ -24,9 +31,9 @@ First, run the development server:
pnpm dev
```

Open [http://localhost:3000](http://localhost:3000) with your browser to see the color palette generator.
Open [http://localhost:3000](http://localhost:3000) with your browser to see the Theme Builder (the primary tool).

To learn how the generator works internally, visit the About page at [http://localhost:3000/about](http://localhost:3000/about).
The archived Gaussian colour-scale generator is available at [http://localhost:3000/old](http://localhost:3000/old). To learn how that generator works internally, visit the About page at [http://localhost:3000/about](http://localhost:3000/about).

### CLI Tool

Expand Down
6 changes: 6 additions & 0 deletions apps/eds-color-palette-generator/next.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,12 @@ import type { NextConfig } from 'next'
const nextConfig: NextConfig = {
/* config options here */
devIndicators: false,
async redirects() {
// The theme builder used to live at /themebuilder and its Share button
// still emits /themebuilder?... links. Next.js preserves the query string.
// Temporary (307) while the app is still evolving.
return [{ source: '/themebuilder', destination: '/', permanent: false }]
},
}

export default nextConfig
29 changes: 14 additions & 15 deletions apps/eds-color-palette-generator/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -29,28 +29,27 @@
"@equinor/eds-tokens": "workspace:^",
"@equinor/eds-utils": "workspace:^",
"colorjs.io": "^0.6.1",
"lucide-react": "^1.21.0",
"next": "16.2.9",
"react": "^19.2.6",
"react-dom": "^19.2.6"
"lucide-react": "^1.12.0",
"next": "16.2.4",
"react": "^19.2.4",
"react-dom": "^19.2.4"
},
"devDependencies": {
"@eslint/eslintrc": "^3.3.5",
"@microsoft/api-extractor": "^7.58.9",
"@next/eslint-plugin-next": "16.2.9",
"@playwright/test": "^1.61.1",
"@tailwindcss/postcss": "^4.3.1",
"@next/eslint-plugin-next": "16.2.4",
"@playwright/test": "^1.59.1",
"@tailwindcss/postcss": "^4.2.4",
"@types/node": "^25.5.2",
"@types/react": "^19.2.15",
"@types/react": "^19.2.14",
"@types/react-dom": "^19.2.3",
"dotenv": "^17.4.0",
"eslint": "^9.38.0",
"eslint-config-next": "16.2.9",
"tailwindcss": "^4.3.1",
"tsx": "^4.22.4",
"eslint-config-next": "16.2.4",
"tailwindcss": "^4.2.4",
"tsx": "^4.21.0",
"typescript": "^5.9.3",
"vite": "^8.1.0",
"vite-plugin-dts": "^5.0.3",
"vitest": "^4.1.9"
"vite": "^8.0.10",
"vite-plugin-dts": "^4.5.4",
"vitest": "^4.1.5"
}
}
136 changes: 136 additions & 0 deletions apps/eds-color-palette-generator/plan/fix-lint-failures.prompt.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,136 @@
# Prompt: Fix the lint failures in the Color Palette Generator app

> Paste this into a fresh Claude Code session (or hand it to an agent) run from the
> repo root. It is scoped entirely to `apps/eds-color-palette-generator`.
> Read `AGENTS.md` and `.claude/CLAUDE.md` first — repo conventions apply.

---

## Role

You are fixing ESLint failures in the Next.js app at `apps/eds-color-palette-generator`.
`pnpm --filter @equinor/eds-color-palette-generator lint` currently reports **6 errors +
1 warning**. Type-check (`types`) and unit tests (`test:run`) are already green, and the
theme-builder-to-root restructure is complete. Your job is to get lint clean **without
changing behaviour** and without regressing types or tests.

## Context (verify before you touch anything)

These failures are **pre-existing** — they were introduced with the theme builder in
commit `a71f92b1` and only surfaced now because that code was promoted to `/`. Two of the
erroring files (`src/app/palette/page.tsx`, `src/components/themebuilder/ComponentPreviewPanel.tsx`)
are unchanged vs `HEAD`, confirming this is not fallout from the restructure.

The rules come from `eslint-config-next/core-web-vitals` (React Compiler / `react-hooks`
recommended-latest under Next 16 + React 19), configured in `eslint.config.mjs`. They are
upstream, not custom — **fix the code, do not disable rules** unless a case is genuinely a
false positive (see the localStorage note below), and then only with a one-line
justification comment.

## The failures (re-run lint to confirm line numbers before editing)

| # | File:line | Rule | Root cause |
| - | --------- | ---- | ---------- |
| 1 | `src/components/themebuilder/ContrastTable.tsx:42` | `react-hooks/rules-of-hooks` | `useMemo` is called **after** the `if (!palette) return null` early-return on line 40 |
| 2 | `src/components/themebuilder/ComponentPreviewPanel.tsx:37` | `react-hooks/rules-of-hooks` | `useState` is called **after** the `if (palettes.length === 0) return null` early-return on line 20 |
| 3 | `src/components/themebuilder/contrast/DataColorChart.tsx:167` | `react-hooks/immutability` | `cumulative += pct` mutates a render-scope variable during `.map()` in JSX |
| 4 | `src/app/page.tsx:72` | react-hooks (setState-in-effect) | effect resets `contrastPaletteIndex` to 0 when the palette list shrinks |
| 5 | `src/app/example/page.tsx:560` | react-hooks (setState-in-effect) | mount effect calls `setCustomPalettes(getSimulationPalettes())` |
| 6 | `src/app/palette/page.tsx:78` | react-hooks (setState-in-effect) | run-once auto-import effect calls `setHasAutoImported(true)` + `setPalettes(...)` |
| 7 | `src/app/palette/page.tsx:3` | `@typescript-eslint/no-unused-vars` (warning) | `useMemo` imported but never used |

## Recommended fixes (confirm each against the actual code; adapt if it drifted)

### 1 & 2 — hooks after an early return (`rules-of-hooks`)

Hooks must run unconditionally, in the same order, on every render. Move every hook call
**above** the early `return null`.

- **`ContrastTable.tsx`** — move the `grid` `useMemo` above the `if (!palette) return null`
guard and make it null-safe, e.g. `useMemo(() => { if (!palette) return []; return FG_INDICES.map(...) }, [palette])`,
then keep `if (!palette) return null` after the hook.
- **`ComponentPreviewPanel.tsx`** — the `accentIdx` `useState` initializer depends on
`neutralIdx` / `accentCandidates`, which are derived after the guard. Preserve the current
default (first non-gray palette) by using a **lazy initializer** — `useState(() => …)` — that
computes the default safely even when `palettes` is empty, then move `if (palettes.length === 0) return null`
to run after the hook. Extract the neutral-finding logic into a module-scope helper (per
repo convention: helpers at module scope) so the initializer and the render body don't
duplicate it.

### 3 — mutation during render (`immutability`)

`DataColorChart.tsx`'s `DonutChart` accumulates `cumulative` while mapping. Precompute the
segments **before** the JSX with a single pass (e.g. build an array of `{ pct, offset }` via
`reduce`/`map` at the top of the component), then render from that array — no reassignment
inside the JSX `.map()`.

### 4 — clamp during render instead of in an effect

`src/app/page.tsx` uses an effect to reset `contrastPaletteIndex` when `palettes` shrinks.
This is the classic "adjusting state during render" case — derive a clamped value during
render and drop the effect:

```ts
const safeContrastIndex =
contrastPaletteIndex >= palettes.length ? 0 : contrastPaletteIndex
```

Use `safeContrastIndex` at every read site. Only keep `setContrastPaletteIndex` for genuine
user-driven changes. Remove the now-dead effect.

### 5 & 6 — setState in a mount effect (client-only hydration)

Both call setState from an effect to hydrate client-only data. **Inspect the data source
first**, because the right fix differs:

- If the source is pure/deterministic and safe during SSR (no `window`/`localStorage`),
convert to a **lazy `useState` initializer** and delete the effect —
`useState(() => getSimulationPalettes())`.
- If it reads `localStorage`/`window` (e.g. `palette/page.tsx`'s auto-import via
`localStorageUtils`, and check whether `getSimulationPalettes` does too), a lazy initializer
would run during SSR and break hydration. Prefer **`useSyncExternalStore`** (server snapshot
returns the default, client snapshot returns the stored value) — the idiomatic SSR-safe
pattern. If that is disproportionate for a one-shot import, keep the effect but add a
narrowly-scoped `// eslint-disable-next-line …` with a one-line comment explaining it is
intentional client-only hydration. Do not blanket-disable the rule for the file.

Whichever you pick, the on-screen result must be identical to today.

### 7 — unused import (warning)

Remove `useMemo` from the import on `src/app/palette/page.tsx:3` (only if fix #6 doesn't
reintroduce a use for it).

## Constraints (non-negotiable)

- Follow `AGENTS.md` / `.claude/CLAUDE.md`. Match existing style (2-space, no semicolons,
single quotes, named exports where applicable, `import type`). The format hook runs
eslint/prettier `--fix` on save; still eyeball the diff.
- **Behaviour-preserving only.** These are lint fixes, not a refactor. Do not change what any
tool renders or how it behaves. If a "fix" would alter visible behaviour, stop and flag it.
- **Ask before any git action** — no commits, branches, pushes, or PRs without explicit
approval. **Never** add AI attribution / "Co-authored-by".
- Keep changes minimal and local to the seven sites above. Don't opportunistically rewrite
surrounding code.

## Verification (all must pass)

```bash
pnpm --filter @equinor/eds-color-palette-generator lint # 0 errors, 0 warnings
pnpm --filter @equinor/eds-color-palette-generator types # clean
pnpm --filter @equinor/eds-color-palette-generator test:run # green (was 126/126)
```

Then drive the app (`pnpm --filter @equinor/eds-color-palette-generator dev`, or the `/run`
skill) and confirm no behaviour changed and no new console errors / hydration warnings on:

- `/` (theme builder) — tab switching, palette add/remove (exercises fix #4), contrast table
(#1), component preview role selector (#2), the data-colour donut (#3)
- `/example` (#5) and `/palette` (#6) — data loads on first paint exactly as before

## Report format

Summarise: (a) each error → the fix applied and why, (b) any case where you chose
`useSyncExternalStore` or a justified disable over a lazy initializer and the reasoning,
(c) verification results (each command's pass/fail), (d) anything you deliberately left
untouched.
Loading