Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/themeable-button-foreground.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@cloudflare/kumo": patch
---

Make primary and destructive Button foreground colors themeable through the semantic `text-kumo-button-emphasis` token.
4 changes: 4 additions & 0 deletions packages/kumo-docs-astro/src/pages/colors.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -284,6 +284,10 @@ Use the solid token `bg-kumo-*` for status dots, `fill-kumo-*` for icons, and `b
<td><code>text-kumo-inverse</code></td>
<td>Text intended for use on high-contrast or inverted backgrounds</td>
</tr>
<tr>
<td><code>text-kumo-button-emphasis</code></td>
<td>Foreground text and icons on primary and destructive Button emphasis backgrounds</td>
</tr>
<tr>
<td><code>text-kumo-link</code></td>
<td>Link text</td>
Expand Down
27 changes: 14 additions & 13 deletions packages/kumo/ai/USAGE.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
11 changes: 11 additions & 0 deletions packages/kumo/scripts/theme-generator/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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: {
Expand Down
13 changes: 13 additions & 0 deletions packages/kumo/scripts/theme-generator/generate-css.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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",
);
});
});
2 changes: 1 addition & 1 deletion packages/kumo/src/components/banner/banner.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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"),
Expand Down
59 changes: 59 additions & 0 deletions packages/kumo/src/components/button/button.browser.test.tsx
Original file line number Diff line number Diff line change
@@ -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(
<>
<Button variant="primary">Save</Button>
<Button variant="destructive">Delete</Button>
</>,
);

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(
<div style={customTheme}>
<Button variant="primary">Save</Button>
<Button variant="destructive" disabled>
Delete
</Button>
<Button variant="primary" loading>
Loading
</Button>
</div>,
);

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");
});
});
30 changes: 30 additions & 0 deletions packages/kumo/src/components/button/button.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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(
<>
<Button variant="primary" disabled>
Save
</Button>
<Button variant="destructive" loading>
Delete
</Button>
</>,
);

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", () => {
Expand Down
4 changes: 2 additions & 2 deletions packages/kumo/src/components/button/button.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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: {
Expand All @@ -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": {
Expand Down
7 changes: 7 additions & 0 deletions packages/kumo/src/styles/theme-kumo.css
Original file line number Diff line number Diff line change
Expand Up @@ -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))
Expand Down Expand Up @@ -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));
Expand Down Expand Up @@ -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));
Expand Down