From 6b3eec0a532c5a8b400d846c0ad274110cd58cce Mon Sep 17 00:00:00 2001 From: gsxdsm Date: Tue, 30 Jun 2026 10:26:23 -0700 Subject: [PATCH] FN-7287: collapse custom shadcn color controls Collapse dashboard shadcn color overrides behind an accessible shared toggle by default. - Add an icon-only expand/collapse affordance with aria state for the shared shadcn color picker. - Move reset and color override inputs into the expanded controls panel with responsive styling. - Cover collapsed and expanded picker behavior across theme selector surfaces. - Add a patch changeset for the dashboard theme control fix. Files changed: .changeset/fn-7287-collapse-shadcn-colors.md | 7 ++ .../dashboard/app/components/ShadcnColorPicker.css | 26 +++++- .../dashboard/app/components/ShadcnColorPicker.tsx | 104 +++++++++++++-------- .../__tests__/ShadcnColorPicker.test.tsx | 48 +++++++++- .../components/__tests__/ThemeDropdown.test.tsx | 11 +++ .../components/__tests__/ThemeSelector.test.tsx | 11 +++ 6 files changed, 163 insertions(+), 44 deletions(-) Fusion-Task-Id: FN-7287 Fusion-Task-Lineage: 315ef23e-07e1-473c-8187-a40d0516960e Co-authored-by: Fusion (runfusion.ai) --- .changeset/fn-7287-collapse-shadcn-colors.md | 7 ++ .../app/components/ShadcnColorPicker.css | 26 ++++- .../app/components/ShadcnColorPicker.tsx | 104 +++++++++++------- .../__tests__/ShadcnColorPicker.test.tsx | 48 +++++++- .../__tests__/ThemeDropdown.test.tsx | 11 ++ .../__tests__/ThemeSelector.test.tsx | 11 ++ 6 files changed, 163 insertions(+), 44 deletions(-) create mode 100644 .changeset/fn-7287-collapse-shadcn-colors.md diff --git a/.changeset/fn-7287-collapse-shadcn-colors.md b/.changeset/fn-7287-collapse-shadcn-colors.md new file mode 100644 index 0000000000..255c23adee --- /dev/null +++ b/.changeset/fn-7287-collapse-shadcn-colors.md @@ -0,0 +1,7 @@ +--- +"@runfusion/fusion": patch +--- + +summary: Collapse custom shadcn color controls by default in dashboard theme surfaces. +category: fix +dev: Adds an accessible shared show/collapse affordance for the custom shadcn color picker. diff --git a/packages/dashboard/app/components/ShadcnColorPicker.css b/packages/dashboard/app/components/ShadcnColorPicker.css index 50afa20b78..6ae5681847 100644 --- a/packages/dashboard/app/components/ShadcnColorPicker.css +++ b/packages/dashboard/app/components/ShadcnColorPicker.css @@ -14,6 +14,10 @@ gap: var(--space-md); } +.shadcn-color-picker-heading { + min-width: 0; +} + .shadcn-color-picker-title { margin: 0; color: var(--text); @@ -27,6 +31,21 @@ font-size: var(--font-size-xs); } +.shadcn-color-picker-toggle { + flex: 0 0 auto; +} + +.shadcn-color-picker-controls-panel { + display: flex; + flex-direction: column; + gap: var(--space-sm); +} + +.shadcn-color-picker-actions { + display: flex; + justify-content: flex-end; +} + .shadcn-color-picker-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(calc(var(--space-2xl) * 7), 1fr)); @@ -85,11 +104,16 @@ @media (max-width: 768px) { .shadcn-color-picker-header, .shadcn-color-picker-row, - .shadcn-color-picker-controls { + .shadcn-color-picker-controls, + .shadcn-color-picker-actions { align-items: stretch; flex-direction: column; } + .shadcn-color-picker-toggle { + align-self: flex-start; + } + .shadcn-color-picker-grid { grid-template-columns: 1fr; } diff --git a/packages/dashboard/app/components/ShadcnColorPicker.tsx b/packages/dashboard/app/components/ShadcnColorPicker.tsx index 6ed4d6b85a..7dec032cfd 100644 --- a/packages/dashboard/app/components/ShadcnColorPicker.tsx +++ b/packages/dashboard/app/components/ShadcnColorPicker.tsx @@ -1,4 +1,5 @@ -import { useMemo } from "react"; +import { useId, useMemo, useState } from "react"; +import { ChevronDown, ChevronUp } from "lucide-react"; import { useTranslation } from "react-i18next"; import { SHADCN_CUSTOM_COLOR_TOKENS, @@ -28,6 +29,9 @@ function toColorInputValue(value: string): string { /* FNXC:Theme 2026-06-20-18:38: The shadcn custom picker is shared by Settings and Command Center; it only edits the sanitized token→hex override map while the parent surfaces decide visibility for shadcn-custom so no other theme receives inline overrides. + +FNXC:Theme 2026-06-30-00:00: +Custom shadcn controls default collapsed to keep dashboard theme surfaces compact while leaving the override editor discoverable through an accessible icon affordance wherever this shared picker renders. */ export function ShadcnColorPicker({ value = {}, @@ -35,16 +39,23 @@ export function ShadcnColorPicker({ resolvedThemeMode = "dark", }: ShadcnColorPickerProps) { const { t } = useTranslation("app"); + const [expanded, setExpanded] = useState(false); + const controlsId = useId(); const sanitizedValue = useMemo(() => sanitizeShadcnCustomColors(value), [value]); const updateToken = (cssVar: string, nextValue: string) => { onChange(sanitizeShadcnCustomColors({ ...sanitizedValue, [cssVar]: nextValue })); }; + const toggleLabel = expanded + ? t("theme.shadcnCustom.collapse", "Collapse custom colors") + : t("theme.shadcnCustom.show", "Show custom colors"); + const ToggleIcon = expanded ? ChevronUp : ChevronDown; + return (
-
+

{t("theme.shadcnCustom.title", "Custom shadcn colors")}

@@ -52,44 +63,61 @@ export function ShadcnColorPicker({ {t("theme.shadcnCustom.description", "Override shadcn design tokens with hex colors. Blank tokens use the theme defaults.")}

-
-
- {SHADCN_CUSTOM_COLOR_TOKENS.map((token) => { - const fallback = getShadcnCustomDefaultValue(token, resolvedThemeMode); - const currentValue = sanitizedValue[token.cssVar] ?? fallback; - const inputId = `shadcn-color-${token.cssVar.replace(/^--/, "").replace(/[^a-z0-9]+/gi, "-")}`; - return ( -
- -
- updateToken(token.cssVar, event.currentTarget.value)} - /> - updateToken(token.cssVar, event.currentTarget.value)} - /> -
-
- ); - })} -
+ {expanded ? ( +
+
+ +
+
+ {SHADCN_CUSTOM_COLOR_TOKENS.map((token) => { + const fallback = getShadcnCustomDefaultValue(token, resolvedThemeMode); + const currentValue = sanitizedValue[token.cssVar] ?? fallback; + const inputId = `shadcn-color-${token.cssVar.replace(/^--/, "").replace(/[^a-z0-9]+/gi, "-")}`; + return ( +
+ +
+ updateToken(token.cssVar, event.currentTarget.value)} + /> + updateToken(token.cssVar, event.currentTarget.value)} + /> +
+
+ ); + })} +
+
+ ) : null}
); } diff --git a/packages/dashboard/app/components/__tests__/ShadcnColorPicker.test.tsx b/packages/dashboard/app/components/__tests__/ShadcnColorPicker.test.tsx index b5922bbd47..3d6ff278bc 100644 --- a/packages/dashboard/app/components/__tests__/ShadcnColorPicker.test.tsx +++ b/packages/dashboard/app/components/__tests__/ShadcnColorPicker.test.tsx @@ -1,30 +1,57 @@ +import { readFileSync } from "node:fs"; import { fireEvent, render, screen, within } from "@testing-library/react"; import { describe, expect, it, vi } from "vitest"; import { ShadcnColorPicker } from "../ShadcnColorPicker"; import { SHADCN_CUSTOM_COLOR_TOKENS } from "../shadcnCustomColors"; +function expandCustomColors() { + const toggle = screen.getByRole("button", { name: "Show custom colors" }); + expect(toggle).toHaveAttribute("aria-expanded", "false"); + expect(toggle).toHaveAttribute("aria-controls"); + fireEvent.click(toggle); + expect(screen.getByRole("button", { name: "Collapse custom colors" })).toHaveAttribute("aria-expanded", "true"); +} + describe("ShadcnColorPicker", () => { - it("renders one color control row per customizable token", () => { + it("is collapsed by default and toggles all custom color controls without dangling shells", () => { render(); expect(screen.getByTestId("shadcn-color-picker")).toBeDefined(); + expect(screen.getByText("Custom shadcn colors")).toBeDefined(); + expect(screen.getByText("Override shadcn design tokens with hex colors. Blank tokens use the theme defaults.")).toBeDefined(); + expect(screen.queryByTestId("shadcn-color-picker-controls")).toBeNull(); + expect(screen.queryByRole("button", { name: "Reset custom colors" })).toBeNull(); + for (const token of SHADCN_CUSTOM_COLOR_TOKENS) { + expect(screen.queryByTestId(`shadcn-color-${token.cssVar}`)).toBeNull(); + } + + expandCustomColors(); + expect(screen.getByTestId("shadcn-color-picker-controls")).toBeDefined(); + expect(screen.getByRole("button", { name: "Reset custom colors" })).toBeDefined(); for (const token of SHADCN_CUSTOM_COLOR_TOKENS) { expect(screen.getByTestId(`shadcn-color-${token.cssVar}`)).toBeDefined(); expect(screen.getByText(token.cssVar)).toBeDefined(); } + + fireEvent.click(screen.getByRole("button", { name: "Collapse custom colors" })); + expect(screen.getByRole("button", { name: "Show custom colors" })).toHaveAttribute("aria-expanded", "false"); + expect(screen.queryByTestId("shadcn-color-picker-controls")).toBeNull(); + expect(screen.queryByRole("button", { name: "Reset custom colors" })).toBeNull(); }); - it("uses light defaults when no override exists", () => { + it("uses light defaults when no override exists after expansion", () => { render(); + expandCustomColors(); const bgRow = screen.getByTestId("shadcn-color---bg"); expect(within(bgRow).getByRole("textbox")).toHaveValue("#ffffff"); }); - it("emits sanitized changes and rejects invalid hex input", () => { + it("emits sanitized changes and rejects invalid hex input after expansion", () => { const onChange = vi.fn(); render(); + expandCustomColors(); const accentRow = screen.getByTestId("shadcn-color---accent"); fireEvent.change(within(accentRow).getByRole("textbox"), { target: { value: "red" } }); expect(onChange).toHaveBeenLastCalledWith({}); @@ -33,18 +60,29 @@ describe("ShadcnColorPicker", () => { expect(onChange).toHaveBeenLastCalledWith({ "--accent": "#FF8800" }); }); - it("normalizes short hex values for the native color input", () => { + it("normalizes short hex values for the native color input after expansion", () => { render(); + expandCustomColors(); const accentRow = screen.getByTestId("shadcn-color---accent"); expect(within(accentRow).getByLabelText("Pick Accent color")).toHaveValue("#ffffff"); }); - it("reset clears all custom color overrides", () => { + it("reset clears all custom color overrides after expansion", () => { const onChange = vi.fn(); render(); + expect(screen.queryByRole("button", { name: "Reset custom colors" })).toBeNull(); + expandCustomColors(); fireEvent.click(screen.getByRole("button", { name: "Reset custom colors" })); expect(onChange).toHaveBeenCalledWith({}); }); + + it("keeps mobile collapsed and expanded layout rules tokenized", () => { + const css = readFileSync("app/components/ShadcnColorPicker.css", "utf8"); + + expect(css).toMatch(/@media \(max-width: 768px\) \{[\s\S]*?\.shadcn-color-picker-toggle \{[\s\S]*?align-self: flex-start;/); + expect(css).toMatch(/\.shadcn-color-picker-controls-panel \{[\s\S]*?gap: var\(--space-sm\);/); + expect(css).not.toMatch(/#[\da-f]{3,8}\b|rgb\(/i); + }); }); diff --git a/packages/dashboard/app/components/__tests__/ThemeDropdown.test.tsx b/packages/dashboard/app/components/__tests__/ThemeDropdown.test.tsx index d04d5c2e0b..4eec126610 100644 --- a/packages/dashboard/app/components/__tests__/ThemeDropdown.test.tsx +++ b/packages/dashboard/app/components/__tests__/ThemeDropdown.test.tsx @@ -77,6 +77,17 @@ describe("ThemeDropdown", () => { />, ); expect(screen.getByTestId("shadcn-color-picker")).toBeDefined(); + const showCustomColors = screen.getByRole("button", { name: "Show custom colors" }); + expect(showCustomColors).toHaveAttribute("aria-expanded", "false"); + expect(screen.queryByTestId("shadcn-color-picker-controls")).toBeNull(); + expect(screen.queryByRole("button", { name: "Reset custom colors" })).toBeNull(); + expect(screen.queryByTestId("shadcn-color---accent")).toBeNull(); + + fireEvent.click(showCustomColors); + expect(screen.getByRole("button", { name: "Collapse custom colors" })).toHaveAttribute("aria-expanded", "true"); + expect(screen.getByTestId("shadcn-color-picker-controls")).toBeDefined(); + const accentRow = screen.getByTestId("shadcn-color---accent"); + expect(within(accentRow).getByRole("textbox")).toHaveValue("#123456"); }); it("renders compact theme mode controls when mode props are supplied", () => { diff --git a/packages/dashboard/app/components/__tests__/ThemeSelector.test.tsx b/packages/dashboard/app/components/__tests__/ThemeSelector.test.tsx index 18aebd0d03..32d640b810 100644 --- a/packages/dashboard/app/components/__tests__/ThemeSelector.test.tsx +++ b/packages/dashboard/app/components/__tests__/ThemeSelector.test.tsx @@ -644,6 +644,17 @@ describe("ThemeSelector", () => { /> ); expect(screen.getByTestId("shadcn-color-picker")).toBeDefined(); + const showCustomColors = screen.getByRole("button", { name: "Show custom colors" }); + expect(showCustomColors).toHaveAttribute("aria-expanded", "false"); + expect(screen.queryByTestId("shadcn-color-picker-controls")).toBeNull(); + expect(screen.queryByRole("button", { name: "Reset custom colors" })).toBeNull(); + expect(screen.queryByTestId("shadcn-color---accent")).toBeNull(); + + fireEvent.click(showCustomColors); + expect(screen.getByRole("button", { name: "Collapse custom colors" })).toHaveAttribute("aria-expanded", "true"); + expect(screen.getByTestId("shadcn-color-picker-controls")).toBeDefined(); + const accentRow = screen.getByTestId("shadcn-color---accent"); + expect(within(accentRow).getByRole("textbox")).toHaveValue("#123456"); expect(screen.getByLabelText("Shadcn Custom theme").getAttribute("aria-pressed")).toBe("true"); });