Wobbling Ty Constraint Order
astral-sh/ruff
A skill your agent uses when a user asks to wobble ty constraint ordering, check constraint-set or TDD ordering determinism, test reversed constraint/typevar IDs, or investigate nondeterministic ty…
Implement a widget's SVG template and Python context builder.
$ npx skills add cryptomilk/hass-eink-dashboard --skill implement-widget -a claude-codeProject install by default; add -g for ~/.claude/skills/.
$ gh skill install cryptomilk/hass-eink-dashboard implement-widget --agent claude-codeProject scope by default; add --scope user for a personal install. Needs GitHub CLI 2.90.0 or later (public preview).
$ git clone --depth 1 https://github.com/cryptomilk/hass-eink-dashboard.git skills-src && mkdir -p .claude/skills && cp -r skills-src/.agents/skills/implement-widget .claude/skills/implement-widget && rm -rf skills-srcUse ~/.claude/skills/ instead of .claude/skills for a personal install. The folder must contain SKILL.md.
Claude Code skills documentation · loads skills from .claude/skills/
Install the "implement-widget" agent skill from https://github.com/cryptomilk/hass-eink-dashboard/tree/main/.agents/skills/implement-widget into .claude/skills/implement-widget/ in this project. Copy the whole folder (SKILL.md and every file beside it), keep the folder name "implement-widget", then confirm the skill loads.Claude Code copies the folder itself, the same result as the manual copy. Check what it changed before you commit it.
$skill-installer install https://github.com/cryptomilk/hass-eink-dashboard/tree/main/.agents/skills/implement-widgetType this inside Codex. $skill-installer <name> installs a curated skill from openai/skills. The installer writes to $CODEX_HOME/skills (default ~/.codex/skills). Restart Codex if the skill does not show up.
$ npx skills add cryptomilk/hass-eink-dashboard --skill implement-widget -a codexProject install goes to .agents/skills/; add -g for ~/.codex/skills/.
$ gh skill install cryptomilk/hass-eink-dashboard implement-widget --agent codexProject scope by default (.agents/skills/); add --scope user for a personal install.
$ git clone --depth 1 https://github.com/cryptomilk/hass-eink-dashboard.git skills-src && mkdir -p .agents/skills && cp -r skills-src/.agents/skills/implement-widget .agents/skills/implement-widget && rm -rf skills-srcUse ~/.agents/skills/ instead of .agents/skills for a personal install.
Codex skills documentation · loads skills from .agents/skills/
Install the "implement-widget" agent skill from https://github.com/cryptomilk/hass-eink-dashboard/tree/main/.agents/skills/implement-widget into .agents/skills/implement-widget/ in this project. Copy the whole folder (SKILL.md and every file beside it), keep the folder name "implement-widget", then confirm the skill loads.Codex copies the folder itself, the same result as the manual copy. Check what it changed before you commit it.
$ npx skills add cryptomilk/hass-eink-dashboard --skill implement-widget -a cursorProject install goes to .agents/skills/; add -g for ~/.cursor/skills/.
$ gh skill install cryptomilk/hass-eink-dashboard implement-widget --agent cursorProject scope by default (.agents/skills/); add --scope user for a personal install.
$ git clone --depth 1 https://github.com/cryptomilk/hass-eink-dashboard.git skills-src && mkdir -p .cursor/skills && cp -r skills-src/.agents/skills/implement-widget .cursor/skills/implement-widget && rm -rf skills-srcUse ~/.cursor/skills/ instead of .cursor/skills for a personal install.
Cursor skills documentation · loads skills from .cursor/skills/, .agents/skills/, .claude/skills/, .codex/skills/
Install the "implement-widget" agent skill from https://github.com/cryptomilk/hass-eink-dashboard/tree/main/.agents/skills/implement-widget into .cursor/skills/implement-widget/ in this project. Copy the whole folder (SKILL.md and every file beside it), keep the folder name "implement-widget", then confirm the skill loads.Cursor copies the folder itself, the same result as the manual copy. Check what it changed before you commit it.
$ gemini skills install https://github.com/cryptomilk/hass-eink-dashboard.git --path .agents/skills/implement-widget--scope user (default) or --scope workspace; --path is the subfolder of the repo that holds the skill; --consent skips the security confirmation prompt.
$ npx skills add cryptomilk/hass-eink-dashboard --skill implement-widget -a gemini-cliProject install goes to .agents/skills/; add -g for ~/.gemini/skills/.
$ gh skill install cryptomilk/hass-eink-dashboard implement-widget --agent gemini-cliProject scope by default (.agents/skills/); add --scope user for a personal install.
$ git clone --depth 1 https://github.com/cryptomilk/hass-eink-dashboard.git skills-src && mkdir -p .gemini/skills && cp -r skills-src/.agents/skills/implement-widget .gemini/skills/implement-widget && rm -rf skills-srcUse ~/.gemini/skills/ instead of .gemini/skills for a personal install, then run /skills reload.
Gemini CLI skills documentation · loads skills from .gemini/skills/, .agents/skills/
Install the "implement-widget" agent skill from https://github.com/cryptomilk/hass-eink-dashboard/tree/main/.agents/skills/implement-widget into .gemini/skills/implement-widget/ in this project. Copy the whole folder (SKILL.md and every file beside it), keep the folder name "implement-widget", then confirm the skill loads.Gemini CLI copies the folder itself, the same result as the manual copy. Check what it changed before you commit it.
$ gh skill install cryptomilk/hass-eink-dashboard implement-widgetInstalls for Copilot at project scope by default; add --scope user for a personal install. Preview a skill first with gh skill preview. Needs GitHub CLI 2.90.0 or later (public preview).
$ npx skills add cryptomilk/hass-eink-dashboard --skill implement-widget -a github-copilotProject install goes to .agents/skills/; add -g for ~/.copilot/skills/.
$ git clone --depth 1 https://github.com/cryptomilk/hass-eink-dashboard.git skills-src && mkdir -p .github/skills && cp -r skills-src/.agents/skills/implement-widget .github/skills/implement-widget && rm -rf skills-srcUse ~/.copilot/skills/ instead of .github/skills for a personal install. Commit .github/skills so cloud agent and code review can use it.
GitHub Copilot skills documentation · loads skills from .github/skills/, .claude/skills/, .agents/skills/
Install the "implement-widget" agent skill from https://github.com/cryptomilk/hass-eink-dashboard/tree/main/.agents/skills/implement-widget into .github/skills/implement-widget/ in this project. Copy the whole folder (SKILL.md and every file beside it), keep the folder name "implement-widget", then confirm the skill loads.GitHub Copilot copies the folder itself, the same result as the manual copy. Check what it changed before you commit it.
$ npx skills add cryptomilk/hass-eink-dashboard --skill implement-widget -a opencodeOpenCode documents no install command of its own. Project install goes to .agents/skills/; add -g for ~/.config/opencode/skills/.
$ gh skill install cryptomilk/hass-eink-dashboard implement-widget --agent opencodeProject scope by default (.agents/skills/); add --scope user for a personal install.
$ git clone --depth 1 https://github.com/cryptomilk/hass-eink-dashboard.git skills-src && mkdir -p .opencode/skills && cp -r skills-src/.agents/skills/implement-widget .opencode/skills/implement-widget && rm -rf skills-srcUse ~/.config/opencode/skills/ instead of .opencode/skills for a personal install.
OpenCode skills documentation · loads skills from .opencode/skills/, .claude/skills/, .agents/skills/
Install the "implement-widget" agent skill from https://github.com/cryptomilk/hass-eink-dashboard/tree/main/.agents/skills/implement-widget into .opencode/skills/implement-widget/ in this project. Copy the whole folder (SKILL.md and every file beside it), keep the folder name "implement-widget", then confirm the skill loads.OpenCode copies the folder itself, the same result as the manual copy. Check what it changed before you commit it.
implement-widgetImplement a widget's SVG template and Python context builder.
Implement Widget is an agent skill from cryptomilk/hass-eink-dashboard. Implement a widget's SVG template and Python context builder. Creates templates/{type}.svg.j2 and widgets/{type}.py with build{type}context(). Follows TDD green phase — makes existing tests pass.
Its SKILL.md is about 4.8k tokens, which your agent loads only when the skill is triggered. It is a single SKILL.md file with no bundled scripts.
It sits in Testing & QA, covering Test-driven development. It works with Python and Home Assistant. The repository describes itself as: This is beta quality, should be mostly stable. The licence is Apache-2.0.
6 steps, taken from the step headings in SKILL.md.
Read from SKILL.md and the folder at commit 6142841. It shows what the files ask for, not the result of running them.
Pre-approves these tools, so the agent can use them without asking each time:
ReadEditWriteBash(uv *)From allowed-tools in the SKILL.md frontmatter.
Shell commands in SKILL.md call:
uvFrom the folder's file list and the shell code blocks in SKILL.md.
Hosts in commands or code, which the agent is likely to contact:
w3.orgFrom URLs in SKILL.md, links to its own repository left out.
Names no API keys, tokens, secrets or passwords.
From names ending in _API_KEY, _TOKEN, _SECRET, _KEY or _PASSWORD in SKILL.md.
Implement Widget loads about 4.8k tokens when it runs. Until then it costs about 54 tokens; SKILL.md has 1,275 words of instructions outside code blocks.
Estimates: characters ÷ 4, the usual rule of thumb; real counts depend on the model's tokenizer. Scripts and assets cost tokens only if the agent reads them.
The automated check found no risky patterns in SKILL.md.
Automated static check — not a guarantee. Review scripts before installing. It scans the text of SKILL.md for risky patterns (piping downloads into a shell, reading credential files, hidden Unicode, destructive commands); files beside SKILL.md are not scanned.
The full file from cryptomilk/hass-eink-dashboard at commit 6142841, republished under its Apache-2.0 licence (© cryptomilk). 1,275 words, ~4,834 tokens.
.claude/skills/implement-widget/SKILL.md (or your agent's skills folder).Implement the SVG template and Python context builder for $widget-type. The output of this work is two artifacts:
custom_components/eink_dashboard/templates/$widget-type.svg.j2_build_$widget-type_context() in widgets/$widget-type.pyTestRender{WidgetName} in
tests/test_render_{widget_type}.py) — these define the expected
behavior.const.py for WidgetType, COLOR_* constants, PADDING,
DEFAULT_CARD_STYLE, Widget, DisplayConfig, color_to_hex.svg_render.py for the Jinja2 environment, _svg_to_png(),
icon filter functions (_mdi_svg_filter, _weather_svg_filter),
and _SVG_RENDERERS.widgets/ (e.g. widgets/tile.py)
for the context builder pattern, and widgets/_helpers.py for
shared layout helpers.templates/_macros.svg.j2 for the three shared macros:
card_container, card_row, chip.!grep -n "^def \|^_SVG_RENDERERS\|^_jinja_env\|^_TEMPLATE_DIR\|^_FONTS_DIR\|^_mdi\|^_weather" custom_components/eink_dashboard/svg_render.py
!grep -n "^def \|^_ACTIVE" custom_components/eink_dashboard/widgets/_helpers.py
!grep -rn "^def _build_" custom_components/eink_dashboard/widgets/*.py
!grep -n "macro card_container\|macro card_row\|macro chip\|macro icon_circle" custom_components/eink_dashboard/templates/_macros.svg.j2
!grep -n "^class WidgetMetrics" -A 10 custom_components/eink_dashboard/render.py
!grep -n "^def _compute_metrics" -A 12 custom_components/eink_dashboard/render.py
!grep -n "^def _device_class_icon" -A 6 custom_components/eink_dashboard/render.py
!grep -n "_SVG_RENDERERS" -A 12 custom_components/eink_dashboard/svg_render.py
Context builders live in widgets/{type}.py and use relative
imports. _compute_metrics, _device_class_icon, and _load_font
live in render.py. color_to_hex, Widget, DisplayConfig,
and constants live in const.py. Layout helpers live in
widgets/_helpers.py.
from ..const import (
DEFAULT_CARD_STYLE,
DEFAULT_ROW_H,
PADDING,
DisplayConfig,
Widget,
color_to_hex,
)
from ..render import (
_compute_metrics,
_device_class_icon,
_load_font, # chip width measurement only
)
from ..svg_render import _mdi_svg_filter
from ._helpers import (
_auto_row_height,
_card_insets,
_color_context,
_fmt,
_metrics_context,
_widget_dim,
)Use _fmt(state_val, config) whenever a context builder formats a
numeric entity state for display. This applies locale-aware decimal
and thousands separators matching the owner's HA preference. Pass
the raw state string — non-numeric strings ("on", "unavailable")
are returned unchanged.
_color_context() in widgets/_helpers.py returns
{"hex_black": ..., "hex_white": ..., "hex_gray": ...} by calling
color_to_hex() on the const.py integer constants. Spread it into
the context dict with **_color_context() so templates can use
{{ hex_gray }} instead of hardcoded hex literals.
card_container uses Jinja2 {% call %} syntax. The macro
passes (x_off, right_inset) back to the caller body:
"border" → (m.padding, m.padding);
"left_bar" → (bar_w + m.padding, 0);
"none" → (0, 0).
Content starts at x_off; content width = w - x_off - right_inset.
Always pass display_levels from config.card_row expects all sizes from WidgetMetrics fields plus
icon_svg (pre-built SVG string, empty string for letter fallback)
and letter (single uppercase char, empty string when icon_svg is
set). primary is the entity label; secondary is the state +
unit. Pass lpad/rpad derived from _card_insets() to prevent
double-padding when a card style already provides insets; use the
same values for x1/x2 of any divider <line> between rows.
Pass icon_no_circle=True when icon_style == "none" — the macro
hides the circle but still renders the icon glyph.chip requires pre-computed w because text width depends on
font metrics unavailable in Jinja2. The context builder must compute
chip widths using _load_font(size).getlength(text) from PIL — the
font object is used only for measurement, not for drawing._mdi_svg_filter(name, size) in the context builder and pass the
result as the icon_svg string to the template. Do NOT call filters
from inside templates — pass the pre-built string via context._device_class_icon(attrs, state_val, domain) returns an MDI
icon name without the "mdi:" prefix, or None. Extract domain:
entity_id.split(".")[0].In const.py, add the new value to WidgetType. Keep alphabetical
order.
Create custom_components/eink_dashboard/templates/$widget-type.svg.j2.
The template receives a pre-computed context dict from the Python context builder. All layout math (positions, sizes, icon SVG strings, chip widths) is computed in Python. Templates only emit SVG markup.
Widget SVGs are composited onto a white canvas using the alpha
channel as a mask (render_dashboard() in render.py). Transparent
pixels preserve whatever is already on the canvas (white by default),
so most templates do not need a white background rect.
Add a full-viewport white background rect only when the template's
rendered geometry is smaller than its SVG viewport — i.e. when content
does not fill w × h. The separator is the canonical example: its
bar (bar_w × bar_h) is much smaller than the widget viewport, so
surrounding transparent area would let earlier widget content bleed
through without the rect.
Card-style and chip-style templates omit the rect because the card
chrome (via card_container) or chip flow covers the meaningful area,
and any remaining transparent edge pixels are harmless (canvas is
white).
Card-style template pattern (tile, waste_schedule):
{%- from "_macros.svg.j2" import card_container, card_row -%}
<svg xmlns="http://www.w3.org/2000/svg"
width="{{ w }}" height="{{ h }}">
{%- call(x_off, r_inset) card_container(
x=0, y=0, w=w, h=h,
card_style=card_style,
radius=m.radius, border=m.border,
padding=m.padding, left_bar=m.left_bar,
display_levels=display_levels) -%}
{%- for row in rows -%}
{{ card_row(
x=x_off, y=row.y,
w=w - x_off - r_inset, row_h=row_h,
padding=m.padding, icon_dia=m.icon_dia,
inner_gap=m.inner_gap, border=m.border,
font_primary=m.font_primary,
font_secondary=m.font_secondary,
primary=row.primary,
secondary=row.secondary,
value=row.value,
icon_svg=row.icon_svg,
letter=row.letter,
lpad=lpad, rpad=rpad) }}
{%- if not loop.last -%}
<line
x1="{{ x_off + lpad }}"
y1="{{ row.y + row_h }}"
x2="{{ w - r_inset - rpad }}"
y2="{{ row.y + row_h }}"
stroke="{{ hex_gray }}" stroke-width="{{ m.divider }}"/>
{%- endif -%}
{%- endfor -%}
{%- endcall -%}
</svg>Chip-style template pattern (device_battery):
{%- from "_macros.svg.j2" import chip -%}
<svg xmlns="http://www.w3.org/2000/svg"
width="{{ w }}" height="{{ h }}">
{%- for c in chips -%}
{{ chip(
x=c.x, y=c.y, w=c.w, h=chip_h,
text=c.text, border=m.border,
icon_svg=c.icon_svg,
inverted=c.inverted) }}
{%- endfor -%}
</svg>Simple template pattern (separator):
When rendered geometry (e.g. a bar or line) is smaller than the SVG viewport, include a full-size white rect before it so the surrounding area is not transparent:
<svg xmlns="http://www.w3.org/2000/svg"
width="{{ w }}" height="{{ h }}">
<rect width="{{ w }}" height="{{ h }}" fill="{{ hex_white }}"/>
<!-- widget-specific SVG elements here -->
</svg>Template coordinates are relative to origin (0, 0) — the outer
composition handles absolute x/y placement on the dashboard.
Create widgets/$widget-type.py with
_build_$widget-type_context(widget, config) -> dict. The context
builder:
widget and config_compute_metrics(row_h) to get WidgetMetrics_device_class_icon() to resolve icon names_mdi_svg_filter(name, size) to build icon SVG stringsThe template receives only this dict; no layout math happens in Jinja2.
Card-style context builder pattern:
def _build_{widget_type}_context(
widget: Widget,
config: DisplayConfig,
) -> dict[str, object]:
"""Build template context for $widget-type widget.
Args:
widget: Widget config dict with x, w, h, entities,
card_style, title.
config: DisplayConfig with states and display_levels.
Returns:
Template context dict.
"""
x = widget.get("x", PADDING)
w = _widget_dim(widget, "w", config["width"] - x)
title: str = widget.get("title", "")
card_style = widget.get(
"card_style", DEFAULT_CARD_STYLE
)
entities = widget.get("entities", [])
states = config.get("states", {})
display_levels = config.get("display_levels", 16)
n = len(entities)
if n == 0:
return {
"w": w,
"h": _widget_dim(widget, "h", DEFAULT_ROW_H),
"rows": [],
}
# Row-based: auto-size height to fit n rows at
# DEFAULT_ROW_H px each. An explicit "h" overrides this.
h = _widget_dim(widget, "h", _auto_row_height(title, n))
row_h = h // n
m = _compute_metrics(row_h)
x_off, r_inset, bar_width = _card_insets(
m, card_style, display_levels
)
lpad = m.padding if x_off == 0 else 0
rpad = m.padding if r_inset == 0 else 0
rows: list[dict[str, object]] = []
for i, entity_id in enumerate(entities):
state = states.get(entity_id)
if state is None:
continue
attrs = state.get("attributes", {})
domain = entity_id.split(".")[0]
# Resolve icon: device_class → attrs["icon"] → letter.
icon_name = _device_class_icon(
attrs, state["state"], domain
)
if icon_name is None:
raw = attrs.get("icon", "")
if raw.startswith("mdi:"):
icon_name = raw[4:]
# Call filter in Python; suppress missing-icon errors.
icon_svg: markupsafe.Markup | str = ""
if icon_name:
with contextlib.suppress(FileNotFoundError):
icon_svg = _mdi_svg_filter(
icon_name, m.icon_inner
)
letter = ""
if not icon_svg:
friendly = attrs.get("friendly_name", entity_id)
letter = friendly[:1].upper() if friendly else ""
unit = attrs.get("unit_of_measurement", "")
state_val = state["state"]
rows.append({
"y": i * row_h,
"primary": attrs.get(
"friendly_name", entity_id
),
"secondary": f"{_fmt(state_val, config)}{unit}",
"value": "",
"icon_svg": icon_svg,
"letter": letter,
})
return {
"w": w,
"h": h,
"card_style": card_style,
"display_levels": display_levels,
"rows": rows,
"row_h": row_h,
"lpad": lpad,
"rpad": rpad,
**_metrics_context(m),
**_color_context(),
}Chip-style context builder — chip width computation:
Chips require pre-computed widths because Jinja2 cannot measure text.
Use a PIL font object (Roboto at round(h * 0.46)) only for
measurement — _load_font is LRU-cached, so this is cheap:
def _build_{widget_type}_context(
widget: Widget,
config: DisplayConfig,
) -> dict[str, object]:
"""Build template context for $widget-type widget."""
x = widget.get("x", PADDING)
w = _widget_dim(widget, "w", config["width"] - x)
h = _widget_dim(widget, "h", DEFAULT_ROW_H)
entities = widget.get("entities", [])
states = config.get("states", {})
m = _compute_metrics(h)
# pad, icon_gap, and font_sz are chip-specific ratios;
# icon_dia and icon_inner come from _compute_metrics() so
# they match card_row at the same height.
pad = round(h * 0.18)
icon_gap = round(h * 0.14)
font_size = max(10, round(h * 0.46))
font = _load_font(font_size) # PIL, for width measurement only.
chip_gap = m.border + 4
chips: list[dict[str, object]] = []
for entity_id in entities:
state = states.get(entity_id)
if state is None:
continue
attrs = state.get("attributes", {})
domain = entity_id.split(".")[0]
icon_name = _device_class_icon(
attrs, state["state"], domain
)
if icon_name is None:
raw = attrs.get("icon", "")
if raw.startswith("mdi:"):
icon_name = raw[4:]
icon_svg: markupsafe.Markup | str = ""
if icon_name:
with contextlib.suppress(FileNotFoundError):
icon_svg = _mdi_svg_filter(
icon_name, m.icon_inner
)
label = attrs.get("friendly_name", entity_id)
text_w = round(font.getlength(label))
icon_w = (m.icon_dia + icon_gap) if icon_svg else 0
chip_w = pad + icon_w + text_w + pad
chips.append({
"text": label,
"icon_svg": icon_svg,
"inverted": state["state"] == "on",
"w": chip_w,
})
# Flow layout: pack chips left-to-right with wrapping.
cx, cy = 0, 0
for c in chips:
if cx > 0 and cx + c["w"] > w:
cx, cy = 0, cy + h + chip_gap
c["x"] = cx
c["y"] = cy
cx += c["w"] + chip_gap
return {
"w": w,
"h": h,
"m": m,
"chip_h": h,
"chips": chips,
}In widgets/__init__.py, add the import and __all__ entry:
from .{widget_type} import _build_{widget_type}_context
__all__ = [
...
"_build_{widget_type}_context",
]In svg_render.py, import from widgets and add to the
_SVG_RENDERERS dict:
from .widgets import _build_{widget_type}_context
_SVG_RENDERERS: dict[str, SvgContextFn] = {
...
WidgetType.{WIDGET_TYPE}: _build_{widget_type}_context,
}uv run --group lint ruff check . && \
uv run --group format ruff format --check . && \
uv run --group typecheck ty check && \
uv run --group test pytestAll tests must pass. Fix any failures — the tests define correct behavior.
font_size parameter. All sizes derive from w and h
via _compute_metrics(). TEXT widget is the only exception.WidgetMetrics. icon_dia and
icon_inner come from _compute_metrics(chip_h) — do not
derive them from inline ratios or _CHIP_*_RATIO constants
(removed in step 0.5). pad, icon_gap, and font_sz are
chip-specific and have no WidgetMetrics field.card_container, card_row, chip from _macros.svg.j2.<rect width="{{ w }}" height="{{ h }}" fill="{{ hex_white }}"/>
when the template's rendered geometry is smaller than its SVG
viewport (e.g. separator bar). Card-style and chip-style templates
omit it — the canvas is pre-filled white and alpha masking handles
compositing.states.get(entity_id) returns
None, skip the entity and continue. Do not crash._mdi_svg_filter(name, size) or _weather_svg_filter(condition, size) in the context
builder and pass the returned string as icon_svg.font-family="Roboto" and
font-weight="500" for Roboto Medium. No _load_font() calls in
templates.y positions
relative to 0, not to the widget's absolute position on the
dashboard. Outer composition handles x/y placement.SVG macros: card_container, card_row, chip in
templates/_macros.svg.j2
SVG pipeline: _jinja_env, _svg_to_png(), _mdi_svg_filter(),
_weather_svg_filter() in svg_render.py
Layout metrics: _compute_metrics(), WidgetMetrics in render.py
Icon name resolution: _device_class_icon() in render.py
Colors: COLOR_BLACK=0, COLOR_WHITE=255, COLOR_GRAY=120,
COLOR_LIGHT_GRAY=180 in const.py
Layout helpers: _widget_dim, _auto_row_height,
_card_insets, _metrics_context, _color_context, _fmt,
_entity_info_context, _title_layout in widgets/_helpers.py
Constants: DEFAULT_ROW_H, Widget, DisplayConfig,
color_to_hex in const.py
Icon style pattern: _ACTIVE_STATES frozenset (in
widgets/_helpers.py) determines active vs inactive state; use
contextlib.suppress(FileNotFoundError) around every
_mdi_svg_filter() call
Custom icon circle: When a widget renders its own icon circle
(not via the card_row macro), use the icon_circle macro from
_macros.svg.j2 — it handles filled/outlined/no-circle variants
and glyph/letter rendering. The context builder must pass
icon_cx, icon_cy, icon_r, icon_glyph_x, icon_glyph_y,
icon_stroke_w, icon_fill, icon_color, icon_svg,
icon_outline, icon_no_circle, letter, and
letter_font_sz to the template. Widen the outline stroke on
2-level displays to avoid dithering — compute this in the context
builder and pass it as icon_stroke_w:
# Widen the outline stroke on 2-level displays.
icon_stroke_w = (
m.border * 3 if display_levels <= 2 else m.border
)The card_row macro handles this widening internally for row
icons. Only apply it explicitly when using icon_circle directly.
© cryptomilk, Apache-2.0. Rendered from Markdown: HTML in the file is shown as text, images as links, and headings moved down two levels. Raw file
Just SKILL.md in .agents/skills/implement-widget of cryptomilk/hass-eink-dashboard.
Open the folder on GitHubat commit 6142841
Implement Widget next to the 5 skills that share the most tags, products or categories with it. Stars are the repository's; “used in” counts other GitHub owners with a copy.
| Skill | Stars | Used in | Tokens | Auto-check | Licence | Repo updated |
|---|---|---|---|---|---|---|
| Implement Widget this skillcryptomilk/hass-eink-dashboard | 116 | — | ~4.8k | Automated safety check: Pass | Apache-2.0 | |
| Wobbling Ty Constraint Orderastral-sh/ruff | 50k | — | ~838 | Automated safety check: Pass | MIT | |
| Test Designjjjkkkjjj/Matft | 147 | — | ~2k | Automated safety check: Pass | BSD-3-Clause | |
| Python Testingaffaan-m/ECC | 275k | 6 repos | ~4.7k | Automated safety check: Pass | MIT | |
| Python Testingaffaan-m/ECC | 275k | 3 repos | ~4.2k | Automated safety check: Pass | MIT | |
| Agent-Core Python TestingopenJiuwen-ai/agent-core | 442 | — | ~2.3k | Automated safety check: Pass | Apache-2.0 |
astral-sh/ruff
A skill your agent uses when a user asks to wobble ty constraint ordering, check constraint-set or TDD ordering determinism, test reversed constraint/typevar IDs, or investigate nondeterministic ty…
jjjkkkjjj/Matft
Procedure for designing and writing Matft's XCTest cases with high coverage — boundary values, dtypes, memory layouts, NaN/inf, empty arrays, broadcasting, platform differences, performance and…
affaan-m/ECC
Python testing strategies using pytest, TDD methodology, fixtures, mocking, parametrization, and coverage requirements.
affaan-m/ECC
使用pytest的Python测试策略,包括TDD方法、夹具、模拟、参数化和覆盖率要求. An agent skill from affaan-m/ECC.
openJiuwen-ai/agent-core
Pytest patterns for the agent-core codebase: a red-green-refactor workflow, conftest fixtures, custom marks, monkeypatch and patch mocking, and async tests.
jh941213/my-cc-harness
Implement comprehensive testing strategies with pytest, fixtures, mocking, and test-driven development.
cryptomilk/hass-eink-dashboard
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…
cryptomilk/hass-eink-dashboard
Write TDD tests for a widget type: structural tests (borders, dividers), alignment tests (icon centering), and scaling tests (proportional sizing).
Works with
Categories
Implement a widget's SVG template and Python context builder. Implement Widget is an agent skill from cryptomilk/hass-eink-dashboard. Implement a widget's SVG template and Python context builder.
Implement Widget fits situations like: tasks that involve Test-driven development.
Run `npx skills add cryptomilk/hass-eink-dashboard --skill implement-widget -a claude-code`. Or copy the skill folder (.agents/skills/implement-widget in cryptomilk/hass-eink-dashboard) into .claude/skills/implement-widget in your project. Claude Code loads it when a task matches its description.
Run `npx skills add cryptomilk/hass-eink-dashboard --skill implement-widget -a codex`. Or copy the skill folder (.agents/skills/implement-widget in cryptomilk/hass-eink-dashboard) into .agents/skills/implement-widget in your project. Codex loads it when a task matches its description.
Cursor, Gemini CLI, GitHub Copilot and OpenCode also load SKILL.md folders. With the skills CLI, run `npx skills add cryptomilk/hass-eink-dashboard --skill implement-widget -a cursor` (or -a gemini-cli, github-copilot or opencode for the others). To copy it by hand, put the folder in .cursor/skills/implement-widget, .gemini/skills/implement-widget, .github/skills/implement-widget and .opencode/skills/implement-widget in your project.
Going by SKILL.md and its folder, Implement Widget needs the command-line tools its instructions call (uv). Our summary lists: Python 3. Its frontmatter pre-approves these tools: Read, Edit, Write, Bash(uv *).
SKILL.md names 1 domain. In commands or code: w3.org; the agent is likely to contact it when it follows the instructions. This is read from the text; nothing was executed.
Our automated static check of SKILL.md found no risky patterns, such as piping downloads into a shell, reading credential files or hidden Unicode. It is not a guarantee. Review the folder before installing.
Implement Widget is published under the Apache-2.0 licence (the repository's licence). It allows redistribution, so the full SKILL.md is shown on this page.
About 4.8k tokens (SKILL.md is roughly 19k characters). Agents keep only the skill's name and description in context until a task matches; then they load SKILL.md in full.
Skills that share tags, products or a category with Implement Widget: Wobbling Ty Constraint Order (astral-sh/ruff, 50k stars), Test Design (jjjkkkjjj/Matft, 147 stars), Python Testing (affaan-m/ECC, 275k stars) and Python Testing (affaan-m/ECC, 275k stars). The comparison table on this page puts their stars, adoption, token cost, safety result and licence side by side.
cryptomilk (a GitHub user) maintains it in cryptomilk/hass-eink-dashboard, which has 116 GitHub stars. The repository holds 3 skills in this directory. The repository was last updated on October 6, 2026.
Source: cryptomilk/hass-eink-dashboard on GitHub. Facts on this page come from the repository at the commit we read; the author's words are quoted as theirs.