Slot card and lock card redesign — unified design language#1116
Slot card and lock card redesign — unified design language#1116
Conversation
Replace the custom condition-entity rendering with a composition that delegates to HA's loadCardHelpers().createRowElement() for the entity itself and adds an LCM overlay strip below it that explains what the state means for *this slot* (allowing or blocking access, with a short context line). The collapsible body now contains: - condition-block: HA entity row + LCM overlay (allowing/blocking tint) - manage-link / empty-state add-link directly under the block - helpers sub-list rendered as plain HA entity rows Empty state now lives inside the section body (previous header row gone). The summary names the entity inline rather than counting passing/blocking conditions, since the section holds at most one condition entity. Dialog mode renamed `add-entity` / `edit-entity` -> `add` / `manage` ahead of Task 5's full dialog rewrite. Removed methods: _renderConditionEntity, _renderConditionContent, _renderConditionHeaderExtra, _getConditionEntityIcon, _getConditionStatusText, _getDomainLabel, _renderManageConditionsRow, _formatScheduleDate, _renderConditionContext, _openEntityMoreInfo. Added methods: _renderConditionsSummary, _renderConditionsBody, _renderConditionBlock, _renderOverlayContext, _renderHelpers, _formatRelative. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…yload
Task 7: remove orphan .lock-status-text style class (no longer rendered
after the per-lock status section was simplified) and document
show_lock_count as a no-op for the redesigned card layout (kept in the
editor so existing card configs continue to validate).
Task 8: config_entry_title is already in the subscribe_code_slot payload
and on the SlotCardData type. Add an explicit assertion to
test_subscribe_code_slot so the contract the new header relies on
("Slot N · {title}") is regression-protected.
Codecov Report❌ Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## main #1116 +/- ##
==========================================
+ Coverage 95.82% 96.59% +0.77%
==========================================
Files 47 47
Lines 5458 5497 +39
Branches 481 468 -13
==========================================
+ Hits 5230 5310 +80
+ Misses 228 187 -41
Flags with carried forward coverage won't be shown. Click here to find out more.
🚀 New features to boost your workflow:
|
There was a problem hiding this comment.
Pull request overview
Re-architects the Lock Code Manager slot card frontend to match the “first-principles” redesign: a new header + hero row, revamped Conditions UX using native HA rows and ha-entity-picker, and a new click-through “Last used” event row, with accompanying style and test updates.
Changes:
- Redesignes the slot card layout (header kicker + state chip + title, hero row for PIN/Enable, new event row).
- Refactors Conditions to use HA entity rows + LCM overlay context and replaces the condition dialog with
ha-entity-picker+ unified manage/add/remove flow. - Updates styling, removes unused shared status-dot styles, and adds/updates frontend + backend regression tests (incl. asserting
config_entry_titlein WS payload).
Reviewed changes
Copilot reviewed 6 out of 7 changed files in this pull request and generated 5 comments.
Show a summary per file
| File | Description |
|---|---|
| ts/slot-card.ts | Implements the redesigned slot card structure, new Conditions rendering/dialog, and new event row behavior. |
| ts/slot-card.styles.ts | Updates CSS to support the new header/hero/conditions overlay/event row designs and dialog styling. |
| ts/slot-card.integration.test.ts | Adds extensive integration coverage for new rendering paths and dialog/event-row behaviors. |
| ts/slot-card-editor.ts | Documents show_lock_count as a preserved no-op for backward compatibility. |
| ts/shared-styles.ts | Removes unused .lcm-status-dot styles from shared status indicator styles. |
| tests/test_websocket.py | Adds regression assertion that config_entry_title is included in subscribe_code_slot payload. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
- Add keyboard accessibility (role=button, tabindex, Enter/Space) to the event row and the Manage / Add condition affordances, plus a visible :focus-visible outline. - Fix _formatRelative reporting "in 1 day" for sub-24h deltas; check absolute milliseconds against the day boundary first so anything under one day collapses to "today" as the docstring already claimed. - Disable the destructive Remove button while _dialogSaving is true and add an early-return re-entry guard at the top of _removeCondition so rapid clicks can't double-fire clear_slot_condition. - Skip rendering the event row entirely when the event entity is in the unavailable state, and make _navigateToEventHistory a no-op in the same case (defense in depth) — clicking through to a more-info dialog with no useful content would be misleading.
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 6 out of 7 changed files in this pull request and generated 3 comments.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
CRITICAL silent failures: - _getEntityRow now catches exceptions and surfaces an error placeholder + _setActionError instead of an infinite Loading… spinner. - _saveConditionChanges and _removeCondition await _subscribe() so resubscribe failures land in the user-visible error banner. - getStubConfig now logs caught errors instead of silently swallowing. Copilot review on b041bea: - Event row checks entity exists in hass.states, not just !unavailable. - Helpers list excludes the active condition entity to prevent the shared cached DOM node from being moved between mount points. - Cancel disabled during in-flight save/remove; in-flight flag tracked separately from the close-resets-state path. Other fixes: - _saveConditionChanges gains the same re-entry guard as _removeCondition. - _startEditing('pin') resubscribe failure no longer swallowed; reverts _revealed and surfaces the error. - _setActionError now tracks its timeout so back-to-back errors don't cut each other off and disconnect cleans up. - _closeConditionDialog resets _dialogEntityId and _dialogMode.
Per design feedback: expansion should only be available when there's content to expand to. When no condition is set: - No helpers: render a static (non-collapsible) header row with a '+ Add condition' button on the right. No chevron, no toggle. - Helpers configured: render the normal collapsible (helpers are content worth expanding for) but place the Add button in the collapsed header instead of inside the body. Drop the empty-state callout since the Add affordance is now prominent on the row. Removes the now-unused .empty-state and .add-link CSS rules. Entire-Checkpoint: 76aa6e84b8e6
…renders The picker is a lazy-loaded HA element. The editor context auto-registers it; a standalone card doesn't. When the dialog opened, the unregistered <ha-entity-picker> tag stayed empty and users couldn't pick an entity. Force registration by piggybacking on the entities-card config element, which uses ha-entity-picker internally. Trigger the load both at connectedCallback (so it's ready before the dialog opens) and at _openConditionDialog (defensive fallback). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> Entire-Checkpoint: eab6e565c1e8
…t applies The .includeDomains array prop alone wasn't reliably filtering entities in the standalone card context (likely a Lit/HA upgrade-timing quirk with array properties). Adding a redundant .entityFilter function — which runs per entity regardless of property-set timing — guarantees the picker only shows calendar/schedule/binary_sensor/switch/input_boolean. Entire-Checkpoint: b6c3aa900267
- Reduce header bottom padding (16→12), content top padding (16→12), content gap (16→12), and hero vertical padding (18→14) to remove the stranded ~50px gap between the name and the hero PIN row. - Override .editable's dashed underline on .hero-pin-value: at 22px monospace with 4px letter-spacing it renders as broken dashes. Use a subtle hover background instead. Entire-Checkpoint: b7f8eca24fc8
…ggered When the PIN is masked and the user clicks it to edit, _startEditing flips _revealed=true so the input shows the actual value. Previously the reveal stayed on after Escape/Blur/Enter, leaving the PIN visible without the user having clicked the eye. Track whether _startEditing owned the reveal flip via _revealedForEdit and revert on edit exit (with a resubscribe so the websocket stops sending the unmasked PIN). User-triggered reveals (via the eye button before editing) are preserved. Entire-Checkpoint: 022640de3827
The dialog's <ha-button slot=...> elements weren't rendering — likely because ha-button is lazy-loaded by HA and never triggered in the standalone card context. Combined with selection requiring a Save click, users were stuck with a picker that did nothing. Redesign the dialog flow: - Picker auto-commits on value-changed (when value is non-empty AND differs from the slot's current condition entity). Closes immediately. - No Save / Cancel buttons. Esc / backdrop closes the dialog. - Remove button (manage mode) renders inline in the body as a plain styled button — no slot dependency. - Saving… indicator shows briefly during in-flight commits. Removes the now-unused _saveConditionChanges method and tests. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> Entire-Checkpoint: 791a7e2ec912
Strikethrough was the wrong visual — it semantically implies 'deleted' but the PIN is just not currently on the lock. It also rendered as broken dashes through dot characters. Split the single .disabled state into two: - .off: slot disabled by user. PIN exists in config but intentionally not pushed. Heavily dimmed dots in a muted pill background. - .pending: slot enabled but lock doesn't have the code yet (out-of-sync, syncing, etc.). Dim dots with a clock-icon prefix. Both drop the strikethrough; the visual cause is now self-evident. Entire-Checkpoint: 3ece33f9f3dd
… card Mirrors the PIN treatment from the previous commit. The slot name no longer gets a strikethrough when the slot is disabled — instead: - Off (slot disabled by user): muted pill background, no decoration. - Pending (slot enabled but lock missing the code): clock-icon prefix. Adds a 'pending' modifier to the slot-chip class list so the same visual semantics apply consistently across the PIN and the name. Entire-Checkpoint: 4a15eb5f1cb0
- Drop .content padding-top so the hero butts up against the header's reduced bottom padding (header 12 → 8). The hero's tinted background provides the visual section break; no extra padding needed between. - Reduce hero vertical padding (14 → 12) and remove the unnecessary min-height: 1.5em on .hero-pin-value (line-height already provides the needed vertical breathing room). - Round the hero corners (border-radius: 12px) to match the other sections; drop the border-top that no longer makes visual sense on a rounded tinted section. Entire-Checkpoint: 4021c802f73b
…oordinator payload
The lock card now renders "Slot N · {entry_title}" for managed slots so
users can see which LCM instance owns each slot without an extra
round-trip. The websocket subscribe_lock_codes payload looks up the
config entry title from the entry id and serializes it alongside the
existing config_entry_id field. Frontend types.ts gains the matching
config_entry_title? field on LockCoordinatorSlotData; tests verify the
new field is present and matches the LCM config entry title for managed
slots in the response shape contract.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Entire-Checkpoint: 4f7fb1fe17b1
Slot card iteration:
- Header redesigned to match the lock card pattern: 40x40 key icon
bubble + "Slot N · {entry_title}" as the 18px title + state chip on
the right. The kicker no longer carries small-uppercase styling — it
IS the title now. The name moves out of the header into the hero band.
- Hero band becomes a 2-row form-like layout. Top row: NAME + name
value + pencil. Bottom row: PIN + value + eye, then ENABLED + switch.
All three labels share a single .hero-field-label style (small caps).
- Card-level state tinting via .slot-card-state-{active|inactive|
disabled} mirrors the lock card's slot-chip backgrounds (primary
blue / warning orange / muted grey). A slot now reads the same
regardless of which card you see it on.
- "Conditions" renamed to "Condition" (singular — only one entity
per slot). State chip text "Blocked by conditions" → "Blocked by
condition".
- "Manage condition" replaced by an inline warning-orange "Remove
condition" link below the condition block; the dialog is now
Add-only and the destructive Remove button is gone from the dialog
body. To swap conditions: remove then re-add.
- Hero corners rounded; vertical padding tightened (12px); hero
min-height removed; content padding-top now 12px (was 0).
Lock card visual unification (extends the iteration started in
b1bd00c/4c73fdda):
- Active/Inactive/Disabled lcm-badges align with the slot card's
.state-chip: 12px radius pill, sentence case, dot prefix, success
green / warning orange / disabled grey. Identity badges
(Managed/Unmanaged) keep the compact uppercase look so they stay
visually distinct from state badges.
- Slot chip layout compacts to a single content row: name on the
left, PIN on the right, no labels. The .slot-top is now row-flex
with the slot label on the left and the badges stacked vertically
on the right via flex-direction: column.
- Slot label gains "Slot N · {entry_title}" rendering (managed slots
only) using the new config_entry_title field, matching the slot
card's kicker.
- Inactive managed slot background switches from muted blue to dulled
warning orange, matching the slot card's inactive state tinting.
- Eye reveal button now also renders for slots with configured_code
or configured_code_length set, so disabled managed slots can still
reveal their stored PIN.
Tests updated for the structural changes (header → icon+title+chip;
hero name row; remove-link replaces manage-link; dialog Add-only;
_handlePickerChange single-arg; _dialogMode removed). New coverage
added for the card-level state class, hero name row affordance,
remove-link keyboard activation, lock card "Slot N · {entry_title}"
rendering, state badge dots, and eye reveal on disabled managed slots.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Entire-Checkpoint: 9931f0b85615
Design:
- Name promoted to 22px hero anchor (matches PIN); drop NAME/ENABLED
labels (form self-describes via typography). PIN keeps its label.
- Placeholder copy: <No Name>/<No PIN> -> 'Not named'/'No PIN set'.
- Editor copy: 'Conditions' -> 'Condition' (singular). YAML accepts
both 'condition' and 'conditions' for backward compat.
Accessibility (CRITICAL — WCAG 2.1.1 violations):
- Hero PIN value: role/tabindex/keydown for keyboard editing.
- Hero name value: same treatment as PIN.
- Lock-card slot chip (managed): keyboard-navigable role/tabindex/keydown.
- Lock-name in slot card: same treatment.
- Collapsible header: role/tabindex/aria-expanded/keydown.
- Action error banner: role='alert'.
- Saving indicator: aria-live='polite'.
- Reveal button on managed lock-card chip: stopPropagation so it
doesn't also trigger chip navigation.
Bug:
- Name-edit auto-focus selector ('.name .name-edit-input') no longer
matched after the hero refactor; corrected to '.name-edit-input'.
Added :focus-visible outlines for the newly keyboard-reachable elements.
Entire-Checkpoint: 4c88931cd5e2
…compiles
The narrowed default literal ['condition', 'lock_status'] caused .includes('conditions')
to fail typecheck — fix by typing the local as ReadonlyArray<string>.
Entire-Checkpoint: 71a4851b867f
The slot and lock cards now share state language (badges, dots, colors), slot identifier pattern, two-state PIN treatment, and card-level state tinting. Layout intentionally diverges (focused single-slot vs compact multi-slot list). Entire-Checkpoint: c56cadc01604
Proposed change
Redesigns the LCM slot card and brings the lock card into visual alignment so a slot reads the same regardless of which card you see it on. Touches both the slot-card.ts hierarchy (header, hero, condition flow, dialog) and the lock-card chip (state badges, layout, slot label, eye reveal). Backend gains a
config_entry_titlefield on managed slot payloads so the lock card can renderSlot N · {entry_title}without an extra round-trip.Slot card iteration
Slot N · {entry_title}as an 18px title + state chip on the right. The kicker IS the title now (no more small-uppercase treatment for it). The name moves out of the header.NAMElabel + name value + pencil. Bottom row:PINlabel + value + eye, thenENABLEDlabel + switch. All three labels share.hero-field-label..slot-card-state-{active|inactive|disabled}) mirrors the lock card's slot-chip backgrounds: active blue, inactive warning orange, disabled muted grey.Conditions→Condition(singular — only one entity per slot). State chip textBlocked by conditions→Blocked by condition.Manage condition→ inlineRemove conditionlink (warning orange, below the condition block). The dialog is now Add-only — to swap conditions: remove then re-add. Destructive Remove button is gone from the dialog body.Lock card visual unification
.state-chip: 12px radius pill, sentence case, colored dot prefix, success green / warning orange / disabled grey. Identity badges (Managed/Unmanaged) keep the compact uppercase look so they stay distinct from state badges.Slot N · {entry_title}rendering for managed slots, matching the slot card's kicker.configured_codeorconfigured_code_lengthset, so disabled managed slots can still reveal their stored PIN.Backend
_serialize_slotacceptsconfig_entry_titleand_serialize_lock_coordinatorlooks it up from the entry id so the lock card can render the per-slot kicker.LockCoordinatorSlotData(frontend types) gains the matchingconfig_entry_title?field.tests/test_websocket.py::test_subscribe_lock_codes_response_shapeasserts the new field is present on managed slots and matches the LCM config entry title.Accessibility
role=button,tabindex=0,aria-label, Enter/Space activation, Tab does NOT trigger)..editableaffordances; pencil/eye buttons keep their existing aria-labels.Type of change
Additional information
Test plan
npm test— 644 vitest tests passed across 15 filesuv run pytest tests/ --timeout=30 -q— 810 passedprek run --all-files— all hooks passnpm run build— bundle regenerated and committedManual verification (recommended before merge)
calendar.vacation).type: custom:lcm-slot,config_entry_title: House Locks,slot: 1) and a lock card (type: custom:lcm-lock-codes,lock_entity_id: lock.front_door) to a Lovelace dashboard.ha-entity-picker. Pick an entity → dialog auto-commits and closes.SLOT N · {entry_title}, badges stack vertically (state with dot prefix on top, Managed below). Inactive managed slot has the warning orange background. Disabled managed slot still shows the eye reveal so the stored PIN can be revealed.🤖 Generated with Claude Code