mirror of
https://github.com/paperclipai/paperclip.git
synced 2026-10-13 23:39:21 +02:00
## Thinking Path > - Paperclip is the open source app people use to manage AI agents for work. > - The board UI uses shadcn theme tokens from `ui/src/index.css`. > - Delete and remove dialogs confirm with a red button: `bg-destructive text-destructive-foreground`. > - In the light theme, `--destructive-foreground` has the same value as `--destructive`. The label is red text on a red fill, so it is not visible. > - Operators cannot read the button that deletes data or removes a connection. > - This pull request sets the light `--destructive-foreground` to the near-white value that the dark theme already uses. > - The benefit is that every destructive confirm button shows a readable label in both themes. ## Linked Issues or Issue Description Fixes #14327 I searched open and closed PRs for `destructive-foreground`, "Remove connection" and related terms. I found no PR that changes this token. ## What Changed - `ui/src/index.css`: in `:root`, `--destructive-foreground` changes from `oklch(0.577 0.245 27.325)` (the same as `--destructive`) to `oklch(0.985 0 0)`. This is the value in `.dark`. - Added `ui/src/components/DestructiveThemeTokens.test.ts`. The test converts both tokens from OKLCH to sRGB and computes the WCAG contrast ratio. `:root` must reach 4.5:1 (AA for normal text). `.dark` must reach 3:1, because it keeps the brighter red chosen in the gallery review (about 3.6:1 today). The fix applies to all five buttons that use `text-destructive-foreground`: remove connection (`Browse.tsx`, `Connections.tsx`), delete folder (`FolderControls.tsx`), delete environment (`CompanyEnvironments.tsx`) and remove chat endpoint (`ChatEndpointDetail.tsx`). No other file uses the token. ## Verification - `pnpm exec vitest run src/components/DestructiveThemeTokens.test.ts` in `ui`: 2 tests pass. The light theme contrast is about 4.8:1. - Before the fix, the test fails for `:root` (1:1, red on red). With a light gray foreground `oklch(0.88 0 0)` it also fails (3.3:1 < 4.5:1). - Manual check on a self-hosted instance in the light theme: Apps → connection → **Remove connection** showed a red button with no visible label. After the fix, the label is white and readable. Before and after, light theme (the same classes as the confirm button: `bg-destructive text-destructive-foreground`): <img src="https://gist.githubusercontent.com/gentslava/3aa0d498e2475dd44e12bed11e7d7862/raw/c793570ebd416fe04cc390df8554515b4207590d/remove-connection-before-after.png" alt="Remove connection button before and after the fix" width="520"> ## Risks - Low risk. The change is one token in the light theme. Only the five confirm buttons above read `--destructive-foreground`. ## Model Used - Claude Opus 5.5 (`claude-opus-5-5`) in Claude Code, with tool use (shell, file edits). It found the cause, wrote the fix and the test, and wrote this description. ## Checklist - [x] I have included a thinking path that traces from project context to this change - [x] I have specified the model used (with version and capability details) - [x] I have checked ROADMAP.md and confirmed this PR does not duplicate planned core work - [x] I have searched GitHub for duplicate or related PRs and linked them above - [x] I have either (a) linked existing issues with `Fixes: #` / `Closes #` / `Refs #` OR (b) described the issue in-PR following the relevant issue template - [x] I have not referenced internal/instance-local Paperclip issues or links (only public GitHub `#NNN` / `github.com/paperclipai/paperclip` URLs) - [x] My branch name describes the change (e.g. `docs/...`, `fix/...`) and contains no internal Paperclip ticket id or instance-derived details - [x] I have run tests locally and they pass - [x] I have added or updated tests where applicable - [x] I have updated relevant documentation to reflect my changes - [x] I have considered and documented any risks above - [x] All Paperclip CI gates are green - [x] Greptile is 5/5 with no open P2s, recommendations, or follow-ups - [x] I will address all Greptile and reviewer comments before requesting merge @cryppadotta @devinfoley a tiny one-line theme fix: the delete buttons in the light theme are red-on-red right now, so people can't see what they are about to click. Would be great to get it in when you have a minute 🙏 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
68 lines
2.8 KiB
TypeScript
68 lines
2.8 KiB
TypeScript
import { readFileSync } from "node:fs";
|
|
import { fileURLToPath } from "node:url";
|
|
import { describe, expect, it } from "vitest";
|
|
|
|
const stylesheet = readFileSync(fileURLToPath(new URL("../index.css", import.meta.url)), "utf8");
|
|
|
|
function cssBlock(selector: string): string {
|
|
const start = stylesheet.indexOf(`\n${selector} {`);
|
|
expect(start, `Missing CSS selector: ${selector}`).toBeGreaterThanOrEqual(0);
|
|
const bodyStart = stylesheet.indexOf("{", start);
|
|
const bodyEnd = stylesheet.indexOf("\n}", bodyStart);
|
|
return stylesheet.slice(bodyStart + 1, bodyEnd);
|
|
}
|
|
|
|
function token(block: string, name: string): string {
|
|
const match = block.match(new RegExp(`^\\s*--${name}:\\s*([^;]+);`, "m"));
|
|
expect(match, `Missing token --${name}`).not.toBeNull();
|
|
return match![1].trim();
|
|
}
|
|
|
|
function oklch(value: string): [number, number, number] {
|
|
const match = value.match(/^oklch\(\s*([\d.]+)\s+([\d.]+)\s+([\d.]+)\s*\)$/);
|
|
expect(match, `Expected an oklch() color, got ${value}`).not.toBeNull();
|
|
return [Number(match![1]), Number(match![2]), Number(match![3])];
|
|
}
|
|
|
|
// WCAG 2 relative luminance of an oklch() color, via OKLab and linear sRGB.
|
|
function relativeLuminance(value: string): number {
|
|
const [l, c, h] = oklch(value);
|
|
const a = c * Math.cos((h * Math.PI) / 180);
|
|
const b = c * Math.sin((h * Math.PI) / 180);
|
|
const lms = [
|
|
l + 0.3963377774 * a + 0.2158037573 * b,
|
|
l - 0.1055613458 * a - 0.0638541728 * b,
|
|
l - 0.0894841775 * a - 1.291485548 * b,
|
|
].map((v) => v ** 3);
|
|
const rgb = [
|
|
4.0767416621 * lms[0] - 3.3077115913 * lms[1] + 0.2309699292 * lms[2],
|
|
-1.2684380046 * lms[0] + 2.6097574011 * lms[1] - 0.3413193965 * lms[2],
|
|
-0.0041960863 * lms[0] - 0.7034186147 * lms[1] + 1.707614701 * lms[2],
|
|
].map((v) => Math.min(1, Math.max(0, v)));
|
|
return 0.2126 * rgb[0] + 0.7152 * rgb[1] + 0.0722 * rgb[2];
|
|
}
|
|
|
|
function contrastRatio(first: string, second: string): number {
|
|
const [lighter, darker] = [relativeLuminance(first), relativeLuminance(second)].sort((x, y) => y - x);
|
|
return (lighter + 0.05) / (darker + 0.05);
|
|
}
|
|
|
|
describe("destructive theme tokens", () => {
|
|
// Confirm buttons use `bg-destructive text-destructive-foreground`. The label
|
|
// must stay readable on the destructive fill in both themes.
|
|
// Light theme meets WCAG AA for normal text (4.5:1). The dark theme keeps the
|
|
// brighter red chosen in the gallery review, so it only has to meet 3:1.
|
|
for (const [selector, minimum] of [
|
|
[":root", 4.5],
|
|
[".dark", 3],
|
|
] as const) {
|
|
it(`keeps destructive-foreground readable on the destructive fill in ${selector}`, () => {
|
|
const block = cssBlock(selector);
|
|
const fill = token(block, "destructive");
|
|
const foreground = token(block, "destructive-foreground");
|
|
|
|
expect(contrastRatio(foreground, fill)).toBeGreaterThanOrEqual(minimum);
|
|
});
|
|
}
|
|
});
|