feat(ai): add_status_badge_v0 — semantic status indicator (51st tool)

Small colored dot + short label: "● Online" / "● Busy" / "● Error"
pattern. Distinguished from the more general add_badge_v0 (just a
pill label) by always having a dot.

tone enum picks dot color:
- success → emerald #10B981
- warning → amber   #F59E0B
- error   → red     #EF4444
- info    → blue    #3B82F6
- neutral → slate   #94A3B8 (default)

Dot uses `frame + cornerRadius=4`, NEVER `ellipse` — an 8×8 ellipse
is the classic "status dot via stacked ellipses" anti-pattern bait.
Keeping it a frame stays clean of rewriteLlmAntiPatterns. Regression
test locked in pen-mcp/add-status-badge-v0.test.ts.

Wired across all three paths + elements.md entry + keyword map +
examples + ab-v1/dashboard-server-status.yaml corpus prompt +
parametric builder test CASES in 7 files.

Tool count: 50 → 51. Test count: 3114 → 3138 (+24).
This commit is contained in:
Fini 2026-04-22 08:50:00 +08:00
parent c6e938e66a
commit baa414bded
21 changed files with 311 additions and 6 deletions

View file

@ -43,6 +43,7 @@ import {
buildSelect,
buildSkeleton,
buildStatGrid,
buildStatusBadge,
buildStepper,
buildSwitch,
buildTabs,
@ -146,6 +147,7 @@ const SERVER_BUILDERS: Record<string, BuilderFn> = {
buildImagePlaceholder(a as Parameters<typeof buildImagePlaceholder>[0]),
add_comment_v0: (a) => buildComment(a as Parameters<typeof buildComment>[0]),
add_modal_shell_v0: (a) => buildModalShell(a as Parameters<typeof buildModalShell>[0]),
add_status_badge_v0: (a) => buildStatusBadge(a as Parameters<typeof buildStatusBadge>[0]),
add_nav_chip_row_v0: (a) => buildNavChipRow(a as Parameters<typeof buildNavChipRow>[0]),
add_activity_ring_v0: (a) => buildActivityRing(a as Parameters<typeof buildActivityRing>[0]),
add_rating_stars_v0: (a) => buildRatingStars(a as Parameters<typeof buildRatingStars>[0]),

View file

@ -52,6 +52,7 @@ import {
buildSelect,
buildSkeleton,
buildStatGrid,
buildStatusBadge,
buildStepper,
buildSwitch,
buildTabs,
@ -189,6 +190,7 @@ const CASES: BuilderCase[] = [
build: () => buildComment({ author: 'Alice', body: 'Great post!', avatar_initial: 'A' }),
},
{ name: 'modal-shell', build: () => buildModalShell({ title: 'Confirm' }) },
{ name: 'status-badge', build: () => buildStatusBadge({ label: 'Online', tone: 'success' }) },
{ name: 'chart-bars', build: () => buildChartBars({ values: [4, 7, 3, 9, 5] }) },
{
name: 'empty-state',

View file

@ -44,6 +44,7 @@ import {
buildSelect,
buildSkeleton,
buildStatGrid,
buildStatusBadge,
buildStepper,
buildSwitch,
buildTabs,
@ -183,6 +184,7 @@ const CASES: BuilderCase[] = [
build: () => buildComment({ author: 'Alice', body: 'Great post!', avatar_initial: 'A' }),
},
{ name: 'modal-shell', build: () => buildModalShell({ title: 'Confirm' }) },
{ name: 'status-badge', build: () => buildStatusBadge({ label: 'Online', tone: 'success' }) },
{ name: 'chart-bars', build: () => buildChartBars({ values: [4, 7, 3, 9, 5] }) },
{
name: 'empty-state',

View file

@ -53,6 +53,7 @@ import {
buildSelect,
buildSkeleton,
buildStatGrid,
buildStatusBadge,
buildStepper,
buildSwitch,
buildTabs,
@ -206,6 +207,7 @@ const CASES: BuilderCase[] = [
build: () => buildComment({ author: 'Alice', body: 'Great post!', avatar_initial: 'A' }),
},
{ name: 'modal-shell', build: () => buildModalShell({ title: 'Confirm' }) },
{ name: 'status-badge', build: () => buildStatusBadge({ label: 'Online', tone: 'success' }) },
{ name: 'chart-bars', build: () => buildChartBars({ values: [4, 7, 3, 9, 5] }) },
{
name: 'empty-state',

View file

@ -50,6 +50,7 @@ import {
buildSelect,
buildSkeleton,
buildStatGrid,
buildStatusBadge,
buildStepper,
buildSwitch,
buildTabs,
@ -285,6 +286,11 @@ const CASES: BuilderCase[] = [
args: { title: 'Confirm', subtitle: 'Are you sure?' },
build: (a) => buildModalShell(a as unknown as Parameters<typeof buildModalShell>[0]),
},
{
toolName: 'add_status_badge_v0',
args: { label: 'Online', tone: 'success' },
build: (a) => buildStatusBadge(a as unknown as Parameters<typeof buildStatusBadge>[0]),
},
{
toolName: 'add_form_field_v0',
args: { label: 'Email' },

View file

@ -61,6 +61,7 @@ import {
buildSelect,
buildSkeleton,
buildStatGrid,
buildStatusBadge,
buildStepper,
buildSwitch,
buildTabs,
@ -112,6 +113,7 @@ import {
type SelectParams,
type SkeletonParams,
type StatGridParams,
type StatusBadgeParams,
type StepperParams,
type SwitchParams,
type TabsParams,
@ -247,6 +249,7 @@ export const ELEMENT_SHIMS: Record<string, ElementShim> = {
add_image_placeholder_v0: wrap<ImagePlaceholderParams>(buildImagePlaceholder),
add_comment_v0: wrap<CommentParams>(buildComment),
add_modal_shell_v0: wrap<ModalShellParams>(buildModalShell),
add_status_badge_v0: wrap<StatusBadgeParams>(buildStatusBadge),
add_nav_chip_row_v0: wrap<NavChipRowParams>(buildNavChipRow),
add_activity_ring_v0: wrap<ActivityRingParams>(buildActivityRing),
add_rating_stars_v0: wrap<RatingStarsParams>(buildRatingStars),

View file

@ -1,11 +1,11 @@
# A/B Corpus v1
Supplemental corpus covering the 8 element tools added after the
Supplemental corpus covering the 9 element tools added after the
ab-v0 baseline was frozen (2026-04-20). Intended for an A/B v2 run
that measures routing + legality on the new tool surface without
re-running the full 24-prompt v0 corpus.
Split: 8 `obvious` prompts, one per new tool. No `optional` prompts
Split: 9 `obvious` prompts, one per new tool. No `optional` prompts
in v1 — the new tools are narrow enough that any reasonable design
request either maps to them directly or doesn't, and the optional
slot is already well-populated in v0.

View file

@ -0,0 +1,15 @@
id: dashboard-server-status
category: dashboard
difficulty: obvious
prompt: |
Design ONLY a single status indicator: a small green dot (about
8px) with the label "Online" next to it, suggesting a server or
service is healthy. Compact inline layout. Do NOT include
surrounding dashboard chrome, service name, metrics, or any
other content — just the dot + label.
expected:
must_contain_roles:
- status-badge
- status-dot
- status-label
expected_tool_if_any: add_status_badge_v0

View file

@ -129,6 +129,10 @@ Chrome / modals:
46. Modal dialog shell (dimmed backdrop + centered card + title — body composed separately) → `add_modal_shell_v0`
Status / presence:
47. Status badge (small colored dot + short label, "● Online" pattern, tone-enum'd) → `add_status_badge_v0`
Activity / history:
41. Vertical timeline (dots + fixed 24px connectors + content; no padding/gap) → `add_timeline_v0`
@ -176,6 +180,7 @@ PREFER an element tool when the spec says any of:
- "image placeholder", "photo slot", "upload zone", "hero image area", "cover placeholder", "图片占位" → `add_image_placeholder_v0`
- "comment", "reply", "feedback row", "review row", "评论" → `add_comment_v0`
- "modal", "dialog", "popup", "confirm dialog", "模态框", "弹窗" → `add_modal_shell_v0`
- "status", "online indicator", "presence dot", "health status", "busy indicator", "状态", "在线" → `add_status_badge_v0`
- "toggle", "switch", "on/off", "开关" → `add_switch_v0`
- "checkbox", "agreement", "select option", "复选框" → `add_checkbox_v0`
- "radio", "single choice", "单选" → `add_radio_v0` (stack multiple in a vertical parent)
@ -313,6 +318,8 @@ add_chart_pie_v0({ values: [1, 1, 1, 1], inner_radius_ratio: 0.5 }) // donut
add_image_placeholder_v0({ width: 320, height: 200, label: "Upload cover" })
add_comment_v0({ author: "Sarah", timestamp: "2h ago", body: "Looks great!", avatar_initial: "S" })
add_modal_shell_v0({ title: "Confirm delete", subtitle: "This cannot be undone." })
add_status_badge_v0({ label: "Online", tone: "success" })
add_status_badge_v0({ label: "Degraded", tone: "warning" })
add_switch_v0({}) // off (default)
add_switch_v0({ active: true }) // on — iOS green

View file

@ -69,17 +69,17 @@ describe('loadCorpus — v1 supplemental corpus (new tools)', () => {
// comment, modal_shell). All obvious — one prompt per tool so an
// A/B v2 run can measure routing + legality on the new surface
// without re-running all 24 v0 prompts. See `corpus/ab-v1/README.md`.
it('loads 8 prompts, all obvious, one per new tool', () => {
it('loads 9 prompts, all obvious, one per new tool', () => {
const prompts = loadCorpus(REPO_CORPUS_V1_DIR);
expect(prompts).toHaveLength(8);
expect(new Set(prompts.map((p) => p.id)).size).toBe(8);
expect(prompts).toHaveLength(9);
expect(new Set(prompts.map((p) => p.id)).size).toBe(9);
for (const p of prompts) {
expect(p.difficulty).toBe('obvious');
expect(p.expected_tool_if_any).toMatch(/^add_[a-z_]+_v0$/);
}
});
it('covers the 8 specific tools added 2026-04-22', () => {
it('covers the 9 specific tools added 2026-04-22', () => {
const prompts = loadCorpus(REPO_CORPUS_V1_DIR);
const tools = new Set(prompts.map((p) => p.expected_tool_if_any));
expect(tools).toEqual(
@ -92,6 +92,7 @@ describe('loadCorpus — v1 supplemental corpus (new tools)', () => {
'add_image_placeholder_v0',
'add_comment_v0',
'add_modal_shell_v0',
'add_status_badge_v0',
]),
);
});

View file

@ -46,6 +46,7 @@ import {
buildSelect,
buildSkeleton,
buildStatGrid,
buildStatusBadge,
buildStepper,
buildSwitch,
buildTabs,
@ -199,6 +200,11 @@ const CASES: LayoutCase[] = [
wrap: true,
},
{ name: 'buildModalShell', tree: () => buildModalShell({ title: 'Confirm' }), wrap: true },
{
name: 'buildStatusBadge',
tree: () => buildStatusBadge({ label: 'Online', tone: 'success' }),
wrap: true,
},
{ name: 'buildChartBars', tree: () => buildChartBars({ values: [4, 7, 3, 9, 5] }) },
// Composites

View file

@ -44,6 +44,7 @@ import {
buildSelect,
buildSkeleton,
buildStatGrid,
buildStatusBadge,
buildStepper,
buildSwitch,
buildTabs,
@ -185,6 +186,7 @@ const CASES: BuilderCase[] = [
build: () => buildComment({ author: 'Alice', body: 'Great post!', avatar_initial: 'A' }),
},
{ name: 'modal-shell', build: () => buildModalShell({ title: 'Confirm' }) },
{ name: 'status-badge', build: () => buildStatusBadge({ label: 'Online', tone: 'success' }) },
{ name: 'chart-bars', build: () => buildChartBars({ values: [4, 7, 3, 9, 5] }) },
{
name: 'empty-state',

View file

@ -42,6 +42,7 @@ import {
buildSelect,
buildSkeleton,
buildStatGrid,
buildStatusBadge,
buildStepper,
buildSwitch,
buildTabs,
@ -165,6 +166,7 @@ const CASES: PerfCase[] = [
build: () => buildComment({ author: 'Alice', body: 'Great post!', avatar_initial: 'A' }),
},
{ name: 'modal-shell', build: () => buildModalShell({ title: 'Confirm' }) },
{ name: 'status-badge', build: () => buildStatusBadge({ label: 'Online', tone: 'success' }) },
{ name: 'chart-bars', build: () => buildChartBars({ values: [4, 7, 3, 9, 5, 8] }) },
{
name: 'empty-state',

View file

@ -44,6 +44,7 @@ import {
buildSelect,
buildSkeleton,
buildStatGrid,
buildStatusBadge,
buildStepper,
buildSwitch,
buildTabs,
@ -189,6 +190,7 @@ const CASES: BuilderCase[] = [
build: () => buildComment({ author: 'Alice', body: 'Great post!', avatar_initial: 'A' }),
},
{ name: 'modal-shell', build: () => buildModalShell({ title: 'Confirm' }) },
{ name: 'status-badge', build: () => buildStatusBadge({ label: 'Online', tone: 'success' }) },
{ name: 'chart-bars', build: () => buildChartBars({ values: [4, 7, 3, 9, 5] }) },
{
name: 'empty-state',

View file

@ -45,6 +45,7 @@ export { buildChartPie, type ChartPieParams } from './chart-pie.js';
export { buildImagePlaceholder, type ImagePlaceholderParams } from './image-placeholder.js';
export { buildComment, type CommentParams } from './comment.js';
export { buildModalShell, type ModalShellParams } from './modal-shell.js';
export { buildStatusBadge, type StatusBadgeParams, type StatusBadgeTone } from './status-badge.js';
export { buildNavChipRow, type NavChipRowItem, type NavChipRowParams } from './nav-chip-row.js';
export { buildActivityRing, type ActivityRingParams } from './activity-ring.js';
export { buildRatingStars, type RatingStarsParams } from './rating-stars.js';

View file

@ -0,0 +1,79 @@
import type { ElementTree } from './helpers.js';
export type StatusBadgeTone = 'success' | 'warning' | 'error' | 'info' | 'neutral';
export interface StatusBadgeParams {
label: string;
/**
* Semantic tone — picks the dot color + subtle text color. When
* callers want a custom color palette, override via a follow-up
* batch_design U-op on the returned nodes. Default 'neutral'.
*/
tone?: StatusBadgeTone;
}
/**
* Status indicator pill: small colored dot + short label. The "●
* Online" / "● Busy" / "● Error" pattern. Always emits a dot
* (that's what makes it visually a "status") so it stays distinct
* from the more general `add_badge_v0` which is just a pill label.
*
* Structure:
* frame(fit_content, horizontal, gap=6, alignItems=center)
* ├ frame(8×8, cornerRadius=4, fill=<toneColor>, role='status-dot')
* └ text(label, 13/500)
*
* Dot uses `frame + cornerRadius=4` (pill), NOT `ellipse` — a small
* 8×8 ellipse is the classic "status dot = stacked ellipses" anti-
* pattern bait. Keep it a frame to stay clean of rewriteLlmAntiPatterns.
*/
export function buildStatusBadge(params: StatusBadgeParams): ElementTree {
const tone: StatusBadgeTone = params.tone ?? 'neutral';
const dotColor = dotColorForTone(tone);
return {
type: 'frame',
name: 'Status Badge',
role: 'status-badge',
width: 'fit_content',
height: 'fit_content',
layout: 'horizontal',
alignItems: 'center',
gap: 6,
children: [
{
type: 'frame',
name: 'Status Dot',
role: 'status-dot',
width: 8,
height: 8,
cornerRadius: 4,
fill: [{ type: 'solid', color: dotColor }],
},
{
type: 'text',
name: 'Label',
role: 'status-label',
content: params.label,
fontSize: 13,
fontWeight: 500,
},
],
};
}
function dotColorForTone(tone: StatusBadgeTone): string {
switch (tone) {
case 'success':
return '#10B981'; // emerald-500
case 'warning':
return '#F59E0B'; // amber-500
case 'error':
return '#EF4444'; // red-500
case 'info':
return '#3B82F6'; // blue-500
case 'neutral':
default:
return '#94A3B8'; // slate-400
}
}

View file

@ -211,6 +211,7 @@ export {
buildImagePlaceholder,
buildComment,
buildModalShell,
buildStatusBadge,
buildNavChipRow,
buildActivityRing,
buildRatingStars,
@ -275,6 +276,8 @@ export {
type ImagePlaceholderParams,
type CommentParams,
type ModalShellParams,
type StatusBadgeParams,
type StatusBadgeTone,
type NavChipRowItem,
type NavChipRowParams,
type ActivityRingParams,

View file

@ -0,0 +1,120 @@
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
import { writeFile, unlink, readFile, mkdir } from 'node:fs/promises';
import { tmpdir } from 'node:os';
import { join } from 'node:path';
import { DESIGN_TOOL_DEFINITIONS, DESIGN_TOOL_NAMES } from '../routes/design-routes';
import { handleAddStatusBadgeV0 } from '../tools/add-status-badge-v0';
import { invalidateCache } from '../document-manager';
const TMP = join(tmpdir(), 'openpencil-add-status-badge-v0');
const EMPTY = JSON.stringify({ version: '1.0.0', children: [] });
async function fresh(name: string): Promise<string> {
const fp = join(TMP, name);
await writeFile(fp, EMPTY, 'utf-8');
return fp;
}
async function readDoc(fp: string): Promise<Record<string, unknown>> {
return JSON.parse(await readFile(fp, 'utf-8'));
}
function getRoot(doc: Record<string, unknown>): Record<string, unknown> {
const pages = doc['pages'] as Array<{ children?: Record<string, unknown>[] }> | undefined;
const top = doc['children'] as Record<string, unknown>[] | undefined;
const root = (top ?? pages?.[0]?.children)?.[0];
if (!root) throw new Error('no root');
return root;
}
beforeEach(async () => {
await mkdir(TMP, { recursive: true });
});
afterEach(async () => {
for (const f of ['a.op']) {
try {
const fp = join(TMP, f);
invalidateCache(fp);
await unlink(fp);
} catch {}
}
});
describe('add_status_badge_v0', () => {
it('registered; required label, tone enum', () => {
expect(DESIGN_TOOL_NAMES.has('add_status_badge_v0')).toBe(true);
const def = DESIGN_TOOL_DEFINITIONS.find((t) => t.name === 'add_status_badge_v0');
expect(def?.inputSchema.required).toEqual(['label']);
const toneProp = (def?.inputSchema.properties as unknown as Record<string, { enum?: string[] }>)
?.tone;
expect(toneProp?.enum).toEqual(['success', 'warning', 'error', 'info', 'neutral']);
});
it('default tone (neutral): slate-400 dot + label', async () => {
const fp = await fresh('a.op');
await handleAddStatusBadgeV0({ filePath: fp, label: 'Idle' });
const badge = getRoot(await readDoc(fp));
expect(badge.role).toBe('status-badge');
expect(badge.layout).toBe('horizontal');
const kids = badge.children as Record<string, unknown>[];
expect(kids.length).toBe(2);
const dot = kids[0];
expect(dot.role).toBe('status-dot');
expect(dot.cornerRadius).toBe(4);
expect(dot.width).toBe(8);
const dotFill = dot.fill as Array<{ color: string }>;
expect(dotFill[0].color).toBe('#94A3B8'); // slate-400
expect(kids[1].content).toBe('Idle');
});
it('tone=success → emerald dot', async () => {
const fp = await fresh('a.op');
await handleAddStatusBadgeV0({ filePath: fp, label: 'Online', tone: 'success' });
const dot = (getRoot(await readDoc(fp)).children as Record<string, unknown>[])[0];
const dotFill = dot.fill as Array<{ color: string }>;
expect(dotFill[0].color).toBe('#10B981');
});
it('tone=warning → amber', async () => {
const fp = await fresh('a.op');
await handleAddStatusBadgeV0({ filePath: fp, label: 'Degraded', tone: 'warning' });
const dot = (getRoot(await readDoc(fp)).children as Record<string, unknown>[])[0];
const dotFill = dot.fill as Array<{ color: string }>;
expect(dotFill[0].color).toBe('#F59E0B');
});
it('tone=error → red', async () => {
const fp = await fresh('a.op');
await handleAddStatusBadgeV0({ filePath: fp, label: 'Down', tone: 'error' });
const dot = (getRoot(await readDoc(fp)).children as Record<string, unknown>[])[0];
const dotFill = dot.fill as Array<{ color: string }>;
expect(dotFill[0].color).toBe('#EF4444');
});
it('tone=info → blue', async () => {
const fp = await fresh('a.op');
await handleAddStatusBadgeV0({ filePath: fp, label: 'New', tone: 'info' });
const dot = (getRoot(await readDoc(fp)).children as Record<string, unknown>[])[0];
const dotFill = dot.fill as Array<{ color: string }>;
expect(dotFill[0].color).toBe('#3B82F6');
});
it('dot is a FRAME with cornerRadius=4 (not an ellipse — anti-pattern avoidance)', async () => {
// 8×8 pill via frame+cornerRadius, NOT a tiny ellipse. Ellipse
// at that size is the classic "status dot = stacked ellipses"
// bait for rewriteLlmAntiPatterns. This test locks the frame
// approach in place.
const fp = await fresh('a.op');
await handleAddStatusBadgeV0({ filePath: fp, label: 'X' });
const dot = (getRoot(await readDoc(fp)).children as Record<string, unknown>[])[0];
expect(dot.type).toBe('frame');
expect(dot.type).not.toBe('ellipse');
});
it('throws on bogus parent_id AND leaves file untouched', async () => {
const fp = await fresh('a.op');
const before = await readFile(fp, 'utf-8');
await expect(
handleAddStatusBadgeV0({ filePath: fp, label: 'X', parent_id: 'nope' }),
).rejects.toThrow(/parent_id.*not found/);
expect(await readFile(fp, 'utf-8')).toBe(before);
});
});

View file

@ -638,6 +638,31 @@ export const ELEMENT_TOOL_DEFINITIONS_EXT = [
required: [],
},
},
{
name: 'add_status_badge_v0',
description:
'Semantic status indicator: small colored dot + short label (e.g. "● Online", "● Busy", ' +
'"● Error"). ALWAYS has a dot — that is what makes it visually a status, distinguishing ' +
'from the more general add_badge_v0 (pill label without dot). tone picks dot color: ' +
'success (green) / warning (amber) / error (red) / info (blue) / neutral (slate, default). ' +
'Use for "status", "presence", "health indicator", "状态". schemaVersion 1.0',
inputSchema: {
type: 'object' as const,
properties: {
schemaVersion: schemaVersionProp,
filePath: filePathProp,
label: { type: 'string', description: 'Label text (e.g. "Online")' },
tone: {
type: 'string',
enum: ['success', 'warning', 'error', 'info', 'neutral'],
description: 'Dot color tone. Default "neutral" (slate gray).',
},
parent_id: parentIdProp,
pageId: pageIdProp,
},
required: ['label'],
},
},
{
name: 'add_image_placeholder_v0',
description:

View file

@ -42,6 +42,7 @@ import { handleAddChartPieV0 } from '../tools/add-chart-pie-v0';
import { handleAddImagePlaceholderV0 } from '../tools/add-image-placeholder-v0';
import { handleAddCommentV0 } from '../tools/add-comment-v0';
import { handleAddModalShellV0 } from '../tools/add-modal-shell-v0';
import { handleAddStatusBadgeV0 } from '../tools/add-status-badge-v0';
import { handleAddSwitchV0 } from '../tools/add-switch-v0';
import { handleAddCheckboxV0 } from '../tools/add-checkbox-v0';
import { handleAddRadioV0 } from '../tools/add-radio-v0';
@ -134,6 +135,8 @@ export async function handleElementToolCall(name: string, a: any): Promise<strin
return JSON.stringify(await handleAddCommentV0(a), null, 2);
case 'add_modal_shell_v0':
return JSON.stringify(await handleAddModalShellV0(a), null, 2);
case 'add_status_badge_v0':
return JSON.stringify(await handleAddStatusBadgeV0(a), null, 2);
case 'add_switch_v0':
return JSON.stringify(await handleAddSwitchV0(a), null, 2);
case 'add_checkbox_v0':

View file

@ -0,0 +1,22 @@
import { assignIdsRecursively, buildStatusBadge, type StatusBadgeParams } from '@zseven-w/pen-core';
import type { handleBatchDesign } from './batch-design';
import { ensureParentExists, insertElementTree } from './element-tool-helpers';
export interface AddStatusBadgeV0Params extends StatusBadgeParams {
parent_id?: string;
filePath?: string;
pageId?: string;
}
/**
* Status indicator: colored dot + label ("● Online"). Tree build
* delegated to `buildStatusBadge`.
*/
export async function handleAddStatusBadgeV0(
params: AddStatusBadgeV0Params,
): Promise<Awaited<ReturnType<typeof handleBatchDesign>>> {
await ensureParentExists(params);
const badge = buildStatusBadge(params);
assignIdsRecursively(badge);
return insertElementTree({ binding: 'sb', tree: badge, ...params });
}