diff --git a/apps/server/src/server/static-theme.ts b/apps/server/src/server/static-theme.ts index fb068e1b..e43d3b81 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 00000000..7bcabcfd --- /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 6194832d..182ffcb1 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 84d7501c..e5e56e30 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 },