| name | implement-widget-frontend |
| description | Add TypeScript types in ha.d.ts and editor schema in eink-dashboard-editor.ts for a new or updated widget type; also handles deprecating an old type (remove from picker, keep schema, add @deprecated JSDoc). Canvas preview was replaced by server-rendered SVGs in step 4.1; no frontend renderer is needed. |
| when_to_use | When adding a new widget type, deprecating an existing one, or updating a widget's TypeScript types or editor form. Run AFTER the Python renderer (implement-widget) is complete. Must also update getSummary(), LABELS, and the ALL_TYPES arrays in the frontend test file. |
| argument-hint | [widget-type] |
| arguments | widget-type |
| allowed-tools | Read, Edit, Write, Bash(pnpm *) |
Implement Widget Frontend: $widget-type
Add TypeScript types and editor schema for $widget-type. The card
fetches SVGs from the Python backend via WebSocket — no canvas renderer
is needed here.
Before you start
- Read existing type definitions in
frontend/src/types/ha.d.ts.
- Read existing schemas in
frontend/src/eink-dashboard-editor.ts.
- Use the widget config specifications below as the source of truth
for which fields to expose.
Existing TS widget type definitions
!grep -n "interface\|^export type" custom_components/eink_dashboard/frontend/src/types/ha.d.ts
Widget config specifications
These are the canonical field lists for each widget type. Use them
to determine which fields to add to the TypeScript interface and
which controls to add to the editor schema.
Project-specific fields (all widgets)
Every widget config extends HA's card config with these
project-specific fields:
card_style — "border" | "left_bar" | "none" (default:
"none"). Controls decorative frame around the widget.
icon_style — "filled" | "outlined" | "none" (default:
"filled"). Controls icon circle rendering.
Tile
Single-entity widget modelled after HA's Tile card.
Config fields (add to interface, expose in schema):
entity (required) — any HA entity
name — override display name
icon — override icon (default: entity's domain icon)
hide_state — boolean, suppress state text
state_content — which state attribute(s) to display
show_entity_picture — show entity picture instead of icon
card_style — project-specific (see above)
icon_style — project-specific (see above)
Fields intentionally omitted: vertical, color, features,
features_position, all tap_action / hold_action /
double_tap_action variants.
Heading
Section heading with optional icon and entity badges, modelled
after HA's Heading card.
Config fields:
heading — heading text
heading_style — "title" | "subtitle"
icon — MDI icon name (e.g. "mdi:fridge")
badges — list of entity badge configs
icon_style — project-specific (default: "none")
card_style — project-specific (see above)
Fields intentionally omitted: tap_action.
Sensor
Single-entity sensor with optional history graph, modelled after
HA's Sensor card.
Config fields:
entity (required) — sensor, counter, input_number, number
name — override display name
icon — override icon
graph — "line" | none (default: none)
hours_to_show — history window (default: 24)
detail — graph detail level (1 or 2)
unit — unit override
limits — { min?: number, max?: number } for graph Y-axis
card_style — project-specific (see above)
icon_style — project-specific (see above)
Fields intentionally omitted: theme.
Entity
Single-entity display with large value, modelled after HA's
Entity card.
Config fields:
entity (required) — any HA entity
name — override display name
icon — override icon
attribute — show a specific attribute instead of state
unit — unit override
card_style — project-specific (see above)
icon_style — project-specific (see above)
Fields intentionally omitted: state_color, theme, all
tap_action / hold_action / double_tap_action variants.
Entities
Multi-entity list card, modelled after HA's Entities card.
Config fields:
title — optional card header text
icon — optional header icon
entities (required) — list of row configs:
- Entity row: string entity ID, or
{ entity, name?, icon? } object
- Divider row:
{ type: "divider" }
- Section row:
{ type: "section", label? }
card_style — project-specific (see above)
icon_style — project-specific (see above)
Fields intentionally omitted: show_header_toggle, header,
footer, state_color, theme.
Clock
Digital time display, modelled after HA's Clock card.
Config fields:
title — optional label above the time
clock_size — "small" | "medium" | "large"
time_format — 12h or 24h override
time_zone — timezone override
show_seconds — boolean (default: false)
card_style — project-specific (see above)
Fields intentionally omitted: clock_style (digital only),
no_background, seconds_motion, border, ticks,
face_style.
Weather, Separator, Device Battery, Waste Schedule
Keep existing frontend definitions unchanged.
Implementation steps
1. Update TypeScript type definitions
In frontend/src/types/ha.d.ts:
Add supporting interfaces (if the widget has structured sub-items):
export interface {WidgetName}Badge {
entity: string;
name?: string;
icon?: string;
show_state?: boolean;
show_icon?: boolean;
}
Add the widget interface:
export interface {WidgetName}Widget extends WidgetBase {
type: "$widget-type";
$main_field?: string;
badges?: (string | {WidgetName}Badge)[];
card_style?: CardStyle;
icon_style?: IconStyle;
}
Fields prefixed with $ are placeholders — substitute the real field
name from the widget spec (e.g. heading for Heading, entity for
Tile).
Include only the fields the Python renderer actually reads. Omit fields
that belong to WidgetBase (x, y, w, font_size, color) since
those are inherited. Document every member with a JSDoc comment.
Add to the Widget union type:
export type Widget =
| TextWidget
| SeparatorWidget
| ...
| {WidgetName}Widget;
2. Update editor schema
In frontend/src/eink-dashboard-editor.ts:
Add to WIDGET_TYPES (only widgets shown in the picker — deprecated
widgets are removed from here but their SCHEMAS entry is kept so
existing configs remain editable):
"$widget-type": {
label: "Widget Name",
description: "One-line description shown in the widget picker.",
icon: "mdi:some-icon",
defaults: {
type: "$widget-type",
x: 24, y: 0, w: 400, h: 56,
$main_field: "",
card_style: DEFAULT_CARD_STYLE,
icon_style: DEFAULT_ICON_STYLE,
},
},
Use x: 24 (= PADDING) for content widgets (entity, tile — anything
with internal text and icons) so newly created widgets land at the
standard canvas inset. Use x: 0 only for structural widgets that
span full width (separator, heading).
Include defaults for content fields so the form is pre-populated on
first open (e.g. heading: "", heading_style: "title"). Override
icon_style when the widget spec defines a different default (e.g.
Heading uses "none" instead of DEFAULT_ICON_STYLE).
Add to ICON_FALLBACK in frontend/src/eink-widget-picker.ts —
every icon value used in WIDGET_TYPES must have a text/emoji entry
in ICON_FALLBACK. Without it the picker renders the raw MDI string
(e.g. "format-list-bulleted") instead of a glyph. Check whether the
icon is already present; add it only if missing:
const ICON_FALLBACK: Record<string, string> = {
"mdi:some-icon": "X",
};
Add to SCHEMAS — use identitySection() plus grouped sections
(identitySection first, Content expanded by default, Layout and
Appearance collapsed). The flatten: true flag on each expandable
section merges the contained fields directly into the widget data
object rather than nesting them under a key:
"$widget-type": (d: DisplayConfig) => [
identitySection(),
{
name: "content",
type: "expandable",
flatten: true,
expanded: true,
title: "Content",
icon: "mdi:text",
schema: [
{ name: "$main_field", selector: { text: {} } },
{
name: "badges",
selector: { entity: { multiple: true } },
},
],
},
{
name: "layout",
type: "expandable",
flatten: true,
title: "Layout",
icon: "mdi:move-resize",
schema: [{ type: "grid", name: "", schema: posXYWH(d) }],
},
{
name: "appearance",
type: "expandable",
flatten: true,
title: "Appearance",
icon: "mdi:palette",
schema: [cardStyleSelector(), iconStyleSelector()],
},
],
Add to getSummary() — return a short human-readable summary for
the widget list row. Two common patterns:
if (t === "$widget-type") {
const s = String(widget.$main_field || "");
return s.length > 30 ? s.slice(0, 30) + "…" : (s || "(empty)");
}
if (t === "$widget-type") {
return widget.entity || "(no entity)";
}
Add to LABELS — every field name used in the schema that does not
already have a label entry needs one:
$main_field: "Editor label for primary field",
Entity domain filtering: Use the correct domain filter for each
widget type:
tile, heading → no domain filter (accepts any entity)
waste_schedule → { domain: "sensor" } (single entity from
waste_collection_schedule)
device_battery → { domain: "sensor" } or no filter
Visibility conditions: Every widget form automatically gets a
collapsible Visibility section appended by _buildVisibilityEditor()
in eink-dashboard-editor.ts. Do NOT add a visibility field to the
widget's SCHEMAS entry — it would duplicate the auto-appended
section. The visibility field is typed as
(Condition | LegacyCondition)[] (both types exported from
ha.d.ts) on WidgetBase, so all widget interfaces inherit it.
3. Update frontend tests
In frontend/test/eink-dashboard-editor.test.ts there are two
ALL_TYPES constant arrays that must be kept in sync manually:
WIDGET_TYPES > ALL_TYPES — lists only picker-visible types
(i.e. what is in WIDGET_TYPES). Update the count in the test
description too (e.g. "has all N widget types").
SCHEMAS > ALL_TYPES — lists every type with a schema builder
(picker-visible + deprecated types kept for editing). Update the
count in the test description too.
The integration test add-widget integration > appends a widget when a type is selected from the picker clicks a data-type="…" card. If
you removed or renamed a widget type in the picker, update the
data-type attribute value in this test to an existing picker type.
4. Verify
pnpm --dir custom_components/eink_dashboard/frontend typecheck && \
pnpm --dir custom_components/eink_dashboard/frontend test
Deprecating a widget type
When a widget type is superseded (e.g. text → heading):
- Remove from
WIDGET_TYPES so it no longer appears in the picker.
- Keep in
SCHEMAS so existing configs remain editable.
- Add
@deprecated JSDoc to the TypeScript interface in ha.d.ts.
- Update
AGENTS.md widget type list.
- Keep in the
Widget union type — removing it would break type
narrowing for existing config objects.
Default values
The Python constant DEFAULT_ROW_H = 56 (in const.py) is the
standard single-row height. Use it as the basis for h defaults in
WIDGET_TYPES and SCHEMAS:
- Single-row widget:
h: 56
- Two-row widget:
h: 112 (= 2 × DEFAULT_ROW_H)
- Chip-style widget:
h: 28
These values keep the frontend defaults in sync with the Python
renderer's own sizing baseline.
Key references
- Python SVG renderer:
custom_components/eink_dashboard/svg_render.py
- Sizing baseline:
DEFAULT_ROW_H = 56 in const.py
- Types:
frontend/src/types/ha.d.ts
- Editor:
frontend/src/eink-dashboard-editor.ts
- Tests:
frontend/test/eink-dashboard-editor.test.ts