From dfb8a6d6cb1f0d7cd0fa23ef5d7a67437b90f65e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Padez?= Date: Sun, 16 Aug 2026 23:13:19 +0000 Subject: [PATCH] a sixth tone, and no raw palette left in the core apps MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The five core apps — FileBrowser, FileViewer, SystemMonitor, Chat, Plugins — now contain ZERO raw Tailwind palette classes. Every colour in them names a state or a surface, so all of it follows the theme. ── Busy ── The 38 ambers were the blocker, and the answer was not to pick between two wrong mappings. They mean RUNNING — Chat's own comment says "the amber a RUNNING task chip wears" — and the vocabulary had no word for it. `warning` would have made a working job look like a failing one, and `--status-in-progress` is blue. So `tone.ts` has six tones instead of five. It was always written as a closed set that could grow when a genuine sixth state appeared; this is that. `--busy-ink` comes with it, and generalises: a fill bright enough to carry black text is far too light to BE text. The other five tones are dark enough to do both jobs, which is why the problem had not surfaced. `text-amber-950` — ink ON the fill — became `text-busy-foreground`, which is what it always was. ── On fidelity, which stopped being the constraint ── Duck's exact colours were preserved up to this point because an extraction has to be provably invisible. The owner has since said the palette is not precious: the wallpaper, the graph-paper grid and the mark are what matter, and everything else can move. So shades collapse — one busy, one red, one green — rather than being approximated to keep old pixels. The verification built for the old constraint keeps its value: it proves the token layer resolves, which is a different claim from "the colours did not change". Verified: the utilities compile (`text-busy-ink`, `bg-busy`, `border-busy`, `text-busy-foreground`) and resolve in the browser. 950 pass / 7 fail unchanged. tsgo clean. Co-Authored-By: Claude Opus 5 --- src/workspaces/components/Data/tone.ts | 9 +++++++-- .../src/apps/Chat/components/InputArea.tsx | 11 ++++------- .../components/TaskRunnerModal.tsx | 16 ++++++++-------- .../officerdev/src/apps/Plugins/PluginDetail.tsx | 12 ++++++------ .../officerdev/src/apps/Plugins/PluginsList.tsx | 6 +++--- .../src/apps/SystemMonitor/BtopView.tsx | 4 ++-- .../src/apps/SystemMonitor/DockerView.tsx | 2 +- .../src/apps/SystemMonitor/Pm2View.tsx | 2 +- src/workspaces/theme/css/base.css | 9 +++++++++ src/workspaces/theme/css/themes/duck-dark.css | 5 +++++ src/workspaces/theme/css/themes/duck-light.css | 5 +++++ src/workspaces/theme/css/themes/plain.css | 5 +++++ 12 files changed, 56 insertions(+), 30 deletions(-) diff --git a/src/workspaces/components/Data/tone.ts b/src/workspaces/components/Data/tone.ts index cfcec618..03471b08 100644 --- a/src/workspaces/components/Data/tone.ts +++ b/src/workspaces/components/Data/tone.ts @@ -1,6 +1,6 @@ // The state vocabulary for data surfaces. See docs/design-language-interface.md § 3. // -// Five tones and no others. The point of a closed set is that colour keeps meaning something: when a +// Six tones and no others. The point of a closed set is that colour keeps meaning something: when a // view can reach for any of Tailwind's twenty-two palettes, green stops reading as "open" and starts // reading as "the author liked green here". Every tone maps to a semantic token, never a palette // number, so it follows the theme and survives dark mode. @@ -8,7 +8,7 @@ // Class strings are written out in full rather than composed (`text-${tone}`) because Tailwind's // scanner reads source text — an interpolated class name is not in the output CSS at all. -export type Tone = 'neutral' | 'success' | 'warning' | 'danger' | 'info'; +export type Tone = 'neutral' | 'success' | 'warning' | 'danger' | 'info' | 'busy'; /** Foreground only — for an icon or a word carrying state inside otherwise normal text. */ export const toneText: Record = { @@ -17,6 +17,9 @@ export const toneText: Record = { warning: 'text-warning', danger: 'text-destructive', info: 'text-info', + // `-ink`, not the fill: busy's fill is bright enough to carry black text, which makes it unreadable AS + // text. The other five are dark enough to serve both jobs. + busy: 'text-busy-ink', }; /** @@ -29,6 +32,7 @@ export const toneFill: Record = { warning: 'bg-warning/15 text-warning', danger: 'bg-destructive/15 text-destructive', info: 'bg-info/15 text-info', + busy: 'bg-busy/15 text-busy-ink', }; /** A left border or rule carrying state, for rows that mark status without a pill. */ @@ -38,4 +42,5 @@ export const toneBorder: Record = { warning: 'border-warning', danger: 'border-destructive', info: 'border-info', + busy: 'border-busy', }; diff --git a/src/workspaces/officerdev/src/apps/Chat/components/InputArea.tsx b/src/workspaces/officerdev/src/apps/Chat/components/InputArea.tsx index 626ed13f..e78d3a8a 100644 --- a/src/workspaces/officerdev/src/apps/Chat/components/InputArea.tsx +++ b/src/workspaces/officerdev/src/apps/Chat/components/InputArea.tsx @@ -144,7 +144,7 @@ export const InputArea = ({ manager }: InputAreaProps) => { size="icon" title={isGenerating ? 'Queue — sends when the current turn finishes' : 'Send'} className={`shrink-0 h-7 w-7 md:h-9 md:w-9 cursor-pointer disabled:opacity-40 ${ - isGenerating ? 'bg-amber-500 hover:bg-amber-500/90 text-amber-950' : 'bg-duck-teal hover:bg-duck-teal/90' + isGenerating ? 'bg-busy hover:bg-busy/90 text-busy-foreground' : 'bg-duck-teal hover:bg-duck-teal/90' }`} > {isGenerating ? : } @@ -264,16 +264,13 @@ const QueuedList = ({ type="button" onClick={onSendNow} title="Send these into the turn that is already running" - className="cursor-pointer self-start rounded px-1 text-[11px] text-amber-700/80 transition-colors hover:bg-amber-500/10 hover:text-amber-700" + className="cursor-pointer self-start rounded px-1 text-[11px] text-busy-ink/80 transition-colors hover:bg-busy/10 hover:text-busy-ink" > Send now — press Enter on an empty box to add {queued.length === 1 ? 'this' : 'these'} to the running turn {queued.map((item) => ( -
- +
+ {item.raw.trim()}