Skip to content
Merged
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
The table of contents is too big for display.
Diff view
Diff view
  •  
  •  
  •  
2 changes: 2 additions & 0 deletions .agents/skills/codeatlas-ui/API.md
Original file line number Diff line number Diff line change
Expand Up @@ -627,6 +627,8 @@ Data attributes: `data-disabled`, `data-state` (`checked` `indeterminate`)

Parts: `ca-chip` `ca-chip--brand` `ca-chip--error` `ca-chip--info` `ca-chip--interactive` `ca-chip--selected` `ca-chip--sm` `ca-chip--success` `ca-chip--warning` `ca-chip__delete` `ca-chip__icon` `ca-chip__label`

Data attributes: `data-theme-scope` (`dark` `light`)

## ChipGroup

`import { ChipGroup } from "@codeatlasdev/ui"`
Expand Down
5 changes: 5 additions & 0 deletions .changeset/island-scope-surfaces.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@codeatlasdev/ui": patch
---

Surfaces and chips inside a `ThemeIsland` (and a `PreviewStage` with a `scheme` or `preset`) follow the island, not the page around it. The layer tokens (`--ca-overlay-bg`, `--ca-panel-bg`, `--ca-chrome-bg`, `--ca-upload-bg`) and the solid style's tint overrides are re-declared on `[data-theme-scope]`, so a dark island on a light page no longer inherits the page's resolved light surface (a `FileUpload` zone, an `AppForm` footer) under dark text. The `success`, `error`, `warning` and `info` chip text follows `[data-theme-scope]` as well as `.dark`.
5 changes: 5 additions & 0 deletions .changeset/scroll-focusable-subtree.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@codeatlasdev/ui": patch
---

`scrollFocusable` re-measures when content is added at any depth, not only as a direct child of the scroller. A fixed-height wrapper never resizes when the content it holds grows, so a lazily loaded demo inside a `PreviewStage` frame left its scrollable region without a tab stop.
5 changes: 5 additions & 0 deletions .changeset/scroll-regions-focusable.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@codeatlasdev/ui": patch
---

`PreviewStage` frames and `ResizablePanel`s that scroll are keyboard-reachable while their content overflows (`scrollFocusable`, WCAG 2.1.1): a demo taller than the stage, or a narrow panel whose content is wider than it, no longer traps its content out of keyboard reach. Without overflow they add no tab stop.
5 changes: 5 additions & 0 deletions .changeset/seg-touch-width.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@codeatlasdev/ui": patch
---

`SegmentedControl` items are at least 44px wide as well as tall under a coarse pointer: in a narrow slot the equal shrinkable columns squeezed short labels (a "390" preset) to 38px, under the touch target.
47 changes: 28 additions & 19 deletions AGENTS.md

Large diffs are not rendered by default.

10 changes: 5 additions & 5 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,7 @@ Then, in the worktree (use paths inside it; your editor or agent must not edit t
1. **Understand and reproduce.** Write the regression test first and run it: it must fail on the current code. Paste that output in the PR.
2. **Fix** the root cause until the test passes. No workarounds for a symptom. Run every test you added or changed, Playwright included.
3. **Before review:** `bun run validate`, a changeset if the package changes, docs if behavior changed. Never bypass the pre-push hook (`--no-verify`, `LEFTHOOK=0`): a gate that fails only under load is a bug; open an issue for it.
4. **Landing (maintainers):** `sh scripts/issue-land.sh 47` rebases on `main`, rebuilds generated files (lockfile, API.md, components.json, css-api), validates, checks the visual baselines locally in Docker (`bun run test:vrt:check`) and squash-merges (a mismatch stops it: review and commit `bun run test:vrt:update`, then rerun). GitHub closes the issue through `Closes #47`; the script then runs `issue:done` (worktree, branch and `in progress` label cleanup).
4. **Landing (maintainers):** `sh scripts/issue-land.sh 47` rebases on `main`, rebuilds generated files (lockfile, API.md, components.json, css-api), runs the land gate (`scripts/test-changed.ts --land`: the tests and screenshots the diff affects plus 5 canary routes, 3 minute budget; a screenshot mismatch stops it: review and commit `bun run test:vrt:update`, then rerun), pushes (the pre-push hook runs `bun run validate`, 90 second budget) and squash-merges. GitHub closes the issue through `Closes #47`; the script then runs `issue:done` (worktree, branch and `in progress` label cleanup).

Issues filed by an AI agent through the `report_issue` MCP tool use the **AI report** form (`ai-report` label) and are sanitized before posting (no project names, paths, URLs, env, secrets or data). Treat them like any bug report; when you close one, `bunx @codeatlasdev/ui workarounds` is how its reporter finds the workaround to remove. See [Reporting problems](https://ui.codeatlas.com.br/docs/getting-started/reporting/).

Expand All @@ -91,7 +91,7 @@ If you find the issue is already fixed or obsolete, prove it with a test that pa

**Test first.** A bug fix starts with a regression test that reproduces the problem: run it, watch it fail on the current code, then fix until it passes, and put that fail-before/pass-after evidence in the PR. Tests prove behavior; tests that only pad coverage (wiring, copies, mock echoes, incidental defaults) are rejected.

> **CI is paused** while the project is pre-stable: the `CI` workflow is disabled (`gh workflow disable ci.yml`) and `CI ok` is no longer a required check on `main`. The local gates are the gate: the pre-push hook runs `bun run validate`; run the browser suites (`bun run test:browser`, `test:e2e`, `test:vrt:check`) yourself when a change touches them. PR titles are still checked (`pr-title.yml`), and releases still run on tags. To turn CI back on: `gh workflow enable ci.yml` and add `CI ok` back as a required status check in the `main` ruleset.
> **CI is paused** while the project is pre-stable: the `CI` workflow is disabled (`gh workflow disable ci.yml`) and `CI ok` is no longer a required check on `main`. The local gates are the gate: the pre-push hook runs `bun run validate`; `bun run test` runs what your change affects (seconds); `bun run test:full` and `bun run test:vrt` run everything and are what a release runs. PR titles are still checked (`pr-title.yml`), and releases still run on tags. To turn CI back on: `gh workflow enable ci.yml` and add `CI ok` back as a required status check in the `main` ruleset.

CI (`.github/workflows/ci.yml`, when enabled) runs the same gates. Locally, both must exit with code 0:

Expand All @@ -102,11 +102,11 @@ bun run test # unit (bun test) + component tests (Chromium) + site E2E/a11

Unit tests run on `bun test` (`bunfig.toml` points it at `packages/ui/tests`); component and visual tests need a real browser and stay on Vitest, so prefer `bun run test` for the full suite. Type checking is split by who understands the files: `bun --check` (the native checker, config in the root `tsconfig.json`) covers scripts, unit tests and Playwright specs, `svelte-check` covers `packages/ui` and `astro check` covers `site`. Bun >= 1.4.3 and Node >= 22.17 are required.

Browser tests always run through the package scripts (`bun run test:browser`, `test:e2e`, …), never a bare `vitest` or `playwright test`. The scripts go through `scripts/test-run.ts`, which tags this repo's browsers, tears them down on exit or Ctrl+C, and cleans up after runs that were killed hard. `bun run test:status` shows runs, `bun run test:reap` cleans dead ones. It never touches other projects' Playwright browsers.
Browser tests always run through the package scripts (`bun run test`, `test:browser`, `test:e2e`, …), never a bare `vitest` or `playwright test`. The scripts go through `scripts/test-run.ts`, which tags this repo's browsers, tears them down on exit or Ctrl+C, cleans up after runs that were killed hard, holds a machine-wide lock so a second suite waits (and says who holds the lock) instead of starting more browsers, and enforces a time budget per command: `bun run test` 60 s, the land gate 180 s (300 s when a shared file changed: every component test and route, no screenshots), `validate` 90 s, `test:full` 300 s and `test:vrt` 180 s (a release runs both). A run over budget fails and prints its 10 slowest tests (on an oversubscribed machine the verdict is advisory, see AGENTS.md). `bun run test:status` shows runs and the lock holder, `bun run test:reap` cleans dead ones. It never touches other projects' Playwright browsers.

Git hooks (lefthook, installed by `bun install`): pre-commit formats staged files with Biome, commit-msg checks the message, pre-push runs `bun run validate`. Browser suites (`bun run test:browser`, `test:e2e`) run on CI and on demand.
Git hooks (lefthook, installed by `bun install`): pre-commit formats staged files with Biome, commit-msg checks the message, pre-push runs `bun run validate`. `bun run test` is what to run while you work: it selects the unit tests, component tests, screenshots and docs routes your diff reaches and prints what it chose and why. A change to a shared file (styles, tokens, theme, primitives, a test setup file) runs every component test, and a change it cannot map to a route runs every route.

Visual regression baselines are rendered in Docker (`linux/amd64`, the Playwright `noble` image matching `@playwright/test`, with the fonts adjusted to resolve like the old `ubuntu-24.04` CI runner), never on the host, so the pixels are identical for everyone. `bun run test:vrt:check` compares both suites (components and site) against the committed baselines; `bun run test:vrt:update` rewrites them in your worktree. Docker is required. Review the changed images before committing them, and run `test:vrt:check` again to confirm they are stable.
Visual regression baselines are rendered natively on your machine, never in Docker, and the platform is part of every baseline's file name (`-chromium-darwin.png`, `-darwin.webp`), so macOS and Linux baselines sit side by side and a platform is only compared with its own. `bun run test:vrt` compares both suites (components, and the landing, templates and theme at the viewport) against the committed baselines; `bun run test:vrt:update` rewrites those of your platform in your worktree. Review the changed images before committing them, and run `test:vrt` twice to confirm they are stable. On a platform with no baselines yet, update first. CI stays disabled, so these local commands are the only visual check.

Fix any errors before opening a pull request. PRs with failing checks will not be merged.

Expand Down
1 change: 1 addition & 0 deletions bun.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

12 changes: 7 additions & 5 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -29,9 +29,11 @@
"release:check": "node scripts/release.js",
"clean": "rm -rf packages/ui/dist site/dist site/.astro",
"lint:fix": "biome check --write .",
"validate": "bun scripts/validate.ts",
"postinstall": "node scripts/fix-ts-symlink.js && node scripts/install-hooks.js && bunx playwright install chromium",
"test": "bun run test:unit && bun run test:browser && bun run test:fixtures && bun run test:e2e",
"validate": "bun scripts/test-run.ts --budget=validate bun scripts/validate.ts",
"postinstall": "node scripts/fix-ts-symlink.js && node scripts/install-hooks.js && bunx playwright install chromium webkit",
"test": "bun scripts/test-run.ts --budget=changed bun scripts/test-changed.ts",
"test:changed": "bun scripts/test-run.ts --budget=changed bun scripts/test-changed.ts",
"test:full": "bun scripts/test-run.ts --budget=full sh -c 'bun run test:unit && bun run test:browser && bun run test:fixtures && bun run test:e2e'",
"test:unit": "bun run --cwd packages/ui test:unit && bun run --cwd site test:unit",
"test:browser": "bun run --cwd packages/ui test:browser",
"test:fixtures": "bun run --cwd packages/ui test:e2e:bun",
Expand All @@ -43,8 +45,8 @@
"issue:status": "bun scripts/issue.ts status",
"issue:triage": "bun scripts/issue.ts triage",
"issue:done": "bun scripts/issue.ts done",
"test:vrt:check": "sh scripts/vrt-docker.sh --check",
"test:vrt:update": "sh scripts/vrt-docker.sh --update",
"test:vrt": "bun scripts/test-run.ts --budget=vrt sh -c 'bun run --cwd packages/ui test:vrt && bun run --cwd site test:vrt'",
"test:vrt:update": "bun scripts/test-run.ts sh -c 'bun run --cwd packages/ui test:vrt:update && bun run --cwd site test:vrt:update'",
"test:reap": "bun scripts/test-run.ts --reap",
"test:status": "bun scripts/test-run.ts --status"
},
Expand Down
26 changes: 26 additions & 0 deletions packages/ui/src/lib/actions/ScrollFocusableHarness.test.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
<script lang="ts">
import { scrollFocusable } from "./scroll-focusable.js";

let { later = false }: { later?: boolean } = $props();
let show = $state(false);
// Well after the action's first measurement and the first frame.
$effect(() => {
if (!later) return;
const t = setTimeout(() => (show = true), 300);
return () => clearTimeout(t);
});
</script>

<!-- The scroller holds a fixed-height wrapper (like the stage's island); the content that
overflows mounts later, inside that wrapper: two levels below the scroller. -->
<div class="scroller" data-testid="scroller" use:scrollFocusable>
<div class="wrapper">
{#if show}<div class="tall">Mounted after the first paint</div>{/if}
</div>
</div>

<style>
.scroller { width: 200px; height: 100px; overflow: auto; }
.wrapper { height: 100%; }
.tall { height: 400px; }
</style>
19 changes: 19 additions & 0 deletions packages/ui/src/lib/actions/scroll-focusable.svelte.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import { describe, expect, it } from "vitest";
import { render } from "vitest-browser-svelte";
import Harness from "./ScrollFocusableHarness.test.svelte";

describe("scrollFocusable", () => {
it("content that overflows after mount, below a direct child, makes the scroller focusable", async () => {
const screen = await render(Harness, { later: true });
const el = screen.getByTestId("scroller").element() as HTMLElement;
expect(el.hasAttribute("tabindex")).toBe(false);
await expect.poll(() => el.scrollHeight > el.clientHeight + 1).toBe(true);
await expect.poll(() => el.tabIndex, { timeout: 2000 }).toBe(0);
});

it("adds no tab stop while nothing overflows", async () => {
const screen = await render(Harness, { later: false });
const el = screen.getByTestId("scroller").element() as HTMLElement;
expect(el.hasAttribute("tabindex")).toBe(false);
});
});
6 changes: 3 additions & 3 deletions packages/ui/src/lib/actions/scroll-focusable.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,13 +18,13 @@ export const scrollFocusable: Action<HTMLElement> = (node) => {
ro.observe(node);
for (const child of node.children) ro.observe(child);
};
// Children rendered after mount (hydration, async data, route content) change the
// overflow too: watch them as they arrive.
// Content rendered after mount (hydration, async data, a lazily loaded demo) changes the
// overflow too, at any depth: a fixed-height wrapper never resizes when what it holds grows.
const mo = new MutationObserver(() => {
watch();
update();
});
mo.observe(node, { childList: true });
mo.observe(node, { childList: true, subtree: true });
watch();
update();
return {
Expand Down
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
26 changes: 20 additions & 6 deletions packages/ui/src/lib/components/Carousel/Carousel.svelte.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,10 @@ const btn = (c: HTMLElement, name: RegExp) =>
name.test(b.getAttribute("aria-label") ?? b.textContent ?? "")
) as HTMLButtonElement;
const settle = () => new Promise((r) => setTimeout(r, 450));
// The autoplay chain is plain setTimeout: a fake clock installed BEFORE render, advanced past several
// of its intervals, proves "nothing rotates" with no wall time (a clock faked after render would not
// own the component's timer and prove nothing).
const fakeClock = () => vi.useFakeTimers({ toFake: ["setTimeout", "clearTimeout"] });

beforeEach(() => {
vi.useRealTimers();
Expand Down Expand Up @@ -135,30 +139,38 @@ describe("Carousel controls", () => {

describe("Carousel autoplay (opt in, always stoppable)", () => {
it("does not rotate and shows no play control unless autoplay is set", async () => {
fakeClock();
const screen = await render(Harness, {});
expect(q(screen.container, ".ca-carousel").querySelector(".ca-carousel__play")).toBeNull();
await new Promise((r) => setTimeout(r, 700));
await vi.advanceTimersByTimeAsync(700);
expect(dots(screen.container)[0].getAttribute("aria-current")).toBe("true");
});

it("rotates on its own, offers Pause, and stops when paused", async () => {
fakeClock();
const screen = await render(Harness, { autoplay: 300, loop: true });
const play = q<HTMLButtonElement>(screen.container, ".ca-carousel__play");
expect(play.getAttribute("aria-label")).toBe("Pause autoplay");
await expect.poll(() => dots(screen.container)[1].getAttribute("aria-current")).toBe("true");
await play.click();
await vi.advanceTimersByTimeAsync(300);
expect(dots(screen.container)[1].getAttribute("aria-current")).toBe("true");
// Dispatch the click directly: the clock stays the one that owns the autoplay timer, and a
// Playwright click (its own round trips) would need real timers.
play.click();
await vi.advanceTimersByTimeAsync(0);
expect(play.getAttribute("aria-label")).toBe("Start autoplay");
const at = dots(screen.container).findIndex((d) => d.hasAttribute("aria-current"));
await new Promise((r) => setTimeout(r, 900));
await vi.advanceTimersByTimeAsync(900);
expect(dots(screen.container).findIndex((d) => d.hasAttribute("aria-current"))).toBe(at);
vi.useRealTimers();
expect(await checkA11y(screen.container)).toHaveNoViolations();
});

it("pauses while focus is inside and while the pointer is over it", async () => {
fakeClock();
const screen = await render(Harness, { autoplay: 300, loop: true });
btn(screen.container, /slide 1 action/i).focus();
const at = dots(screen.container).findIndex((d) => d.hasAttribute("aria-current"));
await new Promise((r) => setTimeout(r, 900));
await vi.advanceTimersByTimeAsync(900);
expect(dots(screen.container).findIndex((d) => d.hasAttribute("aria-current"))).toBe(at);
});

Expand All @@ -184,10 +196,12 @@ describe("Carousel reduced motion", () => {
} as unknown as MediaQueryList)
: real(q)) as typeof window.matchMedia;
try {
fakeClock();
const screen = await render(Harness, { autoplay: 200, loop: true });
expect(screen.container.querySelector(".ca-carousel__play")).toBeNull();
await new Promise((r) => setTimeout(r, 700));
await vi.advanceTimersByTimeAsync(700);
expect(dots(screen.container)[0].getAttribute("aria-current")).toBe("true");
vi.useRealTimers();
expect(await checkA11y(screen.container)).toHaveNoViolations();
} finally {
window.matchMedia = real;
Expand Down
18 changes: 14 additions & 4 deletions packages/ui/src/lib/components/Chip/Chip.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -168,10 +168,20 @@
.ca-chip--brand { background: color-mix(in oklab, var(--ca-brand-main) 14%, transparent); color: var(--ca-brand-text); border-color: color-mix(in oklab, var(--ca-brand-main) 30%, transparent); }

/* Dark mode variants: raise L for legibility */
:global(.dark) .ca-chip--success { color: var(--ca-semantic-success); border-color: color-mix(in oklab, var(--ca-semantic-success) 35%, transparent); }
:global(.dark) .ca-chip--error { color: var(--ca-semantic-error); border-color: color-mix(in oklab, var(--ca-semantic-error) 35%, transparent); }
:global(.dark) .ca-chip--warning { color: var(--ca-semantic-warning); border-color: color-mix(in oklab, var(--ca-semantic-warning) 35%, transparent); }
:global(.dark) .ca-chip--info { color: var(--ca-semantic-info); border-color: color-mix(in oklab, var(--ca-semantic-info) 35%, transparent); }
:global(.dark) .ca-chip--success,
:global([data-theme-scope='dark']) .ca-chip--success { color: var(--ca-semantic-success); border-color: color-mix(in oklab, var(--ca-semantic-success) 35%, transparent); }
:global(.dark) .ca-chip--error,
:global([data-theme-scope='dark']) .ca-chip--error { color: var(--ca-semantic-error); border-color: color-mix(in oklab, var(--ca-semantic-error) 35%, transparent); }
:global(.dark) .ca-chip--warning,
:global([data-theme-scope='dark']) .ca-chip--warning { color: var(--ca-semantic-warning); border-color: color-mix(in oklab, var(--ca-semantic-warning) 35%, transparent); }
:global(.dark) .ca-chip--info,
:global([data-theme-scope='dark']) .ca-chip--info { color: var(--ca-semantic-info); border-color: color-mix(in oklab, var(--ca-semantic-info) 35%, transparent); }

/* A light island inside a dark page: back to the light-mode text of the variants above. */
:global([data-theme-scope='light']) .ca-chip--success { color: color-mix(in oklab, var(--ca-semantic-success) 65%, black); }
:global([data-theme-scope='light']) .ca-chip--error { color: color-mix(in oklab, var(--ca-semantic-error) 65%, black); }
:global([data-theme-scope='light']) .ca-chip--warning { color: color-mix(in oklab, var(--ca-semantic-warning) 65%, black); }
:global([data-theme-scope='light']) .ca-chip--info { color: color-mix(in oklab, var(--ca-semantic-info) 65%, black); }

/* Size */
.ca-chip--sm { font-size: var(--ca-font-size-micro); height: var(--ca-space-5); padding: 0 var(--ca-space-2); }
Expand Down
Loading