From 095069d2aed51b155c6a11e3662c82f14d5d4dcd Mon Sep 17 00:00:00 2001 From: Bruno Fantauzzi Date: Mon, 31 Aug 2026 02:39:27 -0300 Subject: [PATCH] fix button emphasis foreground token --- .changeset/themeable-button-foreground.md | 5 ++ packages/kumo-docs-astro/src/pages/colors.mdx | 4 ++ packages/kumo/ai/USAGE.md | 27 +++++---- .../kumo/scripts/theme-generator/config.ts | 11 ++++ .../theme-generator/generate-css.test.ts | 13 ++++ .../src/components/banner/banner.test.tsx | 2 +- .../components/button/button.browser.test.tsx | 59 +++++++++++++++++++ .../src/components/button/button.test.tsx | 30 ++++++++++ .../kumo/src/components/button/button.tsx | 4 +- packages/kumo/src/styles/theme-kumo.css | 7 +++ 10 files changed, 146 insertions(+), 16 deletions(-) create mode 100644 .changeset/themeable-button-foreground.md create mode 100644 packages/kumo/src/components/button/button.browser.test.tsx diff --git a/.changeset/themeable-button-foreground.md b/.changeset/themeable-button-foreground.md new file mode 100644 index 0000000000..471fb3f11f --- /dev/null +++ b/.changeset/themeable-button-foreground.md @@ -0,0 +1,5 @@ +--- +"@cloudflare/kumo": patch +--- + +Make primary and destructive Button foreground colors themeable through the semantic `text-kumo-button-emphasis` token. diff --git a/packages/kumo-docs-astro/src/pages/colors.mdx b/packages/kumo-docs-astro/src/pages/colors.mdx index 87f7e63115..a23f4c123a 100644 --- a/packages/kumo-docs-astro/src/pages/colors.mdx +++ b/packages/kumo-docs-astro/src/pages/colors.mdx @@ -284,6 +284,10 @@ Use the solid token `bg-kumo-*` for status dots, `fill-kumo-*` for icons, and `b text-kumo-inverse Text intended for use on high-contrast or inverted backgrounds + + text-kumo-button-emphasis + Foreground text and icons on primary and destructive Button emphasis backgrounds + text-kumo-link Link text diff --git a/packages/kumo/ai/USAGE.md b/packages/kumo/ai/USAGE.md index c5f32da522..cbf7f22453 100644 --- a/packages/kumo/ai/USAGE.md +++ b/packages/kumo/ai/USAGE.md @@ -104,19 +104,20 @@ Use the solid token on icons, status dots, and progress fills. Banners and badge ### Text Colors -| Token | Purpose | -| ----------------------- | -------------------------------------------------------------- | -| `text-kumo-default` | Primary body text | -| `text-kumo-strong` | Secondary text with slightly less contrast than default | -| `text-kumo-subtle` | Muted text for descriptions, captions, or secondary labels | -| `text-kumo-inactive` | Disabled or inactive text | -| `text-kumo-placeholder` | Placeholder text in inputs | -| `text-kumo-inverse` | Text intended for use on high-contrast or inverted backgrounds | -| `text-kumo-link` | Link text | -| `text-kumo-info` | Info-colored text | -| `text-kumo-success` | Success-colored text | -| `text-kumo-warning` | Warning-colored text | -| `text-kumo-danger` | Error/destructive text | +| Token | Purpose | +| --------------------------- | -------------------------------------------------------------- | +| `text-kumo-default` | Primary body text | +| `text-kumo-strong` | Secondary text with slightly less contrast than default | +| `text-kumo-subtle` | Muted text for descriptions, captions, or secondary labels | +| `text-kumo-inactive` | Disabled or inactive text | +| `text-kumo-placeholder` | Placeholder text in inputs | +| `text-kumo-inverse` | Text intended for use on high-contrast or inverted backgrounds | +| `text-kumo-button-emphasis` | Foreground text and icons on emphasized Button backgrounds | +| `text-kumo-link` | Link text | +| `text-kumo-info` | Info-colored text | +| `text-kumo-success` | Success-colored text | +| `text-kumo-warning` | Warning-colored text | +| `text-kumo-danger` | Error/destructive text | ### Borders & Rings diff --git a/packages/kumo/scripts/theme-generator/config.ts b/packages/kumo/scripts/theme-generator/config.ts index 80d8954007..decc11f0b4 100644 --- a/packages/kumo/scripts/theme-generator/config.ts +++ b/packages/kumo/scripts/theme-generator/config.ts @@ -39,6 +39,17 @@ export const THEME_CONFIG: ThemeConfig = { }, }, }, + "kumo-button-emphasis": { + newName: "", + description: + "Foreground color for primary and destructive Button emphasis backgrounds", + theme: { + kumo: { + light: "var(--color-white, #fff)", + dark: "var(--color-white, #fff)", + }, + }, + }, "kumo-strong": { newName: "", theme: { diff --git a/packages/kumo/scripts/theme-generator/generate-css.test.ts b/packages/kumo/scripts/theme-generator/generate-css.test.ts index 8a5cdf502c..1fc0f214a8 100644 --- a/packages/kumo/scripts/theme-generator/generate-css.test.ts +++ b/packages/kumo/scripts/theme-generator/generate-css.test.ts @@ -27,4 +27,17 @@ describe("theme css generator", () => { ); expect(css).not.toMatch(/\n\[data-theme="fedramp"\] \{/); }); + + it("emits a themeable Button emphasis foreground with white defaults", () => { + const css = generateKumoThemeCSS(THEME_CONFIG); + + expect(css).toContain(`--text-color-kumo-button-emphasis: light-dark( + var(--color-white, #fff), + var(--color-white, #fff) + );`); + expect(countOccurrences(css, "--text-color-kumo-button-emphasis")).toBe(3); + expect(css).not.toContain( + "--text-color-kumo-button-emphasis: var(--color-white, #fff) !important", + ); + }); }); diff --git a/packages/kumo/src/components/banner/banner.test.tsx b/packages/kumo/src/components/banner/banner.test.tsx index 0faa017a28..041febf052 100644 --- a/packages/kumo/src/components/banner/banner.test.tsx +++ b/packages/kumo/src/components/banner/banner.test.tsx @@ -39,7 +39,7 @@ describe("Banner", () => { const cta = screen.getByTestId("cta"); expect(cta.tagName).toBe("BUTTON"); expect(cta.getAttribute("data-kumo-component")).toBe("Button"); - expect(cta.className).toContain("text-white"); + expect(cta.className).toContain("!text-kumo-button-emphasis"); expect(cta.className).toContain("bg-(--kumo-button-emphasis-bg)"); expect( cta.style.getPropertyValue("--kumo-button-emphasis-gradient-end"), diff --git a/packages/kumo/src/components/button/button.browser.test.tsx b/packages/kumo/src/components/button/button.browser.test.tsx new file mode 100644 index 0000000000..c9a1ea165d --- /dev/null +++ b/packages/kumo/src/components/button/button.browser.test.tsx @@ -0,0 +1,59 @@ +import type { CSSProperties } from "react"; +import { describe, expect, test } from "vite-plus/test"; +import { render } from "vitest-browser-react"; +import { Button } from "./button"; + +const customTheme = { + "--text-color-kumo-button-emphasis": "rgb(17 24 39)", +} as CSSProperties; + +describe("Button emphasis theme", () => { + test("preserves the default white foreground", async () => { + const { getByRole } = await render( + <> + + + , + ); + + for (const button of [ + getByRole("button", { name: "Save" }), + getByRole("button", { name: "Delete" }), + ]) { + await expect.element(button).toBeVisible(); + expect(getComputedStyle(button.element()).color).toBe( + "rgb(255, 255, 255)", + ); + } + }); + + test("uses a custom semantic foreground in enabled, disabled, and loading states", async () => { + const { getByRole } = await render( +
+ + + +
, + ); + + const save = getByRole("button", { name: "Save" }); + const deleteButton = getByRole("button", { name: "Delete" }); + const loading = getByRole("button", { name: "Loading" }); + + await Promise.all([ + expect.element(save).toBeVisible(), + expect.element(deleteButton).toBeVisible(), + expect.element(loading).toBeVisible(), + ]); + + for (const button of [save, deleteButton, loading]) { + expect(getComputedStyle(button.element()).color).toBe("rgb(17, 24, 39)"); + } + expect(getComputedStyle(deleteButton.element()).opacity).toBe("0.5"); + expect(getComputedStyle(loading.element()).opacity).toBe("0.5"); + }); +}); diff --git a/packages/kumo/src/components/button/button.test.tsx b/packages/kumo/src/components/button/button.test.tsx index ed2cf490df..6cdeb7823d 100644 --- a/packages/kumo/src/components/button/button.test.tsx +++ b/packages/kumo/src/components/button/button.test.tsx @@ -192,6 +192,36 @@ describe("Button", () => { expect(className).not.toContain("focus-visible:ring-kumo-brand"); } }); + + it("uses a semantic foreground for emphasized variants", () => { + for (const variant of ["primary", "destructive"] as const) { + const className = buttonVariants({ variant }); + + expect(className).toContain("!text-kumo-button-emphasis"); + expect(className).not.toContain("!text-white"); + expect(className).toContain("disabled:opacity-50"); + } + }); + + it("keeps the semantic emphasis foreground while disabled or loading", () => { + render( + <> + + + , + ); + + for (const button of screen.getAllByRole("button")) { + expect(button.hasAttribute("disabled")).toBe(true); + expect(button.classList.contains("!text-kumo-button-emphasis")).toBe( + true, + ); + } + }); }); describe("RefreshButton", () => { diff --git a/packages/kumo/src/components/button/button.tsx b/packages/kumo/src/components/button/button.tsx index a8e50b53d0..9b3a88ba53 100644 --- a/packages/kumo/src/components/button/button.tsx +++ b/packages/kumo/src/components/button/button.tsx @@ -49,7 +49,7 @@ export const KUMO_BUTTON_VARIANTS = { variant: { primary: { classes: - "relative overflow-hidden bg-(--kumo-button-emphasis-bg) !text-white ring ring-(--kumo-button-emphasis-ring) focus:ring-(--kumo-button-emphasis-ring) focus-visible:ring-(--kumo-button-emphasis-ring) active:ring-(--kumo-button-emphasis-ring) disabled:opacity-50", + "relative overflow-hidden bg-(--kumo-button-emphasis-bg) !text-kumo-button-emphasis ring ring-(--kumo-button-emphasis-ring) focus:ring-(--kumo-button-emphasis-ring) focus-visible:ring-(--kumo-button-emphasis-ring) active:ring-(--kumo-button-emphasis-ring) disabled:opacity-50", description: "High-emphasis button for primary actions", }, secondary: { @@ -63,7 +63,7 @@ export const KUMO_BUTTON_VARIANTS = { }, destructive: { classes: - "relative overflow-hidden bg-(--kumo-button-emphasis-bg) !text-white ring ring-(--kumo-button-emphasis-ring) focus:ring-(--kumo-button-emphasis-ring) focus-visible:ring-(--kumo-button-emphasis-ring) active:ring-(--kumo-button-emphasis-ring) disabled:opacity-50", + "relative overflow-hidden bg-(--kumo-button-emphasis-bg) !text-kumo-button-emphasis ring ring-(--kumo-button-emphasis-ring) focus:ring-(--kumo-button-emphasis-ring) focus-visible:ring-(--kumo-button-emphasis-ring) active:ring-(--kumo-button-emphasis-ring) disabled:opacity-50", description: "Danger button for destructive actions like delete", }, "secondary-destructive": { diff --git a/packages/kumo/src/styles/theme-kumo.css b/packages/kumo/src/styles/theme-kumo.css index 7ac498a1e7..98b5333b1c 100644 --- a/packages/kumo/src/styles/theme-kumo.css +++ b/packages/kumo/src/styles/theme-kumo.css @@ -17,6 +17,11 @@ var(--color-neutral-900, oklch(20.5% 0 0)) ); + --text-color-kumo-button-emphasis: light-dark( + var(--color-white, #fff), + var(--color-white, #fff) + ); + --text-color-kumo-strong: light-dark( var(--color-neutral-950, oklch(14.5% 0 0)), var(--color-neutral-50, oklch(98.5% 0 0)) @@ -294,6 +299,7 @@ oklch(21% 0.006 285.885) ); --text-color-kumo-inverse: var(--color-neutral-100, oklch(97% 0 0)); + --text-color-kumo-button-emphasis: var(--color-white, #fff); --text-color-kumo-strong: var(--color-neutral-950, oklch(14.5% 0 0)); --text-color-kumo-subtle: var(--color-neutral-500, oklch(55.6% 0 0)); --text-color-kumo-inactive: var(--color-neutral-300, oklch(87% 0 0)); @@ -376,6 +382,7 @@ [data-theme="kumo"] [data-mode="dark"] { --text-color-kumo-default: var(--color-neutral-100, oklch(97% 0 0)); --text-color-kumo-inverse: var(--color-neutral-900, oklch(20.5% 0 0)); + --text-color-kumo-button-emphasis: var(--color-white, #fff); --text-color-kumo-strong: var(--color-neutral-50, oklch(98.5% 0 0)); --text-color-kumo-subtle: var(--color-neutral-400, oklch(70.8% 0 0)); --text-color-kumo-inactive: var(--color-neutral-600, oklch(43.9% 0 0));