Skip to content

Slot card and lock card redesign — unified design language#1116

Merged
raman325 merged 23 commits intomainfrom
phase2/slot-card-redesign
May 2, 2026
Merged

Slot card and lock card redesign — unified design language#1116
raman325 merged 23 commits intomainfrom
phase2/slot-card-redesign

Conversation

@raman325
Copy link
Copy Markdown
Owner

@raman325 raman325 commented May 1, 2026

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_title field on managed slot payloads so the lock card can render Slot N · {entry_title} without an extra round-trip.

Slot card iteration

  • Header rebuilt to match the lock card pattern: 40×40 key icon bubble + 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.
  • Hero band becomes a 2-row form-like layout. Top row: NAME label + name value + pencil. Bottom row: PIN label + value + eye, then ENABLED label + switch. All three labels share .hero-field-label.
  • Card-level state tinting (.slot-card-state-{active|inactive|disabled}) mirrors the lock card's slot-chip backgrounds: active blue, inactive warning orange, disabled muted grey.
  • ConditionsCondition (singular — only one entity per slot). State chip text Blocked by conditionsBlocked by condition.
  • Manage condition → inline Remove condition link (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.
  • Hero corners rounded; vertical padding tightened; hero min-height removed.

Lock card visual unification

  • State lcm-badges align with the slot card's .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 chip layout compacts to a single content row (name on the left, PIN on the right, no labels) with the slot label on top-left and badges stacked vertically on top-right.
  • Slot N · {entry_title} rendering for managed slots, matching the slot card's kicker.
  • Inactive managed slot background switches from muted blue to dulled warning orange.
  • Eye reveal button also renders for slots with configured_code or configured_code_length set, so disabled managed slots can still reveal their stored PIN.

Backend

  • _serialize_slot accepts config_entry_title and _serialize_lock_coordinator looks it up from the entry id so the lock card can render the per-slot kicker.
  • LockCoordinatorSlotData (frontend types) gains the matching config_entry_title? field.
  • tests/test_websocket.py::test_subscribe_lock_codes_response_shape asserts the new field is present on managed slots and matches the LCM config entry title.

Accessibility

  • Inline Remove link is keyboard-accessible (role=button, tabindex=0, aria-label, Enter/Space activation, Tab does NOT trigger).
  • Add Condition button has aria-label and stops propagation so it doesn't toggle the parent collapsible.
  • State chip / state badge dot prefix is decorative — text label still carries the meaning.
  • Editable name and PIN have .editable affordances; pencil/eye buttons keep their existing aria-labels.

Type of change

  • New feature (which adds functionality)

Additional information

  • This PR is related to issue: (none)

Test plan

  • npm test — 644 vitest tests passed across 15 files
  • uv run pytest tests/ --timeout=30 -q — 810 passed
  • prek run --all-files — all hooks pass
  • npm run build — bundle regenerated and committed

Manual verification (recommended before merge)

  1. Set up a dev HA instance with an LCM entry titled "House Locks" containing a slot named "Alice" with a PIN, two locks, and a calendar condition entity (e.g. calendar.vacation).
  2. Add the slot card (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.
  3. Slot card — Active state: card has primary blue tint, header reads "Slot 1 · House Locks" with a green state chip "Active". Hero shows NAME / Alice / pencil and PIN / •••• / eye + ENABLED / on.
  4. Slot card — Inactive state (calendar off): card has warning orange tint, chip reads "Blocked by condition" (singular).
  5. Slot card — Disabled state (toggle off): card has muted grey tint + 0.9 opacity, chip reads "Disabled by user".
  6. Slot card — Condition flow: with a condition set, "Remove condition" (warning orange) appears below the condition block; clicking it removes immediately. With no condition, click "Add condition" → dialog opens "Add condition" with empty ha-entity-picker. Pick an entity → dialog auto-commits and closes.
  7. Lock card — Slot chip: each managed slot's label reads 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

raman325 and others added 7 commits May 1, 2026 17:16
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.
Copilot AI review requested due to automatic review settings May 1, 2026 22:40
@github-actions github-actions Bot added python Pull requests that update Python code javascript Pull requests that update javascript code enhancement New feature or request labels May 1, 2026
@codecov
Copy link
Copy Markdown

codecov Bot commented May 1, 2026

Codecov Report

❌ Patch coverage is 96.87500% with 10 lines in your changes missing coverage. Please review.
✅ Project coverage is 96.59%. Comparing base (46c80a2) to head (7dd6fea).
⚠️ Report is 1 commits behind head on main.
✅ All tests successful. No failed tests found.

Files with missing lines Patch % Lines
ts/slot-card.ts 97.31% 7 Missing ⚠️
ts/slot-card-editor.ts 85.71% 2 Missing ⚠️
ts/lock-codes-card.ts 97.36% 1 Missing ⚠️
Additional details and impacted files

Impacted file tree graph

@@            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     
Flag Coverage Δ
python 97.24% <100.00%> (+<0.01%) ⬆️
typescript 94.64% <96.80%> (+3.20%) ⬆️

Flags with carried forward coverage won't be shown. Click here to find out more.

Files with missing lines Coverage Δ
custom_components/lock_code_manager/websocket.py 97.32% <100.00%> (+0.03%) ⬆️
ts/lock-codes-card.styles.ts 100.00% <ø> (ø)
ts/shared-styles.ts 100.00% <ø> (ø)
ts/slot-card.styles.ts 100.00% <ø> (ø)
ts/types.ts 100.00% <ø> (ø)
ts/lock-codes-card.ts 89.83% <97.36%> (+3.21%) ⬆️
ts/slot-card-editor.ts 91.47% <85.71%> (-1.22%) ⬇️
ts/slot-card.ts 96.95% <97.31%> (+6.20%) ⬆️
🚀 New features to boost your workflow:
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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_title in 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.

Comment thread ts/slot-card.ts Outdated
Comment thread ts/slot-card.ts Outdated
Comment thread ts/slot-card.ts Outdated
Comment thread ts/slot-card.ts Outdated
Comment thread ts/slot-card.ts
- 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.
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment thread ts/slot-card.ts Outdated
Comment thread ts/slot-card.ts
Comment thread ts/slot-card.ts Outdated
raman325 and others added 12 commits May 1, 2026 19:33
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
@raman325 raman325 changed the title Slot card redesign — first-principles layout Slot card and lock card redesign — unified design language May 2, 2026
raman325 added 3 commits May 1, 2026 23:38
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
@github-actions github-actions Bot added the documentation Documentation changes label May 2, 2026
@raman325 raman325 merged commit 12c034d into main May 2, 2026
19 checks passed
@raman325 raman325 deleted the phase2/slot-card-redesign branch May 2, 2026 03:46
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation Documentation changes enhancement New feature or request javascript Pull requests that update javascript code python Pull requests that update Python code

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants