Skip to content

feat(cards): derive layout tiers from the effective grid span - #220

Merged
fx merged 6 commits into
mainfrom
feat/0011-tier-derivation
Jul 27, 2026
Merged

feat(cards): derive layout tiers from the effective grid span#220
fx merged 6 commits into
mainfrom
feat/0011-tier-derivation

Conversation

@fx

@fx fx commented Jul 27, 2026

Copy link
Copy Markdown
Owner

Summary

Tier derivation and the plumbing that carries it to cards — the first task of docs/changes/0011-layout-tiers.md. data-tier is now stamped, which closes the last deferred entry in the stable selector contract.

⚠️ This PR contains a temporary visual regression

Between this change and its PRs 2 and 3, a card placed small renders larger than it did, and a card placed large renders smaller. Every value that used to scale off size — min-heights, the thermostat's arc radius, glyph sizes, button and text steps — is pinned to what the medium branch produced, with a comment at each site naming the PR that owns the real tier layout.

That is deliberate. Re-keying those tables onto tiers here would be doing PRs 2 and 3 badly, in a change whose scope is derivation and plumbing. But it is user-visible on main in the interim, so it belongs in the description rather than in the diff.

Derivation

src/utils/cardTier.ts is pure — no React, no grid. deriveCardTier({width, height}) partitions on width >= 2 and height >= 2 into full | row | tall | glance, total by construction with glance as the floor. scaleSpanToColumns maps stored span to effective span (width only; rows do not scale with the breakpoint), floored at one cell.

The card cannot be told something different from what the grid laid out. GridLayoutSection computes effectiveSpanOf(item) once and reads the same expression twice — to build the react-grid-layout layout, and to call children(item, effectiveSpan). A test walks the rendered .react-grid-item elements and asserts the callback's span equals the w/h the grid received.

offsetWidth is untouched: it still measures exactly what it measured before, and nothing was added to liebe-section — no padding, no border, no wrapper — so the constraint the theming spec records is preserved. The span plumbing is arithmetic on stored values, not measurement.

tier defaults to row rather than being required, because shells render outside grids (config preview, sidebar widget, stories) and the contract promises data-tier is present. A well-chosen default hides its own failure, so there is a test that a card inside a grid receives a derived tier and never the fallback — two spans the fallback cannot produce, asserted positively and against row. Severing the derivation fails it.

The size prop, and the tests that asserted it

size is gone from CardProps and GridCard, which touches all 14 domain cards, the camera, the four weather variants, TextCard and Separator. Non-grid consumers migrated: the config preview takes the effective span, the quick-controls widget passes an explicit 2×1, and the theme gallery derives each cell's tier from its own span.

Twelve test blocks asserted the removed API. They were handled by shape, not uniformly:

  • Six deleted — dedicated size variants blocks asserting only that the shell stamps a prop it was handed. That is shell behaviour, covered once in the shell's own test; seven per-card copies existed only because size was a per-card prop. Translating them would have produced seven copies of one passthrough that look like per-card tier coverage while testing nothing card-specific.
  • One translatedLightCard's, whose comment ties it to the shell's min-height stops that this PR replaces. That is a behaviour thread, not a passthrough.
  • Two collapsedshould render different sizes correctly and should show humidity in all sizes never tested size; they asserted the same thing three times with size as decoration. The inner assertion is real, so each became one render and one assertion, with the misleading name dropped.
  • One line swappeddata-sizedata-tier in the shell's contract-attribute test.
  • Two rewritten as exemption guards — the camera's matting and stats tests. The change document exempts the camera from tier consumption, so the constant it now feeds is the exemption; both assert it holds at every tier and fail legibly if span-keyed behaviour returns.

The condition on those deletions: the change document now requires PRs 2 and 3 to deliver a per-card tier assertion for each card, not only a story, naming that they replace the blocks removed here.

Floors, measured

The shell's three data-size min-height stops became two data-tier stops (60px glance/row, 120px tall/full), since height is the only thing a floor expresses and only the row count moves it. Inner minHeight floors were removed from five cards — collapsing their size ternary to medium had left every card propped open from the inside at a height that would have stopped a glance tile ever being one cell tall, quietly defeating the change. The shell owns the floor now.

Measured in Chromium against the shipped dist/liebe.css, reading offsetHeight and computed min-block-size: the quick-controls strip is byte-identical at 72px (the floor never binds there), the config preview's 2×2 item grows 28px and a short TextCard preview shrinks 8px — both inside a panel that already reserves 200px and centres its content, so the dialog does not move.

Testing

  • npm test — 1667 passed, 2 skipped
  • npm run lint, npm run typecheck — clean
  • Patch coverage — 53/53 changed lines, zero partial branches
  • npm run build-storybook, npm run build:ha:prod — both succeed
  • cr — no findings on the final state. codex unavailable (usage limit until 1 August)

Found, not fixed

A pre-existing data-integrity bug surfaced while naming the scaling: handleLayoutChange inverse-scales every item on any drag, and the round-trip is lossy at narrow breakpoints — so moving one card on a phone silently rewrites the stored width and position of every other card. Untouched here; filed as #219, and worth fixing before PRs 2 and 3 have people dragging cards at every breakpoint.

Scope

The per-card tier layouts are PRs 2 and 3; the change stays draft until they land.

Summary by CodeRabbit

  • New Features
    • Introduced responsive card tiers—glance, row, tall, and full—derived from each card’s effective grid span.
    • Card layouts now adapt across breakpoints, with previews reflecting the rendered tier.
    • Glance error cards provide accessible names and open detail dialogs with Retry and Dismiss actions.
    • Loading placeholders now adjust their content and sizing by tier.
  • Bug Fixes
    • Improved consistency for card heights, spacing, and unavailable states across layouts.
  • Documentation
    • Updated design-system and theming specifications to document tier behavior and rendered attributes.

Implements PR 1 of docs/changes/0011-layout-tiers.md: the derivation and
the plumbing that carries it, with no per-card tier layouts yet.

`deriveCardTier` partitions an effective span into glance/row/tall/full,
and `scaleSpanToColumns` is the stored-to-effective mapping the grid
already applied inline. `GridLayoutSection` now reads one expression for
both the react-grid-layout layout and its child callback, so what a card
is told about its size cannot drift from what the grid lays out, and it
does that with arithmetic on the stored span rather than by measuring —
`liebe-section`'s offsetWidth is untouched. `GridView` is the only place
that derives a tier from a layout; cards take tier and span as props.

The legacy `size` prop is gone from `CardProps` and `GridCard`, and with
it the shell's three min-height stops, which become two: only the row
count moves a height floor. Cards that scaled themselves off `size` are
collapsed to a single rendering, and the inner min-height floors they
carried are removed outright — the shell owns the floor now, keyed on the
tier, so a one-cell glance tile is not propped open from the inside.

This is a user-visible interim regression on main: between this change
and its PRs 2 and 3, a card placed small renders larger than it did and
one placed large renders smaller, because every size-scaled value is
pinned to what `medium` produced. The per-card tier layouts restore it.

Non-grid renderers migrate here too, since removing `size` breaks them:
the configuration preview takes the effective span from whoever opens it,
`QuickControlsWidget` states an explicit row tier, and the workshop's
theme gallery derives each cell's tier from that cell's span. Skeleton
and card-shaped error states are tier-aware — a glance placeholder is a
small tile rather than a shorter large one, and a glance error omits its
message and actions, keeping the message as the tile's tooltip.

`data-tier` moves from Deferred to Stamped in the stable selector
contract. The camera receives the stamp without consuming it, per this
change's exemption; two of its tests now assert that exemption rather
than the span-keyed behaviour it replaced.
Copilot AI review requested due to automatic review settings July 27, 2026 03:12
@coderabbitai

coderabbitai Bot commented Jul 27, 2026

Copy link
Copy Markdown

Review Change Stack

Warning

Review limit reached

You’ve reached a temporary PR review limit under our Fair Usage Limits Policy.

Your recent review volume is higher than typical usage, so adaptive limits are currently applied.

Next review available in: 8 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 8a5d9547-7abe-4dde-8b2a-5cdb5aaf4014

📥 Commits

Reviewing files that changed from the base of the PR and between cb03909 and bbad7d0.

📒 Files selected for processing (20)
  • docs/changes/0011-layout-tiers.md
  • docs/specs/design-system/index.md
  • src/components/BinarySensorCard.tsx
  • src/components/ButtonCard.tsx
  • src/components/GridCard.css
  • src/components/InputBooleanCard.tsx
  • src/components/InputDateTimeCard.tsx
  • src/components/InputNumberCard.tsx
  • src/components/InputTextCard.tsx
  • src/components/LightCard.tsx
  • src/components/__tests__/cardLoadingTier.test.tsx
  • src/components/__tests__/cardShellStyles.test.ts
  • src/components/__tests__/cardSpanMemo.test.tsx
  • src/components/ui/ErrorDisplay.tsx
  • src/components/ui/SkeletonCard.tsx
  • src/components/ui/cardStates.test.tsx
  • src/styles/tokens.css
  • src/theme/tokens.ts
  • src/utils/__tests__/cardTier.test.ts
  • src/utils/cardTier.ts
📝 Walkthrough

Walkthrough

The card layout system replaces legacy size props with responsive tier and span values. Effective spans are derived from breakpoint columns, card shells stamp data-tier, previews preserve layout spans, and skeleton/error states now render tier-specific behavior.

Changes

Responsive tier propagation

Layer / File(s) Summary
Tier model and grid plumbing
src/utils/cardTier.ts, app/utils/responsive.ts, src/components/GridLayoutSection.tsx, src/components/GridView.tsx
Defines tier/span utilities, scales spans across breakpoints, derives tiers from effective spans, and passes span data through grid configuration flows.
Card rendering and state migration
src/components/*Card.tsx, src/components/GridCard.tsx, src/components/ui/*Card.tsx, src/components/CardConfig.tsx
Replaces card size props with tier and optional span, stamps data-tier, updates previews, and centralizes tier-based shell sizing.
Validation and supporting contracts
src/components/**/__tests__/*, src/utils/__tests__/cardTier.test.ts, docs/specs/*, docs/changes/*
Adds tier, responsive span, loading-state, error-state, preview, and selector-contract coverage while removing legacy size-variant assertions.

Estimated code review effort: 4 (Complex) | ~60 minutes

Sequence Diagram(s)

sequenceDiagram
  participant GridLayoutSection
  participant GridView
  participant CardShell
  participant CardConfig
  GridLayoutSection->>GridView: provide effective span
  GridView->>GridView: derive tier from span
  GridView->>CardShell: render tier and span
  GridView->>CardConfig: open preview with span
  CardConfig->>CardShell: derive preview tier and render
Loading

Possibly related PRs

  • fx/liebe#194: Both update Storybook card sizing controls and related card story modules.
  • fx/liebe#200: Both modify the card shell and its stable data-* selector contract.
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 23.08% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately captures the main change: deriving card layout tiers from the effective grid span.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/0011-tier-derivation

Comment @coderabbitai help to get the list of available commands.

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Pull request overview

Implements the first stage of layout tiers (change 0011) by deriving a card’s tier from its effective rendered grid span, plumbing that tier/span through the grid → cards → config previews, and stamping data-tier on the card shell to complete the stable selector contract.

Changes:

  • Added deriveCardTier + scaleSpanToColumns utilities (with unit tests) and a named getEffectiveColumns breakpoint helper.
  • Updated grid rendering to compute an effective span once, derive tier from it, and pass tier + span through to cards and configuration previews.
  • Replaced legacy size plumbing with tier-aware shell attributes and updated cards/stories/tests accordingly (including tier-aware skeleton/error states).

Reviewed changes

Copilot reviewed 69 out of 69 changed files in this pull request and generated no comments.

Show a summary per file
File Description
src/utils/cardTier.ts Introduces tier derivation and stored→effective span scaling utilities.
src/utils/tests/cardTier.test.ts Unit coverage for tier boundaries, span scaling, and effective column behavior.
src/theme/customCss.stories.tsx Updates story usage from size to explicit tier.
src/components/widgets/QuickControlsWidget.tsx Sidebar widget now passes explicit tier + span for non-grid rendering.
src/components/WeatherCard/WeatherCardModern.tsx Migrates weather modern variant from size to tier and pins interim visuals.
src/components/WeatherCard/WeatherCardMinimal.tsx Migrates weather minimal variant to tier and tier-aware skeleton/error.
src/components/WeatherCard/WeatherCardDetailed.tsx Migrates weather detailed variant to tier and pins interim visuals.
src/components/WeatherCard/WeatherCardDefault.tsx Migrates weather default variant to tier and pins interim visuals.
src/components/WeatherCard/WeatherCard.stories.tsx Story controls/args migrated from size to tier.
src/components/WeatherCard/index.tsx Ensures config preview receives the same span the card was given.
src/components/WeatherCard.test.tsx Removes/condenses size-based tests; keeps meaningful data-point assertions.
src/components/ui/SkeletonCard.tsx Makes skeletons tier-aware (composition + sizing keyed by tier).
src/components/ui/ErrorDisplay.tsx Adds optional tier for card-variant degradation at glance.
src/components/ui/cardStates.test.tsx Tests tier-aware loading and error placeholder behavior.
src/components/TextCard.tsx Removes size prop and inner height floors to let shell/tier own sizing.
src/components/TextCard.stories.tsx Removes size control/args from TextCard stories.
src/components/Separator.tsx Removes unused size prop from separator component surface.
src/components/SensorCard.tsx Migrates to tier, removes size-keyed inner floors, updates memo compare.
src/components/SensorCard.stories.tsx Migrates story controls/args from size to tier.
src/components/LightCard.tsx Migrates to tier and threads span for card-owned config preview.
src/components/LightCard.stories.tsx Migrates story controls/args from size to tier.
src/components/InputTextCard.tsx Migrates to tier and normalizes control sizing for interim state.
src/components/InputTextCard.test.tsx Removes deleted size variants coverage.
src/components/InputTextCard.stories.tsx Migrates story controls/args from size to tier.
src/components/InputSelectCard.tsx Migrates to tier and normalizes text sizing for interim state.
src/components/InputSelectCard.test.tsx Removes deleted size variants coverage.
src/components/InputSelectCard.stories.tsx Migrates story controls/args from size to tier.
src/components/InputNumberCard.tsx Migrates to tier and normalizes control sizing for interim state.
src/components/InputNumberCard.test.tsx Removes deleted size variants coverage.
src/components/InputNumberCard.stories.tsx Migrates story controls/args from size to tier.
src/components/InputDateTimeCard.tsx Migrates to tier and normalizes control sizing for interim state.
src/components/InputDateTimeCard.test.tsx Removes deleted size variants coverage.
src/components/InputDateTimeCard.stories.tsx Migrates story controls/args from size to tier.
src/components/InputBooleanCard.tsx Migrates to tier and normalizes switch sizing for interim state.
src/components/InputBooleanCard.test.tsx Removes deleted size variants coverage.
src/components/InputBooleanCard.stories.tsx Migrates story controls/args from size to tier.
src/components/GridView.tsx Derives tier from effective span and plumbs tier/span into cards + config modal.
src/components/GridLayoutSection.tsx Exposes effective span via child callback and centralizes effective column logic.
src/components/GridCard.tsx Replaces data-size with data-tier and updates context/props accordingly.
src/components/GridCard.stories.tsx Updates shell story controls and renames size showcase to tier showcase.
src/components/GridCard.css Moves shell floor sizing to tier-based min-block-size stops.
src/components/FanCard.tsx Migrates to tier, removes size-based inner floors, updates memo compare.
src/components/FanCard.stories.tsx Migrates story controls/args from size to tier.
src/components/CoverCard.tsx Migrates to tier, removes size-based inner floors, updates memo compare.
src/components/CoverCard.test.tsx Removes deleted size variants coverage.
src/components/CoverCard.stories.tsx Migrates story controls/args from size to tier.
src/components/ClimateCard.tsx Migrates to tier, removes size-based inner floors, pins interim visuals.
src/components/ClimateCard.stories.tsx Migrates story controls/args from size to tier.
src/components/cardRegistry.ts Updates shared CardProps contract to tier + (effective) span.
src/components/CardConfig.tsx Makes config preview tier-correct via passed effective span fallbacking to stored.
src/components/CameraCard/index.tsx Switches to tier stamping only; keeps camera sizing behavior span-independent here.
src/components/CameraCard/CameraCard.stories.tsx Migrates story controls/args from size to tier.
src/components/CameraCard/tests/CameraCard.test.tsx Adds tier-exemption guard assertions for camera overlays/matting.
src/components/ButtonCard.tsx Migrates to tier and updates memo compare for tier changes.
src/components/ButtonCard.stories.tsx Migrates story controls/args from size to tier.
src/components/BinarySensorCard.tsx Migrates to tier and threads span for card-owned config preview.
src/components/BinarySensorCard.stories.tsx Migrates story controls/args from size to tier.
src/components/tests/LightCard.test.tsx Updates shell contract assertion from data-size to data-tier.
src/components/tests/GridView.tier.test.tsx End-to-end tests for effective-span tier derivation + config preview tier.
src/components/tests/GridLayoutSection.test.tsx Tests the effective span callback matches what the grid actually receives.
src/components/tests/GridCard.test.tsx Updates selector attribute stamping test to data-tier.
src/components/tests/cardLoadingTier.test.tsx Table test ensuring every card passes tier into loading skeletons.
src/components/tests/CardConfig.test.tsx Tests preview tier uses caller span and falls back to stored dimensions.
src/components/tests/ButtonCard.test.tsx Removes size-decorative rerender loop; keeps meaningful assertion.
docs/specs/theming/index.md Moves data-tier into the stamped stable selector contract and documents semantics.
docs/specs/design-system/index.md Updates status to “tier derivation implemented” (per-tier layouts still pending).
docs/changes/0011-layout-tiers.md Marks PR1 task complete; strengthens PR2/PR3 test obligations.
app/utils/responsive.ts Adds getEffectiveColumns helper for breakpoint vs stored column selection.
.storybook/themeGallery.tsx Gallery derives tier/span from its own tile dimensions to mirror GridView behavior.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

…tion

# Conflicts:
#	docs/specs/design-system/index.md
#	docs/specs/theming/index.md
@codecov

codecov Bot commented Jul 27, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 73.93%. Comparing base (133355c) to head (8679d9e).
⚠️ Report is 1 commits behind head on main.

Additional details and impacted files
@@            Coverage Diff             @@
##             main     #220      +/-   ##
==========================================
+ Coverage   72.45%   73.93%   +1.48%     
==========================================
  Files         136      137       +1     
  Lines        5481     5482       +1     
  Branches     1761     1737      -24     
==========================================
+ Hits         3971     4053      +82     
+ Misses       1092     1052      -40     
+ Partials      418      377      -41     

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

Copilot AI left a comment

Copy link
Copy Markdown

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 69 out of 69 changed files in this pull request and generated 1 comment.

Comment thread src/components/GridView.tsx
The text and separator branches of `GridView` opened the configuration
modal without a span. The reasoning for that was that `TextCard` and
`Separator` take no tier, so there was nothing for a span to feed — but
the preview does not render those components bare. `CardConfig` wraps
both in the card shell and stamps `data-tier` on it, derived from the
span it is given, falling back to the item's stored dimensions when it
is given none. On a narrow breakpoint that advertises a tier the item is
not laid out at, and `data-tier` is under the stable selector contract,
so a theme selecting on it styles the preview as a different card than
the one behind it.

Pass the span at both call sites, and make it a required parameter of
`handleConfigureItem` so the next branch added there cannot quietly
repeat this.

The two leaves accept `onConfigure` and ignore it — neither has a
configure affordance yet — so the new test stands in for them with stubs
that do nothing but call the callback; the grid, the modal and the shell
underneath are all real.

Copilot AI left a comment

Copy link
Copy Markdown

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 70 out of 70 changed files in this pull request and generated 1 comment.

Comment thread src/components/ui/ErrorDisplay.tsx Outdated
At `glance` the card-shaped error tile had room for the icon and the short
title only, and kept the message in a `title` attribute. That is close to
inaccessible on Liebe's primary surface: a wall tablet has no hover to
reveal a tooltip, and `title` is exposed to assistive technology
inconsistently even where hover exists. The tile also dropped `Retry`
outright, which on a disconnected tile is the only way out of the state.

The tile now carries the message as its accessible name and becomes a
button onto the detail dialog, which holds the full message and the
`Retry`/`Dismiss` actions. Both users reach the detail: assistive
technology announces it from the name without opening anything, and a
touch user presses the tile.

The dialog's `Retry` is soft red rather than solid: white on `red-9`
measures 3.91:1 and `Modal` forces `blue` (4.25:1) on an uncoloured
primary action, so either would have added an AA contrast violation to
the workshop's a11y baseline. Soft red is 4.54:1, and axe reports 0
violations on the affected stories in both appearances.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 4

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/components/InputTextCard.tsx (1)

183-221: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Increase these interactive hit areas. TextField.Root and the three IconButtons are still size="2"; bump them to size="3" or otherwise ensure the final touch targets are at least 44px.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/components/InputTextCard.tsx` around lines 183 - 221, Increase the touch
targets in the InputTextCard editing and display controls: update TextField.Root
and all three IconButton elements from size="2" to size="3", or apply equivalent
styling that guarantees each final interactive target is at least 44px. Preserve
their existing behavior and layout.

Source: Coding guidelines

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@docs/changes/0011-layout-tiers.md`:
- Around line 45-46: Remove the behavioral error-tile accessibility and recovery
contract from docs/changes/0011-layout-tiers.md, retaining only rollout context
and a link to the single owning specification. Ensure the owning specification
documents tier-aware SkeletonCard/error/unavailable behavior, including the
glance accessible name, detail-dialog button, message, Retry, and Dismiss
actions.

In `@src/components/GridCard.css`:
- Around line 78-99: Define `--liebe-*` tier-floor custom properties in the base
layer for the 60px and 120px values, then update the
`[data-tier='glance']`/`[data-tier='row']` and
`[data-tier='tall']`/`[data-tier='full']` rules to use those tokens via
`var(...)`. Preserve the existing tier mappings and base values while enabling
theme and user overrides.

In `@src/components/InputDateTimeCard.tsx`:
- Around line 203-214: Restore compliant touch targets across
InputDateTimeCard.tsx lines 203-214 and 237-238, and InputNumberCard.tsx lines
209, 224, and 251-252: set each interactive input and
edit/decrement/increment/submit/cancel control to size="3" or larger, and ensure
each has a guaranteed minimum 44px touch target.

In `@src/components/ui/SkeletonCard.tsx`:
- Around line 97-109: Update SkeletonCard’s root Card rendering to preserve the
shared liebe-card shell and data-tier selector contract, while moving minHeight
into the layered tier styling rather than an inline-only value. Ensure loading
cards remain targetable by themes, and add a test covering the rendered shell
class/attribute and tier selector.

---

Outside diff comments:
In `@src/components/InputTextCard.tsx`:
- Around line 183-221: Increase the touch targets in the InputTextCard editing
and display controls: update TextField.Root and all three IconButton elements
from size="2" to size="3", or apply equivalent styling that guarantees each
final interactive target is at least 44px. Preserve their existing behavior and
layout.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 6a845d21-7ca2-4eb9-b7c0-4ed366533837

📥 Commits

Reviewing files that changed from the base of the PR and between 133355c and cb03909.

📒 Files selected for processing (70)
  • .storybook/themeGallery.tsx
  • app/utils/responsive.ts
  • docs/changes/0011-layout-tiers.md
  • docs/specs/design-system/index.md
  • docs/specs/theming/index.md
  • src/components/BinarySensorCard.stories.tsx
  • src/components/BinarySensorCard.tsx
  • src/components/ButtonCard.stories.tsx
  • src/components/ButtonCard.tsx
  • src/components/CameraCard/CameraCard.stories.tsx
  • src/components/CameraCard/__tests__/CameraCard.test.tsx
  • src/components/CameraCard/index.tsx
  • src/components/CardConfig.tsx
  • src/components/ClimateCard.stories.tsx
  • src/components/ClimateCard.tsx
  • src/components/CoverCard.stories.tsx
  • src/components/CoverCard.test.tsx
  • src/components/CoverCard.tsx
  • src/components/FanCard.stories.tsx
  • src/components/FanCard.tsx
  • src/components/GridCard.css
  • src/components/GridCard.stories.tsx
  • src/components/GridCard.tsx
  • src/components/GridLayoutSection.tsx
  • src/components/GridView.tsx
  • src/components/InputBooleanCard.stories.tsx
  • src/components/InputBooleanCard.test.tsx
  • src/components/InputBooleanCard.tsx
  • src/components/InputDateTimeCard.stories.tsx
  • src/components/InputDateTimeCard.test.tsx
  • src/components/InputDateTimeCard.tsx
  • src/components/InputNumberCard.stories.tsx
  • src/components/InputNumberCard.test.tsx
  • src/components/InputNumberCard.tsx
  • src/components/InputSelectCard.stories.tsx
  • src/components/InputSelectCard.test.tsx
  • src/components/InputSelectCard.tsx
  • src/components/InputTextCard.stories.tsx
  • src/components/InputTextCard.test.tsx
  • src/components/InputTextCard.tsx
  • src/components/LightCard.stories.tsx
  • src/components/LightCard.tsx
  • src/components/SensorCard.stories.tsx
  • src/components/SensorCard.tsx
  • src/components/Separator.tsx
  • src/components/TextCard.stories.tsx
  • src/components/TextCard.tsx
  • src/components/WeatherCard.test.tsx
  • src/components/WeatherCard/WeatherCard.stories.tsx
  • src/components/WeatherCard/WeatherCardDefault.tsx
  • src/components/WeatherCard/WeatherCardDetailed.tsx
  • src/components/WeatherCard/WeatherCardMinimal.tsx
  • src/components/WeatherCard/WeatherCardModern.tsx
  • src/components/WeatherCard/index.tsx
  • src/components/__tests__/ButtonCard.test.tsx
  • src/components/__tests__/CardConfig.test.tsx
  • src/components/__tests__/GridCard.test.tsx
  • src/components/__tests__/GridLayoutSection.test.tsx
  • src/components/__tests__/GridView.tier.nonEntity.test.tsx
  • src/components/__tests__/GridView.tier.test.tsx
  • src/components/__tests__/LightCard.test.tsx
  • src/components/__tests__/cardLoadingTier.test.tsx
  • src/components/cardRegistry.ts
  • src/components/ui/ErrorDisplay.tsx
  • src/components/ui/SkeletonCard.tsx
  • src/components/ui/cardStates.test.tsx
  • src/components/widgets/QuickControlsWidget.tsx
  • src/theme/customCss.stories.tsx
  • src/utils/__tests__/cardTier.test.ts
  • src/utils/cardTier.ts
💤 Files with no reviewable changes (8)
  • src/components/InputNumberCard.test.tsx
  • src/components/InputSelectCard.test.tsx
  • src/components/InputBooleanCard.test.tsx
  • src/components/CoverCard.test.tsx
  • src/components/InputTextCard.test.tsx
  • src/components/TextCard.stories.tsx
  • src/components/Separator.tsx
  • src/components/InputDateTimeCard.test.tsx

Comment thread docs/changes/0011-layout-tiers.md Outdated
Comment thread src/components/GridCard.css Outdated
Comment thread src/components/InputDateTimeCard.tsx Outdated
Comment thread src/components/ui/SkeletonCard.tsx

Copilot AI left a comment

Copy link
Copy Markdown

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 70 out of 70 changed files in this pull request and generated 3 comments.

Comment thread src/components/LightCard.tsx
Comment thread src/components/BinarySensorCard.tsx
Comment thread docs/changes/0011-layout-tiers.md Outdated
fx added 2 commits July 27, 2026 04:32
Seven review findings on the tier-derivation PR.

The skeleton violated the contract this change establishes: it rendered a
plain Radix Card with no `liebe-card` class and no `data-tier`, while the
selector contract now guarantees both on every rendered card. It stamps
them itself rather than rendering through `GridCard` — the shell requires a
truthful `domain` and carries the gesture controller, the more-info dialog
and the edit-mode buttons, none of which a placeholder for an absent card
may have. Its inline `minHeight` goes with it: the floor is now the sheet's,
so it participates in the layered overrides an inline declaration outranked.

Those floors were raw literals, unreachable through the token contract.
They are now `--liebe-card-min-height-row` / `-tall`, catalogued in
`src/theme/tokens.ts` and in the geometry table.

The memo comparators of the three cards that accept a span ignored it.
`span` rides beside `tier` precisely because the tier is lossy — a
breakpoint change can move `row` 3x1 to `row` 4x1 — so a tier-only
comparator holds the card at its last render and its `CardConfig.Modal`
preview opens at the span it was memoized at. `isSameSpan` compares by
value, since the grid builds a fresh object every render.

The input helpers' controls resolved to Radix `2` after this change
replaced the small-card `size="small"` (Radix `1`); the ones this change
touched go to `size="3"`. 44px stays out of reach until #204.

Finally, the glance error tile's rules — accessible name, tile-as-button,
modal actions, no `title` — are behaviour, so they move from the change
doc into the design-system spec that owns them.
…tion

One conflict, and only in the design-system spec's changelog table: 0015
(#221) appended its "sparkline data source" row to main while this branch
appended two 0011 rows. Kept every row — main's in place, this branch's as
the tail, which is what a rebase would have produced. All three carry
2026-07-27, so the date alone does not order them.

No source file conflicted.
@fx
fx merged commit 3dec08b into main Jul 27, 2026
5 checks passed
@fx
fx deleted the feat/0011-tier-derivation branch July 27, 2026 05:00
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants