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:
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user