From c6f8ed43fd729ee1128d1ee271c8d9e9b95cda70 Mon Sep 17 00:00:00 2001 From: EthanHealy01 Date: Mon, 13 Jul 2026 19:09:26 +0100 Subject: [PATCH] refactor(ui): extract shared ColorGridPicker for the accent picker Per review on #7009: move the accent swatch grid out of GeneralSection into a reusable core/ui/ColorGridPicker (swatch trigger + popover + radiogroup grid with an optional default/unset icon cell + a footer slot). GeneralSection now renders and passes the presets, default option and the custom-colour ColorInput as the footer. Removes the three raw- - - -
- {/* Default — an icon chip for the neutral theme (surfaces untinted, blue buttons); stores the sentinel, a separate state from the colour swatches. */} - {/* eslint-disable-next-line no-restricted-syntax -- an icon chip, not a text button; the shared Button's variants don't fit */} - - {THEME_ACCENT_PRESETS.map((color) => { - const selected = value.toLowerCase() === color.toLowerCase(); - return ( - // eslint-disable-next-line no-restricted-syntax -- a colour swatch, not a text button; the shared Button's variants don't fit -
- {/* Custom colour — below the presets; our ColorInput, clamped off white/grey/black. */} -
+ defaultOption={{ + // Default is not a colour — an icon chip signalling the neutral theme + // (surfaces untinted, blue buttons); stores the sentinel. + value: DEFAULT_ACCENT, + icon: "star-rounded", + label: t("settings.general.themeAccentDefault", "Default"), + hint: t( + "settings.general.themeAccentDefaultHint", + "Default theme (recommended)", + ), + }} + footer={ + <> {t( "settings.general.themeAccentCustomHint", @@ -276,9 +141,9 @@ function AccentSwatchDropdown({ )} popoverProps={{ withinPortal: false }} /> -
-
- + + } + /> ); } diff --git a/frontend/editor/src/core/ui/ColorGridPicker.tsx b/frontend/editor/src/core/ui/ColorGridPicker.tsx new file mode 100644 index 0000000000..1525a8269f --- /dev/null +++ b/frontend/editor/src/core/ui/ColorGridPicker.tsx @@ -0,0 +1,204 @@ +import { useState, type ReactNode } from "react"; +import { Popover } from "@mantine/core"; +import LocalIcon from "@app/components/shared/LocalIcon"; + +/** + * A compact swatch trigger that opens a grid of selectable colours (a + * radiogroup), with an optional leading "default/unset" cell shown as an icon + * chip rather than a hue, and an optional footer slot (e.g. a custom-colour + * input). Generic DS control — the caller supplies the colours and semantics. + */ +export interface ColorGridDefaultOption { + /** The value stored when this cell is chosen (a sentinel, not a colour). */ + value: string; + /** LocalIcon name shown in the chip. */ + icon: string; + label: string; + /** Native title tooltip. */ + hint?: string; +} + +export interface ColorGridPickerProps { + /** Current value: a colour from `colors`, or `defaultOption.value`. */ + value: string; + onChange: (value: string) => void; + /** Preset colours (any CSS colour string). */ + colors: string[]; + /** Accessible name for the trigger and the radiogroup. */ + ariaLabel: string; + /** Grid columns; defaults to 5. */ + columns?: number; + /** Optional leading "default/unset" cell. */ + defaultOption?: ColorGridDefaultOption; + /** Optional content below the grid (e.g. a custom-colour picker). */ + footer?: ReactNode; + /** Popover z-index, for use inside modals. */ + zIndex?: number; +} + +const SWATCH = "1.75rem"; + +export function ColorGridPicker({ + value, + onChange, + colors, + ariaLabel, + columns = 5, + defaultOption, + footer, + zIndex, +}: ColorGridPickerProps) { + const [opened, setOpened] = useState(false); + const isDefault = !!defaultOption && value === defaultOption.value; + const select = (v: string) => { + onChange(v); + setOpened(false); + }; + return ( + + + + + +
+ {defaultOption && ( + + )} + {colors.map((color) => { + const selected = value.toLowerCase() === color.toLowerCase(); + return ( +
+ {footer && ( +
+ {footer} +
+ )} +
+
+ ); +}