feat(FN-3042): scale board typography and improve theme selector accessibil

Completes Step 2 of FN-3042 by scaling board typography styles across TaskCard and QuickEntryBox components, adding a new CustomModelDropdown stylesheet, and introducing keyboard focus-visible styles for theme selector controls. The merge includes new regression tests for board font scaling, resolve

Fusion-Task-Id: FN-3042
This commit is contained in:
Fusion
2026-04-30 23:22:22 -07:00
committed by gsxdsm
parent 05c942b6ed
commit 444032c58d
9 changed files with 177 additions and 64 deletions

View File

@@ -16,8 +16,8 @@ describe("mobile input font size CSS", () => {
const quickEntryMatch = css.match(/\.quick-entry-input\s*\{[^}]*\}/);
expect(quickEntryMatch).not.toBeNull();
// Should have 13px font-size on desktop
expect(quickEntryMatch![0]).toContain("font-size: 13px");
// Should keep quick entry typography below the mobile 16px override baseline
expect(quickEntryMatch![0]).toContain("font-size: 0.8125rem");
});
it("form-group textarea has desktop font-size below 16px", () => {

View File

@@ -609,6 +609,12 @@ html .column.drag-over * {
color: var(--bg);
}
.theme-mode-btn:focus-visible {
outline: none;
border-color: var(--todo);
box-shadow: var(--focus-ring-strong);
}
.theme-section-title {
font-size: 12px;
font-weight: 600;
@@ -652,6 +658,12 @@ html .column.drag-over * {
color: var(--text);
}
.theme-font-size-btn:focus-visible {
outline: none;
border-color: var(--todo);
box-shadow: var(--focus-ring-strong);
}
.theme-grid {
display: grid;
grid-template-columns: repeat(4, 1fr);
@@ -687,6 +699,12 @@ html .column.drag-over * {
background: color-mix(in srgb, var(--todo) 14%, transparent);
}
.theme-option:focus-visible {
outline: none;
border-color: var(--todo);
box-shadow: var(--focus-ring-strong);
}
.theme-option-swatch {
width: calc(var(--space-2xl) + var(--space-lg));
height: calc(var(--space-2xl) + var(--space-lg));
@@ -1446,6 +1464,12 @@ html .column.drag-over * {
border-color: var(--text-muted);
}
.theme-reset-btn:focus-visible {
outline: none;
border-color: var(--todo);
box-shadow: var(--focus-ring-strong);
}
/* Theme current preview in settings */
.theme-current-preview {
display: flex;

View File

@@ -18,7 +18,7 @@
border: none;
border-bottom: 1px solid var(--border);
color: var(--text);
font-size: 13px;
font-size: 0.8125rem;
line-height: 1.4;
font-family: inherit;
outline: none;
@@ -88,7 +88,7 @@
min-height: unset;
resize: none;
border-radius: var(--radius-md);
font-size: 15px;
font-size: 0.9375rem;
line-height: 1.6;
}
@@ -149,14 +149,14 @@
.node-picker-item-status {
margin-left: auto;
color: var(--text-muted);
font-size: 12px;
font-size: 0.75rem;
}
.quick-entry-subtasks-toggle {
display: inline-flex;
align-items: center;
gap: 6px;
font-size: 12px;
font-size: 0.75rem;
color: var(--text-muted);
cursor: pointer;
margin-left: 8px;
@@ -171,14 +171,14 @@
}
.quick-entry-hint {
font-size: 11px;
font-size: 0.6875rem;
color: var(--text-dim);
margin-left: auto;
white-space: nowrap;
}
.quick-entry-model-trigger {
font-size: 12px;
font-size: 0.75rem;
padding: 3px 8px;
}
@@ -226,7 +226,7 @@
color: var(--text-primary);
cursor: pointer;
text-align: left;
font-size: 13px;
font-size: 0.8125rem;
transition: background-color 0.15s;
}
@@ -248,7 +248,7 @@
.model-menu-item-value {
flex: 1;
text-align: right;
font-size: 11px;
font-size: 0.6875rem;
color: var(--text-dim);
overflow: hidden;
text-overflow: ellipsis;
@@ -270,7 +270,7 @@
border: none;
color: var(--text-muted);
cursor: pointer;
font-size: 12px;
font-size: 0.75rem;
padding: 4px 0;
width: fit-content;
}
@@ -280,7 +280,7 @@
}
.model-submenu-header {
font-size: 12px;
font-size: 0.75rem;
font-weight: 600;
color: var(--text-primary);
padding-bottom: 4px;
@@ -292,7 +292,7 @@
align-items: center;
justify-content: space-between;
gap: 8px;
font-size: 11px;
font-size: 0.6875rem;
color: var(--text-error, #e53e3e);
padding-top: 4px;
}
@@ -449,7 +449,7 @@
}
.agent-list-modal .agent-state-filter-select {
font-size: 16px;
font-size: 1rem;
min-height: 44px;
}
@@ -497,7 +497,7 @@
.agent-list-modal .agent-create-form .input,
.agent-list-modal .agent-create-form .select {
width: 100%;
font-size: 16px;
font-size: 1rem;
min-height: 44px;
}

View File

@@ -67,14 +67,14 @@
.card-id {
display: inline-block;
font-size: 11px;
font-size: 0.6875rem;
font-weight: 600;
color: var(--text-muted);
font-family: var(--font-mono);
}
.card-title {
font-size: 13px;
font-size: 0.8125rem;
line-height: 1.4;
word-break: break-word;
}
@@ -85,7 +85,7 @@
align-items: center;
gap: var(--space-sm);
margin-top: var(--space-sm);
font-size: 11px;
font-size: 0.6875rem;
color: var(--text-dim);
position: relative;
z-index: 1;
@@ -94,7 +94,7 @@
.card-status-badge {
display: inline-flex;
align-items: center;
font-size: 10px;
font-size: 0.625rem;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.5px;
@@ -170,7 +170,7 @@
.card-size-badge {
display: inline-flex;
align-items: center;
font-size: 10px;
font-size: 0.625rem;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.5px;
@@ -199,7 +199,7 @@
.card-priority-badge {
display: inline-flex;
align-items: center;
font-size: 10px;
font-size: 0.625rem;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.4px;
@@ -225,7 +225,7 @@
.card-execution-mode-badge {
display: inline-flex;
align-items: center;
font-size: 10px;
font-size: 0.625rem;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.4px;
@@ -252,14 +252,14 @@
background: color-mix(in srgb, var(--color-error) 10%, transparent);
border: var(--btn-border-width) solid color-mix(in srgb, var(--color-error-dark) 25%, transparent);
border-radius: var(--radius-sm);
font-size: 11px;
font-size: 0.6875rem;
color: var(--color-error-dark);
line-height: 1.4;
}
.card-error-icon {
flex-shrink: 0;
font-size: 12px;
font-size: 0.75rem;
}
.card-error-text {
@@ -336,7 +336,7 @@
display: inline-flex;
align-items: center;
gap: 3px;
font-size: 11px;
font-size: 0.6875rem;
color: var(--triage);
position: relative;
cursor: default;
@@ -363,7 +363,7 @@
display: inline-flex;
align-items: center;
gap: 3px;
font-size: 11px;
font-size: 0.6875rem;
color: var(--in-progress);
position: relative;
cursor: default;
@@ -380,7 +380,7 @@
border: 1px solid var(--border);
border-radius: var(--radius-sm);
padding: var(--space-xs) var(--space-sm);
font-size: 11px;
font-size: 0.6875rem;
white-space: nowrap;
z-index: 10;
pointer-events: none;
@@ -393,7 +393,7 @@
display: inline-flex;
align-items: center;
gap: 3px;
font-size: 10px;
font-size: 0.625rem;
font-weight: 600;
font-family: var(--font-mono);
color: var(--badge-mission-text);
@@ -436,7 +436,7 @@
display: inline-flex;
align-items: center;
gap: 4px;
font-size: 10px;
font-size: 0.625rem;
font-weight: 600;
color: var(--text-muted);
background: color-mix(in srgb, var(--text-muted) 18%, transparent);
@@ -483,7 +483,7 @@
border: none;
background: transparent;
color: var(--text-muted);
font-size: 12px;
font-size: 0.75rem;
cursor: pointer;
min-width: 0;
flex: 1 1 auto;
@@ -521,7 +521,7 @@
border-radius: var(--radius-pill);
background: color-mix(in srgb, var(--text-muted) 12%, transparent);
color: var(--text-muted);
font-size: 11px;
font-size: 0.6875rem;
font-weight: 600;
font-family: var(--font-mono);
line-height: 1;
@@ -543,7 +543,7 @@
}
.card-progress-label {
font-size: 11px;
font-size: 0.6875rem;
font-weight: 600;
color: var(--text-muted);
font-family: var(--font-mono);
@@ -561,7 +561,7 @@
background: none;
border: none;
cursor: pointer;
font-size: calc(var(--space-sm) + var(--space-xs) * 0.75);
font-size: 0.6875rem;
color: var(--text-muted);
transition: color var(--transition-fast);
}
@@ -596,7 +596,7 @@
display: flex;
align-items: center;
gap: 6px;
font-size: 12px;
font-size: 0.75rem;
}
.card-step-dot {
@@ -649,7 +649,7 @@
padding: 0 var(--space-xs);
border-radius: var(--radius-pill);
border: 1px solid var(--border);
font-size: calc(var(--space-sm) + var(--space-xs) * 0.25);
font-size: 0.5625rem;
color: var(--text-muted);
background: var(--surface);
white-space: nowrap;
@@ -718,7 +718,7 @@
background: transparent;
border: 1px solid transparent;
color: var(--text);
font-size: 13px;
font-size: 0.8125rem;
font-family: inherit;
outline: none;
resize: none;
@@ -823,7 +823,7 @@
padding: calc(var(--space-xs) / 4)
calc(var(--space-sm) - (var(--space-xs) / 2));
margin-left: var(--space-xs);
font-size: calc(var(--space-sm) + (var(--space-xs) / 2));
font-size: 0.625rem;
font-weight: 500;
background: var(--card);
border: 1px solid var(--border);
@@ -865,7 +865,7 @@
padding: calc(var(--space-xs) / 4)
calc(var(--space-sm) - (var(--space-xs) / 2));
margin-left: var(--space-xs);
font-size: calc(var(--space-sm) + (var(--space-xs) / 2));
font-size: 0.625rem;
font-weight: 500;
background: var(--card);
border: 1px solid var(--border);
@@ -909,7 +909,7 @@
display: block;
width: 100%;
padding: 6px 12px;
font-size: 12px;
font-size: 0.75rem;
font-weight: 400;
color: var(--text);
background: transparent;
@@ -934,7 +934,7 @@
align-items: center;
gap: var(--space-sm);
margin-top: var(--space-xs);
font-size: calc(var(--space-sm) + var(--space-xs) * 0.75);
font-size: 0.6875rem;
color: var(--text-muted);
}
@@ -948,7 +948,7 @@
}
.card-edit-loading-text {
font-size: calc(var(--space-sm) + var(--space-xs) * 0.75);
font-size: 0.6875rem;
color: var(--text-muted);
}
@@ -1009,7 +1009,7 @@
}
.card-progress-label {
font-size: 10px;
font-size: 0.625rem;
min-width: 30px;
}
@@ -1024,12 +1024,12 @@
.card-time-indicator {
padding: var(--space-xs) var(--space-sm);
font-size: 10px;
font-size: 0.625rem;
}
/* Card: smaller status badges for 280px width */
.card-status-badge {
font-size: 9px;
font-size: 0.5625rem;
padding: 1px 6px;
}

View File

@@ -0,0 +1,85 @@
import { describe, it, expect, beforeAll, afterAll } from "vitest";
import { loadAllAppCss } from "../../test/cssFixture";
function effectivePxForFontSize(fontSize: string, rootPx: number): number {
if (fontSize.endsWith("rem")) {
return Number.parseFloat(fontSize) * rootPx;
}
return Number.parseFloat(fontSize);
}
describe("board font scale", () => {
let styleEl: HTMLStyleElement;
beforeAll(() => {
styleEl = document.createElement("style");
styleEl.textContent = loadAllAppCss();
document.head.appendChild(styleEl);
});
afterAll(() => {
styleEl.remove();
document.documentElement.style.fontSize = "";
document.body.innerHTML = "";
});
it("scales board typography when dashboard root font scale changes", () => {
document.body.innerHTML = `
<div class="column-header"><h2>Todo</h2></div>
<div class="column-count">3</div>
<div class="column-desc">work</div>
<article class="card">
<div class="card-id">FN-1</div>
<div class="card-title">Task title</div>
<div class="card-meta">meta</div>
</article>
<div class="quick-entry-box">
<textarea class="quick-entry-input"></textarea>
<span class="quick-entry-hint">Hint</span>
</div>
`;
const titleEl = document.querySelector(".card-title") as Element;
const headerEl = document.querySelector(".column-header h2") as Element;
const quickEl = document.querySelector(".quick-entry-input") as Element;
const countEl = document.querySelector(".column-count") as Element;
const getPx = (el: Element): number => {
const fontSize = getComputedStyle(el).fontSize;
const rootInline = document.documentElement.style.fontSize;
const rootPx = rootInline.endsWith("%")
? (Number.parseFloat(rootInline) / 100) * 16
: Number.parseFloat(getComputedStyle(document.documentElement).fontSize);
return effectivePxForFontSize(fontSize, rootPx);
};
document.documentElement.style.fontSize = "85%";
const title85 = getPx(titleEl);
const header85 = getPx(headerEl);
const quick85 = getPx(quickEl);
document.documentElement.style.fontSize = "100%";
const title100 = getPx(titleEl);
const header100 = getPx(headerEl);
const quick100 = getPx(quickEl);
const count100 = getPx(countEl);
document.documentElement.style.fontSize = "125%";
const title125 = getPx(titleEl);
const header125 = getPx(headerEl);
const quick125 = getPx(quickEl);
expect(title100).toBeCloseTo(13, 2);
expect(header100).toBeCloseTo(14, 2);
expect(quick100).toBeCloseTo(13, 2);
expect(count100).toBeCloseTo(12, 2);
expect(title85).toBeCloseTo(11.05, 2);
expect(header85).toBeCloseTo(11.9, 2);
expect(quick85).toBeCloseTo(11.05, 2);
expect(title125).toBeCloseTo(16.25, 2);
expect(header125).toBeCloseTo(17.5, 2);
expect(quick125).toBeCloseTo(16.25, 2);
});
});

View File

@@ -908,7 +908,7 @@
[data-color-theme="factory"] .column-header h2 {
text-transform: uppercase;
letter-spacing: 0.1em;
font-size: 12px;
font-size: 0.75rem;
}
[data-color-theme="factory"] .column-count,

View File

@@ -821,7 +821,7 @@ body {
}
.column-header h2 {
font-size: 14px;
font-size: 0.875rem;
font-weight: 600;
flex: 1;
min-width: 0;
@@ -831,7 +831,7 @@ body {
}
.column-count {
font-size: 12px;
font-size: 0.75rem;
color: var(--text-muted);
background: var(--card);
padding: calc(var(--space-xs) / 2) var(--space-sm);
@@ -854,7 +854,7 @@ body {
}
.column-desc {
font-size: 11px;
font-size: 0.6875rem;
color: var(--text-dim);
padding: var(--space-xs) calc(var(--space-lg) - 2px) calc(var(--space-md) - 2px);
}
@@ -2248,7 +2248,7 @@ input[type="range"]:focus-visible {
}
.step-progress-label {
font-size: 11px;
font-size: 0.6875rem;
font-weight: 600;
color: var(--text-muted);
font-family: var(--font-mono);
@@ -2894,7 +2894,7 @@ input[type="range"]:focus-visible {
}
.toggle-label {
font-size: calc(var(--space-sm) + var(--space-xs) * 0.75);
font-size: 0.6875rem;
color: var(--text-muted);
white-space: nowrap;
}

View File

@@ -31,7 +31,7 @@ export type AgentOnboardingStreamEvent =
export type AgentOnboardingStreamCallback = (event: AgentOnboardingStreamEvent, eventId?: number) => void;
const createFnAgent = engineCreateFnAgent;
const createFnAgent: typeof engineCreateFnAgent = engineCreateFnAgent;
const SESSION_TTL_MS = 30 * 60 * 1000;
const CLEANUP_INTERVAL_MS = 5 * 60 * 1000;
const GENERATION_TIMEOUT_MS = 120_000;
@@ -140,23 +140,27 @@ export function parseAgentOnboardingResponse(text: string): { type: "question";
} catch {
parsed = JSON.parse(repairJson(candidate));
}
if (
!parsed ||
typeof parsed !== "object" ||
!("type" in parsed) ||
((parsed as { type?: unknown }).type !== "question" &&
(parsed as { type?: unknown }).type !== "complete")
) {
if (typeof parsed !== "object" || parsed === null || !("type" in parsed)) {
throw new Error("AI returned invalid response type");
}
if ((parsed as { type: string }).type === "complete") {
const data = (parsed as { data?: Record<string, unknown> }).data ?? {};
const typed = parsed as { type?: unknown; data?: unknown };
if (typed.type !== "question" && typed.type !== "complete") {
throw new Error("AI returned invalid response type");
}
if (typed.type === "complete") {
const data = (typed.data ?? {}) as { name?: unknown; instructionsText?: unknown; maxTurns?: unknown };
if (typeof data.name !== "string" || !data.name.trim()) throw new Error("Invalid summary.name");
if (typeof data.instructionsText !== "string" || !data.instructionsText.trim()) throw new Error("Invalid summary.instructionsText");
const maxTurns = data.maxTurns;
if (!Number.isInteger(maxTurns) || (maxTurns as number) <= 0) throw new Error("Invalid summary.maxTurns");
if (typeof maxTurns !== "number" || !Number.isInteger(maxTurns) || maxTurns <= 0) {
throw new Error("Invalid summary.maxTurns");
}
return { type: "complete", data: typed.data as AgentOnboardingSummary };
}
return parsed as { type: "question"; data: PlanningQuestion } | { type: "complete"; data: AgentOnboardingSummary };
return { type: "question", data: typed.data as PlanningQuestion };
}
export function createAgentOnboardingSessionPrompt(input: {
@@ -231,8 +235,8 @@ async function runGenerationWithTimeout<T>(session: Session, operation: () => Pr
}
async function continueConversation(session: Session, message: string): Promise<void> {
if (!session.agent) throw new Error("Session agent not initialized");
const agent = session.agent;
if (!agent) throw new Error("Session agent not initialized");
session.thinkingOutput = "";
try {
await runGenerationWithTimeout(session, async () => {