diff --git a/frontend/editor/src/core/components/shared/config/configSections/GeneralSection.tsx b/frontend/editor/src/core/components/shared/config/configSections/GeneralSection.tsx index e418d2468d..11465e937d 100644 --- a/frontend/editor/src/core/components/shared/config/configSections/GeneralSection.tsx +++ b/frontend/editor/src/core/components/shared/config/configSections/GeneralSection.tsx @@ -11,11 +11,11 @@ import { Group, Anchor, Badge, - Popover, } from "@mantine/core"; import { Button } from "@app/ui/Button"; import { ActionIcon } from "@app/ui/ActionIcon"; import { ColorInput } from "@app/ui/ColorInput"; +import { ColorGridPicker } from "@app/ui/ColorGridPicker"; import { SegmentedControl } from "@app/ui/SegmentedControl"; import { clampAccentChoice } from "@app/utils/customPrimary"; import { useTranslation } from "react-i18next"; @@ -84,7 +84,7 @@ interface GeneralSectionProps { desktopUpdateMode?: DesktopUpdateModeControl; } -/** Accent-colour picker: a swatch trigger opening a 3×5 grid — a "Default" icon cell + 14 accents — plus a "Custom" picker below. */ +/** Accent-colour picker: a swatch grid (a "Default" icon cell + 14 accents) with a "Custom" picker below, via the shared ColorGridPicker. */ function AccentSwatchDropdown({ value, onChange, @@ -95,8 +95,6 @@ function AccentSwatchDropdown({ ariaLabel: string; }) { const { t } = useTranslation(); - const [opened, setOpened] = useState(false); - const isDefault = value === DEFAULT_ACCENT; // Resolve the "default" sentinel to the blue it maps to so the ColorInput shows a real colour. const asHex = (v: string) => v === DEFAULT_ACCENT ? DEFAULT_ACCENT_COLOR : v; @@ -104,158 +102,25 @@ function AccentSwatchDropdown({ const [draft, setDraft] = useState(() => asHex(value)); useEffect(() => setDraft(asHex(value)), [value]); return ( - - - {/* eslint-disable-next-line no-restricted-syntax -- swatch-trigger control, not a text button; the shared Button's variants/padding don't fit */} - - - -
- {/* 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} +
+ )} +
+
+ ); +}