fix: UI polish — notification history buttons, CSS test error handling, schedule time picker, empty state labels
Lint & Test / test (push) Successful in 31s

- Notification history: replace text buttons with icon buttons, use modal-footer for proper positioning
- CSS test: reject 0-LED picture sources with clear error message, show WS close reason in UI
- Calibration: distribute LEDs by aspect ratio (16:9 default) instead of evenly across edges
- Value source schedule: replace native time input with custom HH:MM picker matching automation style
- Remove "No ... yet" empty state labels from all CardSection instances
This commit is contained in:
2026-03-22 20:58:13 +03:00
parent 52c8614a3c
commit f376622482
7 changed files with 201 additions and 39 deletions
@@ -668,14 +668,20 @@ def create_pixel_mapper(
return PixelMapper(calibration, interpolation_mode)
def create_default_calibration(led_count: int) -> CalibrationConfig:
def create_default_calibration(
led_count: int,
aspect_width: int = 16,
aspect_height: int = 9,
) -> CalibrationConfig:
"""Create a default calibration for a rectangular screen.
Assumes LEDs are evenly distributed around the screen edges in clockwise order
starting from bottom-left.
Distributes LEDs proportionally to the screen aspect ratio so that
horizontal and vertical edges have equal LED density.
Args:
led_count: Total number of LEDs
aspect_width: Screen width component of the aspect ratio (default 16)
aspect_height: Screen height component of the aspect ratio (default 9)
Returns:
Default calibration configuration
@@ -683,15 +689,48 @@ def create_default_calibration(led_count: int) -> CalibrationConfig:
if led_count < 4:
raise ValueError("Need at least 4 LEDs for default calibration")
# Distribute LEDs evenly across 4 edges
leds_per_edge = led_count // 4
remainder = led_count % 4
# Distribute LEDs proportionally to aspect ratio (same density per edge)
perimeter = 2 * (aspect_width + aspect_height)
h_frac = aspect_width / perimeter # fraction for each horizontal edge
v_frac = aspect_height / perimeter # fraction for each vertical edge
# Distribute remainder to longer edges (bottom and top)
bottom_count = leds_per_edge + (1 if remainder > 0 else 0)
right_count = leds_per_edge
top_count = leds_per_edge + (1 if remainder > 1 else 0)
left_count = leds_per_edge + (1 if remainder > 2 else 0)
# Float counts, then round so total == led_count
raw_h = led_count * h_frac
raw_v = led_count * v_frac
bottom_count = round(raw_h)
top_count = round(raw_h)
right_count = round(raw_v)
left_count = round(raw_v)
# Fix rounding error
diff = led_count - (bottom_count + top_count + right_count + left_count)
# Distribute remainder to horizontal edges first (longer edges)
if diff > 0:
bottom_count += 1
diff -= 1
if diff > 0:
top_count += 1
diff -= 1
if diff > 0:
right_count += 1
diff -= 1
if diff > 0:
left_count += 1
diff -= 1
# If we over-counted, remove from shorter edges first
if diff < 0:
left_count += diff # diff is negative
diff = 0
if left_count < 0:
diff = left_count
left_count = 0
right_count += diff
# Ensure each edge has at least 1 LED
bottom_count = max(1, bottom_count)
top_count = max(1, top_count)
right_count = max(1, right_count)
left_count = max(1, left_count)
config = CalibrationConfig(
layout="clockwise",
@@ -703,7 +742,8 @@ def create_default_calibration(led_count: int) -> CalibrationConfig:
)
logger.info(
f"Created default calibration for {led_count} LEDs: "
f"Created default calibration for {led_count} LEDs "
f"(aspect {aspect_width}:{aspect_height}): "
f"bottom={bottom_count}, right={right_count}, "
f"top={top_count}, left={left_count}"
)