From 7cc5662a1c0c76776b9e102e73bd33711cfe7cc3 Mon Sep 17 00:00:00 2001 From: Brad Harris Date: Sat, 15 Aug 2026 03:06:38 -0600 Subject: [PATCH] Consolidate the icon-color palette onto one shared module MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The eight-colour brand palette existed in three places, with the hexes duplicated a fourth time: 1. apps/server/src/server/static-theme.ts — VALID_ICON_COLORS id tuple 2. apps/web/src/hooks/use-icon-color.ts — ICON_COLORS id tuple, plus ICON_COLOR_OPTIONS re-listing the same hexes as `swatch` 3. scripts/generate-icon-colors.ts — the same ids with primary/dark hexes Adding a colour meant editing three files in two TS programs. All three now derive from a new dependency-free apps/server/src/shared/icon-colors.ts (the #844/#849 shared-module pattern), which holds the ids and both hex channels in one ordered table. Behaviour is unchanged: the tuple order (teal, blue, purple, red, orange, amber, pink, cyan) is preserved exactly, so the Appearance settings swatch order and the server's `iconColor must be one of: ...` error string are byte-identical. Re-running the icon generator produces zero git diff. Co-Authored-By: Claude Opus 5 (1M context) --- apps/server/src/server/static-theme.ts | 15 ++------ apps/server/src/shared/icon-colors.ts | 29 +++++++++++++++ apps/web/src/hooks/use-icon-color.ts | 51 +++++++++++++++----------- scripts/generate-icon-colors.ts | 12 +----- 4 files changed, 65 insertions(+), 42 deletions(-) create mode 100644 apps/server/src/shared/icon-colors.ts diff --git a/apps/server/src/server/static-theme.ts b/apps/server/src/server/static-theme.ts index fb068e1bb..e43d3b819 100644 --- a/apps/server/src/server/static-theme.ts +++ b/apps/server/src/server/static-theme.ts @@ -1,21 +1,14 @@ +import { ICON_COLOR_IDS, type IconColorId } from "../shared/icon-colors.js"; + type EmbeddedStaticAsset = { routePath: string; contentType: string; base64: string; }; -export const VALID_ICON_COLORS = [ - "teal", - "blue", - "purple", - "red", - "orange", - "amber", - "pink", - "cyan", -] as const; +export const VALID_ICON_COLORS = ICON_COLOR_IDS; -export type IconColor = (typeof VALID_ICON_COLORS)[number]; +export type IconColor = IconColorId; const DEFAULT_ICON_COLOR: IconColor = "teal"; diff --git a/apps/server/src/shared/icon-colors.ts b/apps/server/src/shared/icon-colors.ts new file mode 100644 index 000000000..7bcabcfd3 --- /dev/null +++ b/apps/server/src/shared/icon-colors.ts @@ -0,0 +1,29 @@ +/** + * Brand icon colour palette. + * + * Single source of truth shared by the server (settings validation and the + * static theme runtime), the web client (Appearance settings swatches — see + * apps/web/src/hooks/use-icon-color.ts) and scripts/generate-icon-colors.ts, + * which renders the SVG/PNG variants under apps/web/public/icons/. Web and the + * script import this module directly across the workspace boundary, so keep it + * dependency-free: no node imports, no browser globals. + * + * Order matters — it drives the swatch order rendered in Appearance settings. + */ + +export const ICON_COLOR_PALETTE = [ + { id: "teal", primary: "#14B981", dark: "#0D8358" }, + { id: "blue", primary: "#3B82F6", dark: "#2563EB" }, + { id: "purple", primary: "#8B5CF6", dark: "#6D28D9" }, + { id: "red", primary: "#EF4444", dark: "#B91C1C" }, + { id: "orange", primary: "#F97316", dark: "#C2410C" }, + { id: "amber", primary: "#F59E0B", dark: "#B45309" }, + { id: "pink", primary: "#EC4899", dark: "#BE185D" }, + { id: "cyan", primary: "#06B6D4", dark: "#0E7490" }, +] as const; + +export type IconColorId = (typeof ICON_COLOR_PALETTE)[number]["id"]; + +export const ICON_COLOR_IDS: readonly IconColorId[] = ICON_COLOR_PALETTE.map( + (color) => color.id +); diff --git a/apps/web/src/hooks/use-icon-color.ts b/apps/web/src/hooks/use-icon-color.ts index 6194832d9..182ffcb15 100644 --- a/apps/web/src/hooks/use-icon-color.ts +++ b/apps/web/src/hooks/use-icon-color.ts @@ -1,17 +1,29 @@ import { useCallback } from "react"; import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query"; -export const ICON_COLORS = [ - "teal", - "blue", - "purple", - "red", - "orange", - "amber", - "pink", - "cyan", -] as const; -export type IconColorId = (typeof ICON_COLORS)[number]; +/** + * Palette ids and swatch hexes come from the shared server module so the web + * client always matches what the server accepts and what the icon generator + * renders (see apps/server/src/shared/icon-colors.ts). Display labels are + * web-only and live here. + */ +import { + ICON_COLOR_PALETTE, + type IconColorId, +} from "../../../server/src/shared/icon-colors"; + +export type { IconColorId }; + +const ICON_COLOR_LABELS: Record = { + teal: "Teal", + blue: "Blue", + purple: "Purple", + red: "Red", + orange: "Orange", + amber: "Amber", + pink: "Pink", + cyan: "Cyan", +}; export type IconColorDefinition = { id: IconColorId; @@ -19,16 +31,13 @@ export type IconColorDefinition = { swatch: string; }; -export const ICON_COLOR_OPTIONS: IconColorDefinition[] = [ - { id: "teal", label: "Teal", swatch: "#14B981" }, - { id: "blue", label: "Blue", swatch: "#3B82F6" }, - { id: "purple", label: "Purple", swatch: "#8B5CF6" }, - { id: "red", label: "Red", swatch: "#EF4444" }, - { id: "orange", label: "Orange", swatch: "#F97316" }, - { id: "amber", label: "Amber", swatch: "#F59E0B" }, - { id: "pink", label: "Pink", swatch: "#EC4899" }, - { id: "cyan", label: "Cyan", swatch: "#06B6D4" }, -]; +export const ICON_COLOR_OPTIONS: IconColorDefinition[] = ICON_COLOR_PALETTE.map( + (color) => ({ + id: color.id, + label: ICON_COLOR_LABELS[color.id], + swatch: color.primary, + }) +); export function useIconColor(): { iconColor: IconColorId; diff --git a/scripts/generate-icon-colors.ts b/scripts/generate-icon-colors.ts index 84d7501c7..e5e56e303 100644 --- a/scripts/generate-icon-colors.ts +++ b/scripts/generate-icon-colors.ts @@ -11,6 +11,7 @@ import fs from "node:fs"; import path from "node:path"; import sharp from "sharp"; +import { ICON_COLOR_PALETTE } from "../apps/server/src/shared/icon-colors.js"; const REPO_ROOT = path.resolve(import.meta.dirname, ".."); const PUBLIC_DIR = path.join(REPO_ROOT, "apps/web/public"); @@ -22,16 +23,7 @@ const ICONS_DIR = path.join(PUBLIC_DIR, "icons"); const ORIGINAL_PRIMARY = "rgb(7.843137%, 72.54902%, 50.588235%)"; const ORIGINAL_DARK = "rgb(5.098039%, 51.372549%, 34.509804%)"; -const COLORS = [ - { id: "teal", primary: "#14B981", dark: "#0D8358" }, - { id: "blue", primary: "#3B82F6", dark: "#2563EB" }, - { id: "purple", primary: "#8B5CF6", dark: "#6D28D9" }, - { id: "red", primary: "#EF4444", dark: "#B91C1C" }, - { id: "orange", primary: "#F97316", dark: "#C2410C" }, - { id: "amber", primary: "#F59E0B", dark: "#B45309" }, - { id: "pink", primary: "#EC4899", dark: "#BE185D" }, - { id: "cyan", primary: "#06B6D4", dark: "#0E7490" }, -] as const; +const COLORS = ICON_COLOR_PALETTE; const SIZES = [ { name: "pwa-512.png", size: 512 },