Frontend improvements: CSS foundations, accessibility, UX enhancements

CSS: Add design token variables (spacing, timing, weights, z-index layers),
migrate all hardcoded z-index to named vars, fix light theme contrast for
WCAG AA, add skeleton loading cards, mask-composite fallback, card padding.

Accessibility: aria-live on toast, aria-label on health dots, sr-only class,
graph container keyboard focusable, MQTT password wrapped in form element.

UX: Modal auto-focus on open, inline field validation with blur, undo toast
with countdown, bulk action progress indicator, API error toast on failure.

i18n: Add common.undo, validation.required, bulk.processing, api.error.*
keys in EN/RU/ZH.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-03-20 01:51:22 +03:00
parent 43fbc1eff5
commit 47c696bae3
21 changed files with 397 additions and 38 deletions
+39 -3
View File
@@ -12,11 +12,47 @@
--warning-color: #ff9800;
--info-color: #2196F3;
--font-mono: 'Cascadia Code', 'Fira Code', 'JetBrains Mono', 'SF Mono', 'Consolas', 'Liberation Mono', monospace;
/* Spacing scale */
--space-xs: 4px;
--space-sm: 8px;
--space-md: 12px;
--space-lg: 20px;
--space-xl: 40px;
/* Border radius */
--radius: 8px;
--radius-sm: 4px;
--radius-md: 8px;
--radius-lg: 12px;
--radius-pill: 100px;
/* Animation timing */
--duration-fast: 0.15s;
--duration-normal: 0.25s;
--duration-slow: 0.4s;
--ease-out: cubic-bezier(0.16, 1, 0.3, 1);
--ease-spring: cubic-bezier(0.34, 1.56, 0.64, 1);
/* Font weights */
--weight-normal: 400;
--weight-medium: 500;
--weight-semibold: 600;
--weight-bold: 700;
/* Z-index layers */
--z-card-elevated: 10;
--z-sticky: 100;
--z-dropdown: 200;
--z-bulk-toolbar: 1000;
--z-modal: 2000;
--z-log-overlay: 2100;
--z-confirm: 2500;
--z-command-palette: 3000;
--z-toast: 3000;
--z-overlay-spinner: 9999;
--z-lightbox: 10000;
--z-connection: 10000;
}
/* ── SVG icon base ── */
@@ -59,8 +95,8 @@
--card-bg: #ffffff;
--text-color: #333333;
--text-primary: #333333;
--text-secondary: #666;
--text-muted: #999;
--text-secondary: #595959;
--text-muted: #767676;
--border-color: #e0e0e0;
--display-badge-bg: rgba(255, 255, 255, 0.85);
--primary-text-color: #3d8b40;
@@ -186,7 +222,7 @@ body,
.dashboard-target,
.perf-chart-card,
header {
transition: background-color 0.3s ease, color 0.3s ease, border-color 0.3s ease;
transition: background-color var(--duration-normal) ease, color var(--duration-normal) ease, border-color var(--duration-normal) ease;
}
/* ── Respect reduced motion preference ── */
@@ -2,6 +2,59 @@ section {
margin-bottom: 40px;
}
/* ── Skeleton loading placeholders ── */
@keyframes skeletonPulse {
0%, 100% { opacity: 0.06; }
50% { opacity: 0.12; }
}
.skeleton-card {
background: var(--card-bg);
border: 1px solid var(--border-color);
border-radius: var(--radius-md);
padding: 16px 20px 20px;
display: flex;
flex-direction: column;
gap: 12px;
min-height: 140px;
}
.skeleton-line {
height: 14px;
border-radius: 4px;
background: var(--text-color);
animation: skeletonPulse 1.5s ease-in-out infinite;
}
.skeleton-line-title {
width: 60%;
height: 18px;
}
.skeleton-line-short {
width: 40%;
}
.skeleton-line-medium {
width: 75%;
}
.skeleton-actions {
display: flex;
gap: 8px;
margin-top: auto;
padding-top: 12px;
border-top: 1px solid var(--border-color);
}
.skeleton-btn {
height: 32px;
flex: 1;
border-radius: var(--radius-sm);
background: var(--text-color);
animation: skeletonPulse 1.5s ease-in-out infinite;
}
.displays-grid,
.devices-grid {
display: grid;
@@ -54,7 +107,7 @@ section {
background: var(--card-bg);
border: 1px solid var(--border-color);
border-radius: var(--radius-md);
padding: 12px 20px 20px;
padding: 16px 20px 20px;
position: relative;
transition: transform 0.2s ease, box-shadow 0.2s ease, border-color 0.2s ease;
display: flex;
@@ -152,6 +205,17 @@ section {
animation: rotateBorder 4s linear infinite;
}
/* Fallback for browsers without mask-composite support (older Firefox) */
@supports not (mask-composite: exclude) {
.card-running::before {
-webkit-mask: none;
mask: none;
background: none;
border: 2px solid var(--primary-color);
opacity: 0.7;
}
}
@keyframes rotateBorder {
to { --border-angle: 360deg; }
}
@@ -1192,7 +1256,7 @@ ul.section-tip li {
display: flex;
align-items: center;
gap: 12px;
z-index: 1000;
z-index: var(--z-bulk-toolbar);
box-shadow: 0 -4px 24px rgba(0, 0, 0, 0.3);
transition: transform 0.25s ease;
white-space: nowrap;
@@ -193,6 +193,21 @@ select:focus {
box-shadow: 0 0 0 3px rgba(76, 175, 80, 0.15);
}
/* Inline validation states */
input.field-invalid,
select.field-invalid {
border-color: var(--danger-color);
box-shadow: 0 0 0 2px color-mix(in srgb, var(--danger-color) 15%, transparent);
}
.field-error-msg {
display: block;
color: var(--danger-color);
font-size: 0.78rem;
margin-top: 4px;
line-height: 1.3;
}
/* Remove browser autofill styling */
input:-webkit-autofill,
input:-webkit-autofill:hover,
@@ -260,7 +275,7 @@ input:-webkit-autofill:focus {
flex-direction: column;
align-items: center;
justify-content: center;
z-index: 9999;
z-index: var(--z-overlay-spinner);
backdrop-filter: blur(4px);
}
@@ -353,7 +368,7 @@ input:-webkit-autofill:focus {
font-size: 15px;
opacity: 0;
transition: opacity 0.4s cubic-bezier(0.16, 1, 0.3, 1), transform 0.4s cubic-bezier(0.16, 1, 0.3, 1);
z-index: 3000;
z-index: var(--z-toast);
box-shadow: 0 4px 20px var(--shadow-color);
min-width: 300px;
text-align: center;
@@ -384,6 +399,52 @@ input:-webkit-autofill:focus {
background: var(--info-color);
}
/* Toast with undo action */
.toast-with-action {
display: flex;
align-items: center;
gap: 12px;
}
.toast-message {
flex: 1;
}
.toast-undo-btn {
background: rgba(255, 255, 255, 0.25);
border: 1px solid rgba(255, 255, 255, 0.4);
color: white;
padding: 4px 12px;
border-radius: var(--radius-sm);
font-weight: var(--weight-semibold, 600);
font-size: 0.85rem;
cursor: pointer;
transition: background var(--duration-fast, 0.15s);
white-space: nowrap;
flex-shrink: 0;
}
.toast-undo-btn:hover {
background: rgba(255, 255, 255, 0.4);
}
.toast-timer {
width: 100%;
height: 3px;
position: absolute;
bottom: 0;
left: 0;
border-radius: 0 0 var(--radius-md) var(--radius-md);
background: rgba(255, 255, 255, 0.3);
transform-origin: left;
animation: toastTimer var(--toast-duration, 5s) linear forwards;
}
@keyframes toastTimer {
from { transform: scaleX(1); }
to { transform: scaleX(0); }
}
/* ── Card Tags ──────────────────────────────────────────── */
.card-tags {
@@ -604,7 +665,7 @@ textarea:focus-visible {
.icon-select-popup {
position: fixed;
z-index: 10000;
z-index: var(--z-lightbox);
overflow: hidden;
opacity: 0;
transition: opacity 0.15s ease;
@@ -683,7 +744,7 @@ textarea:focus-visible {
.type-picker-overlay {
position: fixed;
inset: 0;
z-index: 3000;
z-index: var(--z-command-palette);
display: flex;
justify-content: center;
padding-top: 15vh;
@@ -758,7 +819,7 @@ textarea:focus-visible {
display: none;
position: fixed;
inset: 0;
z-index: 10000;
z-index: var(--z-lightbox);
background: rgba(0, 0, 0, 0.5);
justify-content: center;
align-items: flex-start;
@@ -5,7 +5,7 @@ header {
padding: 8px 20px;
position: sticky;
top: 0;
z-index: 100;
z-index: var(--z-sticky);
background: var(--bg-color);
border-bottom: 2px solid var(--border-color);
}
@@ -133,7 +133,7 @@ h2 {
.connection-overlay {
position: fixed;
inset: 0;
z-index: 10000;
z-index: var(--z-connection);
display: flex;
align-items: center;
justify-content: center;
@@ -177,6 +177,19 @@ h2 {
animation: conn-spin 0.8s linear infinite;
}
/* Visually hidden — screen readers only */
.sr-only {
position: absolute;
width: 1px;
height: 1px;
padding: 0;
margin: -1px;
overflow: hidden;
clip: rect(0, 0, 0, 0);
white-space: nowrap;
border: 0;
}
/* WLED device health indicator */
.health-dot {
display: inline-block;
@@ -448,7 +461,7 @@ h2 {
#command-palette {
position: fixed;
inset: 0;
z-index: 3000;
z-index: var(--z-command-palette);
display: flex;
justify-content: center;
padding-top: 15vh;
@@ -154,7 +154,7 @@
bottom: 0;
left: 0;
right: 0;
z-index: 100;
z-index: var(--z-sticky);
background: var(--card-bg);
border-bottom: none;
border-top: 1px solid var(--border-color);
@@ -7,7 +7,7 @@
width: 100%;
height: 100%;
background: rgba(0, 0, 0, 0.8);
z-index: 2000;
z-index: var(--z-modal);
align-items: center;
justify-content: center;
animation: fadeIn 0.2s ease-out;
@@ -16,7 +16,7 @@
/* Confirm dialog must stack above all other modals */
#confirm-modal {
z-index: 2500;
z-index: var(--z-confirm);
}
/* Audio test spectrum canvas */
@@ -393,7 +393,7 @@
.log-overlay {
position: fixed;
inset: 0;
z-index: 2100;
z-index: var(--z-log-overlay);
display: flex;
flex-direction: column;
background: var(--bg-color, #111);
@@ -1007,7 +1007,7 @@
width: 100%;
height: 100%;
background: rgba(0, 0, 0, 0.92);
z-index: 10000;
z-index: var(--z-lightbox);
justify-content: center;
align-items: center;
cursor: zoom-out;