feat(FN-844): add theme-driven logo and CTA button token system

- Add CSS custom properties (--kb-logo-accent, --kb-cta-*) for logo accent color and new-task action buttons
- Update logo.svg to use currentColor for theme-aware accent stroke
- Apply tokenized styling to Header logo, QuickEntryBox, InlineCreateCard, ListView, and Column new-task buttons
- Add theme-aware test assertions for logo and CTA buttons in Header, InlineCreateCard, ListView, and QuickEntryBox tests
- Document theme token system in dashboard README
This commit is contained in:
gsxdsm
2026-04-04 04:13:36 -07:00
parent c5bbb4bcb5
commit e4019c0c99
12 changed files with 111 additions and 16 deletions

View File

@@ -318,6 +318,27 @@ To add a new color theme:
3. Add the swatch class for the theme picker in the CSS
4. Update `ThemeSelector.tsx` with the new theme option
### Theme-Driven Logo and Task-Creation CTAs
The Fusion logo and all task-creation action buttons (including the "+ New Task" and "Save" buttons in board view, list view, and inline creation surfaces) are fully tokenized and respond to the active color theme. This ensures branding and task-creation affordances stay visually consistent across all 12 color themes and both light/dark modes.
**Theme tokens used:**
| Token | Purpose |
|-------|---------|
| `--logo-accent` | Fusion logo SVG fill color (defaults to `var(--todo)`) |
| `--cta-bg` / `--cta-bg-hover` | Task-creation CTA button background |
| `--cta-border` / `--cta-border-hover` | Task-creation CTA button border |
| `--cta-text` / `--cta-text-hover` | Task-creation CTA button text |
| `--cta-glow` | Box-shadow glow on hover |
**Semantic class:** `.btn-task-create` — used consistently on:
- Board column "+ New Task" button (`Column.tsx`)
- List view "+ New Task" button (`ListView.tsx`)
- Inline create card "Save" button (`InlineCreateCard.tsx`)
- Quick entry box "Save" button (`QuickEntryBox.tsx`)
All color themes automatically provide values for these tokens. Adding a new color theme requires only setting `--todo` (and optionally `--logo-accent` if the logo should differ from the todo column color).
## Performance Characteristics
The dashboard includes several runtime safeguards to stay responsive during long sessions and on larger boards:

View File

@@ -165,7 +165,7 @@ function ColumnComponent({ column, tasks, projectId, maxConcurrent, onMoveTask,
</label>
)}
{onNewTask && (
<button className="btn btn-primary btn-sm" onClick={onNewTask}>
<button className="btn btn-task-create btn-sm" onClick={onNewTask}>
+ New Task
</button>
)}

View File

@@ -242,7 +242,20 @@ export function Header({
return (
<header className="header">
<div className="header-left">
<img src="/logo.svg" alt="Fusion logo" className="header-logo" width={24} height={24} />
<svg
className="header-logo"
width={24}
height={24}
viewBox="0 0 128 128"
fill="none"
aria-label="Fusion logo"
role="img"
>
<circle cx="44" cy="44" r="20" fill="currentColor" />
<circle cx="84" cy="44" r="20" fill="currentColor" />
<circle cx="44" cy="84" r="20" fill="currentColor" />
<circle cx="84" cy="84" r="20" fill="currentColor" />
</svg>
<h1 className="logo">Fusion</h1>
{/* Project Selector - shown when 2+ projects on desktop only */}

View File

@@ -747,7 +747,7 @@ export function InlineCreateCard({
<span className="inline-create-hint">Enter to create · Esc to cancel</span>
<button
type="button"
className="btn btn-primary btn-sm"
className="btn btn-task-create btn-sm"
onClick={handleSubmit}
disabled={!description.trim() || submitting}
data-testid="save-button"

View File

@@ -705,7 +705,7 @@ export function ListView({
</div>
)}
{onNewTask ? (
<button className="btn btn-primary btn-sm" onClick={onNewTask}>
<button className="btn btn-task-create btn-sm" onClick={onNewTask}>
+ New Task
</button>
) : null}

View File

@@ -910,7 +910,7 @@ export function QuickEntryBox({ onCreate, addToast, tasks = [], availableModels,
{!isSubmitting && (
<button
type="button"
className="btn btn-sm"
className="btn btn-task-create btn-sm"
onClick={handleSaveClick}
onMouseDown={(e) => e.preventDefault()}
disabled={!description.trim() || isSubmitting}

View File

@@ -43,17 +43,20 @@ describe("Header", () => {
afterEach(() => {
vi.restoreAllMocks();
});
it("renders a logo image with correct src and alt", () => {
it("renders a theme-driven logo element (inline SVG) with aria-label", () => {
render(<Header />);
const logo = screen.getByAltText("Fusion logo");
// The logo is now an inline SVG with aria-label instead of img with alt
const logo = screen.getByLabelText("Fusion logo");
expect(logo).toBeDefined();
expect(logo.tagName).toBe("IMG");
expect((logo as HTMLImageElement).src).toContain("/logo.svg");
expect(logo.tagName.toLowerCase()).toBe("svg");
// The SVG should have the currentColor" fill
const circles = logo.querySelectorAll("circle");
expect(circles).toHaveLength(4);
});
it("renders the logo before the h1 element", () => {
render(<Header />);
const logo = screen.getByAltText("Fusion logo");
const logo = screen.getByLabelText("Fusion logo");
const h1 = screen.getByRole("heading", { level: 1 });
// Logo should be a preceding sibling of the h1
expect(logo.compareDocumentPosition(h1) & Node.DOCUMENT_POSITION_FOLLOWING).toBeTruthy();

View File

@@ -334,6 +334,17 @@ describe("InlineCreateCard model selector", () => {
});
});
it("Save button uses theme-driven btn-task-create class", () => {
const { props } = renderCard();
expandCard();
const textarea = screen.getByPlaceholderText("What needs to be done?");
fireEvent.change(textarea, { target: { value: "Some task" } });
// Save button should have the btn-task-create class
const saveButton = screen.getByRole("button", { name: "Save" });
expect(saveButton.className).toContain("btn-task-create");
});
it("includes selected models in the submit payload", async () => {
const { props } = renderCard();
expandCard();

View File

@@ -432,6 +432,14 @@ describe("ListView", () => {
expect(mockOnNewTask).toHaveBeenCalled();
});
it("+ New Task button uses theme-driven btn-task-create class", () => {
const mockOnNewTask = vi.fn();
renderListView({ onNewTask: mockOnNewTask });
const newTaskButton = screen.getByText("+ New Task");
expect(newTaskButton.className).toContain("btn-task-create");
});
it("does not render + New Task button when onNewTask is not provided", () => {
renderListView({ onNewTask: undefined });

View File

@@ -1457,6 +1457,8 @@ describe("QuickEntryBox", () => {
// Button should have data-testid="save-button"
const saveButton = screen.getByTestId("save-button");
expect(saveButton).toBeTruthy();
// Save button uses theme-driven class for task creation CTA
expect(saveButton.className).toContain("btn-task-create");
});
it("save button has correct title attribute", () => {

View File

@@ -1,6 +1,6 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 128 128" fill="none">
<circle cx="44" cy="44" r="20" fill="#58a6ff"/>
<circle cx="84" cy="44" r="20" fill="#58a6ff"/>
<circle cx="44" cy="84" r="20" fill="#58a6ff"/>
<circle cx="84" cy="84" r="20" fill="#58a6ff"/>
<circle cx="44" cy="44" r="20" fill="currentColor"/>
<circle cx="84" cy="44" r="20" fill="currentColor"/>
<circle cx="44" cy="84" r="20" fill="currentColor"/>
<circle cx="84" cy="84" r="20" fill="currentColor"/>
</svg>

Before

Width:  |  Height:  |  Size: 282 B

After

Width:  |  Height:  |  Size: 302 B

View File

@@ -100,6 +100,17 @@
--color-error: #f85149;
--color-error-dark: #da3633;
--color-muted: #8b949e;
/* Logo & branding tokens */
--logo-accent: var(--todo);
/* Task-creation CTA tokens */
--cta-bg: #238636;
--cta-border: #2ea043;
--cta-text: #fff;
--cta-bg-hover: #2ea043;
--cta-border-hover: #3fb950;
--cta-glow: 0 0 8px rgba(46, 160, 67, 0.3);
}
html,
@@ -138,6 +149,7 @@ body {
width: 24px;
height: 24px;
flex-shrink: 0;
color: var(--logo-accent);
}
.header-actions {
display: flex;
@@ -598,6 +610,7 @@ body {
font-size: 20px;
font-weight: 700;
letter-spacing: -0.5px;
color: var(--logo-accent);
}
/* === Buttons === */
.btn {
@@ -640,6 +653,18 @@ body {
box-shadow: var(--shadow-glow);
}
/* Theme-driven task-creation CTA — uses tokens so every color theme updates consistently */
.btn-task-create {
background: var(--cta-bg);
border-color: var(--cta-border);
color: var(--cta-text);
}
.btn-task-create:hover {
background: var(--cta-bg-hover);
border-color: var(--cta-border-hover);
box-shadow: var(--cta-glow);
}
.btn-danger {
background: var(--color-error-dark);
border-color: var(--color-error);
@@ -7982,6 +8007,16 @@ html .column.drag-over * {
background: #0969da;
}
[data-theme="light"] .btn-task-create {
background: #1a7f37;
border-color: #1f883d;
}
[data-theme="light"] .btn-task-create:hover {
background: #1f883d;
box-shadow: var(--shadow-glow);
}
[data-theme="light"] .btn-primary {
background: #1a7f37;
border-color: #1f883d;
@@ -8459,14 +8494,16 @@ html .column.drag-over * {
font-weight: 600;
}
[data-color-theme="factory"] .btn-primary {
[data-color-theme="factory"] .btn-primary,
[data-color-theme="factory"] .btn-task-create {
background: transparent;
border-color: var(--todo);
color: var(--todo);
box-shadow: none;
}
[data-color-theme="factory"] .btn-primary:hover {
[data-color-theme="factory"] .btn-primary:hover,
[data-color-theme="factory"] .btn-task-create:hover {
background: var(--todo);
color: var(--bg);
box-shadow: var(--shadow-glow);