Skip to content

Commit 094dce5

Browse files
committed
feat(core): wire checkbox and radioGroup recipe rendering
1 parent bb31e85 commit 094dce5

4 files changed

Lines changed: 145 additions & 13 deletions

File tree

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
import { assert, describe, test } from "@rezi-ui/testkit";
2+
import { coerceToLegacyTheme } from "../../theme/interop.js";
3+
import { darkTheme } from "../../theme/presets.js";
4+
import { ui } from "../../widgets/ui.js";
5+
import { type DrawOp, renderOps } from "./recipeRendering.test-utils.js";
6+
7+
const dsTheme = coerceToLegacyTheme(darkTheme);
8+
9+
function findTextOp(ops: readonly DrawOp[], text: string): DrawOp | undefined {
10+
return ops.find((op) => op.kind === "drawText" && op.text === text);
11+
}
12+
13+
describe("checkbox recipe rendering", () => {
14+
test("uses recipe colors with semantic-token themes", () => {
15+
const ops = renderOps(ui.checkbox({ id: "cb", checked: false, label: "Option" }), {
16+
viewport: { cols: 40, rows: 3 },
17+
theme: dsTheme,
18+
});
19+
const indicator = findTextOp(ops, "[ ]");
20+
const label = findTextOp(ops, "Option");
21+
assert.ok(indicator && indicator.kind === "drawText");
22+
assert.ok(label && label.kind === "drawText");
23+
if (!indicator || indicator.kind !== "drawText" || !label || label.kind !== "drawText") return;
24+
assert.deepEqual(indicator.style?.fg, dsTheme.colors["fg.secondary"]);
25+
assert.deepEqual(label.style?.fg, dsTheme.colors["fg.primary"]);
26+
});
27+
28+
test("renders checked indicator with selected recipe style", () => {
29+
const uncheckedOps = renderOps(ui.checkbox({ id: "u", checked: false, label: "U" }), {
30+
viewport: { cols: 30, rows: 2 },
31+
theme: dsTheme,
32+
});
33+
const checkedOps = renderOps(ui.checkbox({ id: "c", checked: true, label: "C" }), {
34+
viewport: { cols: 30, rows: 2 },
35+
theme: dsTheme,
36+
});
37+
const unchecked = findTextOp(uncheckedOps, "[ ]");
38+
const checked = findTextOp(checkedOps, "[x]");
39+
assert.ok(unchecked && unchecked.kind === "drawText");
40+
assert.ok(checked && checked.kind === "drawText");
41+
if (!unchecked || unchecked.kind !== "drawText" || !checked || checked.kind !== "drawText")
42+
return;
43+
assert.notDeepEqual(checked.style?.fg, unchecked.style?.fg);
44+
assert.deepEqual(checked.style?.fg, dsTheme.colors["accent.primary"]);
45+
});
46+
47+
test("uses disabled recipe colors", () => {
48+
const ops = renderOps(
49+
ui.checkbox({ id: "d", checked: true, label: "Disabled", disabled: true }),
50+
{
51+
viewport: { cols: 40, rows: 2 },
52+
theme: dsTheme,
53+
},
54+
);
55+
const indicator = findTextOp(ops, "[x]");
56+
const label = findTextOp(ops, "Disabled");
57+
assert.ok(indicator && indicator.kind === "drawText");
58+
assert.ok(label && label.kind === "drawText");
59+
if (!indicator || indicator.kind !== "drawText" || !label || label.kind !== "drawText") return;
60+
assert.deepEqual(indicator.style?.fg, dsTheme.colors["disabled.fg"]);
61+
assert.deepEqual(label.style?.fg, dsTheme.colors["disabled.fg"]);
62+
});
63+
64+
test("uses focus recipe styling when focused", () => {
65+
const ops = renderOps(ui.checkbox({ id: "f", checked: false, label: "Focus me" }), {
66+
viewport: { cols: 40, rows: 2 },
67+
theme: dsTheme,
68+
focusedId: "f",
69+
});
70+
const indicator = findTextOp(ops, "[ ]");
71+
const label = findTextOp(ops, "Focus me");
72+
assert.ok(indicator && indicator.kind === "drawText");
73+
assert.ok(label && label.kind === "drawText");
74+
if (!indicator || indicator.kind !== "drawText" || !label || label.kind !== "drawText") return;
75+
assert.equal(indicator.style?.bold, true);
76+
assert.equal(label.style?.bold, true);
77+
});
78+
});

packages/core/src/renderer/renderToDrawlist/widgets/basic.ts

Lines changed: 50 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1485,6 +1485,8 @@ export function renderBasicWidget(
14851485
checked?: unknown;
14861486
label?: unknown;
14871487
disabled?: unknown;
1488+
dsTone?: unknown;
1489+
dsSize?: unknown;
14881490
};
14891491
const id = typeof props.id === "string" ? props.id : null;
14901492
const focused = id !== null && focusState.focusedId === id;
@@ -1493,14 +1495,23 @@ export function renderBasicWidget(
14931495
const label = typeof props.label === "string" ? props.label : "";
14941496
const indicator = checked ? "[x]" : "[ ]";
14951497
const colorTokens = getColorTokens(theme);
1498+
const dsTone = readWidgetTone(props.dsTone);
1499+
const dsSize = readWidgetSize(props.dsSize) ?? "md";
14961500
builder.pushClip(rect.x, rect.y, rect.w, rect.h);
14971501
if (colorTokens !== null) {
14981502
const state = disabled
14991503
? ("disabled" as const)
15001504
: focused
15011505
? ("focus" as const)
1502-
: ("default" as const);
1503-
const recipeResult = checkboxRecipe(colorTokens, { state, checked });
1506+
: checked
1507+
? ("selected" as const)
1508+
: ("default" as const);
1509+
const recipeResult = checkboxRecipe(
1510+
colorTokens,
1511+
dsTone === undefined
1512+
? { state, checked, size: dsSize }
1513+
: { state, checked, tone: dsTone, size: dsSize },
1514+
);
15041515
const indicatorStyle = mergeTextStyle(parentStyle, recipeResult.indicator);
15051516
const labelStyle = mergeTextStyle(parentStyle, recipeResult.label);
15061517
builder.drawText(rect.x, rect.y, indicator, indicatorStyle);
@@ -1527,6 +1538,8 @@ export function renderBasicWidget(
15271538
options?: unknown;
15281539
direction?: unknown;
15291540
disabled?: unknown;
1541+
dsTone?: unknown;
1542+
dsSize?: unknown;
15301543
};
15311544
const id = typeof props.id === "string" ? props.id : null;
15321545
const focused = id !== null && focusState.focusedId === id;
@@ -1536,20 +1549,47 @@ export function renderBasicWidget(
15361549
const options = Array.isArray(props.options)
15371550
? (props.options as readonly SelectOption[])
15381551
: [];
1539-
1540-
const focusStyle = focused ? { underline: true, bold: true } : undefined;
1541-
const style = mergeTextStyle(
1542-
parentStyle,
1543-
disabled ? { fg: theme.colors.muted, ...focusStyle } : focusStyle,
1544-
);
1552+
const colorTokens = getColorTokens(theme);
1553+
const dsTone = readWidgetTone(props.dsTone);
1554+
const dsSize = readWidgetSize(props.dsSize) ?? "md";
15451555

15461556
builder.pushClip(rect.x, rect.y, rect.w, rect.h);
15471557
let cx = rect.x;
15481558
let cy = rect.y;
15491559
for (const opt of options) {
1550-
const mark = opt.value === value ? "(o)" : "( )";
1560+
const selected = opt.value === value;
1561+
const mark = selected ? "(o)" : "( )";
1562+
if (colorTokens !== null) {
1563+
const state = disabled
1564+
? ("disabled" as const)
1565+
: focused && selected
1566+
? ("focus" as const)
1567+
: selected
1568+
? ("selected" as const)
1569+
: ("default" as const);
1570+
const recipeResult = checkboxRecipe(
1571+
colorTokens,
1572+
dsTone === undefined
1573+
? { state, checked: selected, size: dsSize }
1574+
: { state, checked: selected, tone: dsTone, size: dsSize },
1575+
);
1576+
const indicatorStyle = mergeTextStyle(parentStyle, recipeResult.indicator);
1577+
const labelStyle = mergeTextStyle(parentStyle, recipeResult.label);
1578+
builder.drawText(cx, cy, mark, indicatorStyle);
1579+
if (opt.label.length > 0) {
1580+
builder.drawText(cx + measureTextCells(mark) + 1, cy, opt.label, labelStyle);
1581+
}
1582+
} else {
1583+
const focusStyle = focused ? { underline: true, bold: true } : undefined;
1584+
const style = mergeTextStyle(
1585+
parentStyle,
1586+
disabled ? { fg: theme.colors.muted, ...focusStyle } : focusStyle,
1587+
);
1588+
const chunk = `${mark} ${opt.label}`;
1589+
builder.drawText(cx, cy, chunk, style);
1590+
}
1591+
15511592
const chunk = `${mark} ${opt.label}`;
1552-
builder.drawText(cx, cy, chunk, style);
15531593
if (direction === "horizontal") {
15541594
cx += measureTextCells(chunk) + 2;
15551595
} else {

packages/core/src/ui/recipes.ts

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -513,6 +513,8 @@ export function dividerRecipe(colors: ColorTokens): DividerRecipeResult {
513513
export type CheckboxRecipeParams = Readonly<{
514514
state?: WidgetState;
515515
checked?: boolean;
516+
tone?: WidgetTone;
517+
size?: WidgetSize;
516518
}>;
517519

518520
export type CheckboxRecipeResult = Readonly<{
@@ -528,6 +530,10 @@ export function checkboxRecipe(
528530
): CheckboxRecipeResult {
529531
const state = params.state ?? "default";
530532
const checked = params.checked ?? false;
533+
const tone = params.tone ?? "default";
534+
const large = params.size === "lg";
535+
const selected = checked || state === "selected";
536+
const selectedColor = resolveToneColor(colors, tone);
531537

532538
if (state === "disabled") {
533539
return {
@@ -539,12 +545,12 @@ export function checkboxRecipe(
539545
const isFocused = state === "focus";
540546
return {
541547
indicator: {
542-
fg: checked ? colors.accent.primary : colors.fg.secondary,
543-
bold: isFocused,
548+
fg: selected ? selectedColor : colors.fg.secondary,
549+
bold: isFocused || large,
544550
},
545551
label: {
546552
fg: colors.fg.primary,
547-
bold: isFocused,
553+
bold: isFocused || large,
548554
},
549555
};
550556
}

packages/core/src/widgets/types.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1398,6 +1398,10 @@ export type CheckboxProps = Readonly<{
13981398
onChange?: (checked: boolean) => void;
13991399
/** Whether the checkbox is disabled. */
14001400
disabled?: boolean;
1401+
/** Design system: tone for checked/focus rendering. */
1402+
dsTone?: WidgetTone;
1403+
/** Design system: size preset. */
1404+
dsSize?: WidgetSize;
14011405
}>;
14021406

14031407
/** Props for radio group widget. */
@@ -1418,6 +1422,10 @@ export type RadioGroupProps = Readonly<{
14181422
direction?: "horizontal" | "vertical";
14191423
/** Whether the radio group is disabled. */
14201424
disabled?: boolean;
1425+
/** Design system: tone for selected/focus rendering. */
1426+
dsTone?: WidgetTone;
1427+
/** Design system: size preset. */
1428+
dsSize?: WidgetSize;
14211429
}>;
14221430

14231431
/* ========== Navigation Widgets ========== */

0 commit comments

Comments
 (0)