feat(ui): live card-color picker, monotonic uptime ticker tweaks, default preset uses base palette
Three adjacent UI fixes that surfaced while soaking the Lumenworks redesign: - ``card-colors.ts`` now writes the user's picked color through to *every* card representing the same entity (e.g. the targets-tab card AND its dashboard mirror), not just the one that owns the picker. Sets the ``--ch`` custom property on each match instead of a literal ``border-left``, which avoided the double-stripe (custom border + Lumenworks ``::before`` channel stripe) the old approach produced and reaches mirrors the picker callback's ``.closest()`` lookup couldn't. - ``appearance.ts`` "default" preset now *clears* its colour overrides instead of stamping the historic muted greys (#1a1a1a / #2d2d2d / #f5f5f5 / #ffffff). With the redesign's pure-black / pure-white base palette in ``base.css``, "default" should mean "use the base" — the preset swatch in Appearance now matches what ships out of the box. Existing users with "default" selected will see a one-time visual shift to the new neutrals on next reload; this is intentional. - ``dashboard.css`` mod-metric label row gets explicit sizing for the small status glyphs (clock / check / warning) so they sit beside the mono-caps label without competing with the big value. Errors cell picks up the coral channel tint when the count is non-zero.
This commit is contained in:
@@ -347,6 +347,29 @@
|
|||||||
letter-spacing: 0.18em;
|
letter-spacing: 0.18em;
|
||||||
text-transform: uppercase;
|
text-transform: uppercase;
|
||||||
color: var(--lux-ink-mute, var(--text-secondary));
|
color: var(--lux-ink-mute, var(--text-secondary));
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Label-row icon sizing — the status glyph (clock/check/warning) sits
|
||||||
|
next to the mono caps label, not the big value. Tiny + muted so it
|
||||||
|
reads as an accent, not a competing element. */
|
||||||
|
.mod-metric .k > .icon,
|
||||||
|
.mod-metric .k > svg {
|
||||||
|
width: 10px;
|
||||||
|
height: 10px;
|
||||||
|
flex-shrink: 0;
|
||||||
|
opacity: 0.85;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Errors cell: the label icon shifts to coral when there are errors
|
||||||
|
(matching the big-value tint). */
|
||||||
|
.mod-metric.has-errors .k,
|
||||||
|
.mod-metric.has-errors .k > .icon,
|
||||||
|
.mod-metric.has-errors .k > svg {
|
||||||
|
color: var(--ch-coral, var(--danger-color));
|
||||||
|
opacity: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
.mod-metric .v {
|
.mod-metric .v {
|
||||||
@@ -366,6 +389,12 @@
|
|||||||
color: var(--ch);
|
color: var(--ch);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Errors cell — coral tint when > 0 so non-zero counts draw the eye
|
||||||
|
without needing an inline glyph that'd eat horizontal space. */
|
||||||
|
.mod-metric .v.has-errors {
|
||||||
|
color: var(--ch-coral, var(--danger-color));
|
||||||
|
}
|
||||||
|
|
||||||
.mod-metric .v small {
|
.mod-metric .v small {
|
||||||
font-family: var(--font-mono, monospace);
|
font-family: var(--font-mono, monospace);
|
||||||
font-size: 0.65rem;
|
font-size: 0.65rem;
|
||||||
|
|||||||
@@ -25,6 +25,18 @@ import { ICON_TRASH } from './icons.ts';
|
|||||||
const STORAGE_KEY = 'cardColors';
|
const STORAGE_KEY = 'cardColors';
|
||||||
const DEFAULT_SWATCH = '#808080';
|
const DEFAULT_SWATCH = '#808080';
|
||||||
|
|
||||||
|
/** Data attributes used as the entity-id key on card elements across the
|
||||||
|
* app. setCardColor() walks all of these so a single picker click updates
|
||||||
|
* every card representing the same entity (e.g. the targets-tab card AND
|
||||||
|
* its dashboard mirror), not just the one that owns the picker. */
|
||||||
|
const CARD_ID_ATTRS: readonly string[] = [
|
||||||
|
'data-target-id', 'data-device-id', 'data-automation-id',
|
||||||
|
'data-sync-clock-id', 'data-stream-id', 'data-template-id',
|
||||||
|
'data-pattern-template-id', 'data-pp-template-id', 'data-cspt-id',
|
||||||
|
'data-audio-template-id', 'data-audio-source-id', 'data-gi-id',
|
||||||
|
'data-scene-id', 'data-id',
|
||||||
|
];
|
||||||
|
|
||||||
function _getAll(): Record<string, string> {
|
function _getAll(): Record<string, string> {
|
||||||
try { return JSON.parse(localStorage.getItem(STORAGE_KEY) ?? '{}') || {}; }
|
try { return JSON.parse(localStorage.getItem(STORAGE_KEY) ?? '{}') || {}; }
|
||||||
catch { return {}; }
|
catch { return {}; }
|
||||||
@@ -38,15 +50,30 @@ export function setCardColor(id: string, hex: string): void {
|
|||||||
const m = _getAll();
|
const m = _getAll();
|
||||||
if (hex) m[id] = hex; else delete m[id];
|
if (hex) m[id] = hex; else delete m[id];
|
||||||
localStorage.setItem(STORAGE_KEY, JSON.stringify(m));
|
localStorage.setItem(STORAGE_KEY, JSON.stringify(m));
|
||||||
|
|
||||||
|
// Live-update every card representing this entity. The card stripe is
|
||||||
|
// the ::before pseudo-element backed by --ch (see cards.css), so we
|
||||||
|
// override --ch inline rather than setting border-left — that avoids
|
||||||
|
// the double-stripe (custom border + primary --ch) the old approach
|
||||||
|
// produced, and reaches dashboard mirrors that the picker callback's
|
||||||
|
// .closest() lookup couldn't.
|
||||||
|
const escaped = CSS.escape(id);
|
||||||
|
const selector = CARD_ID_ATTRS.map(a => `[${a}="${escaped}"]`).join(',');
|
||||||
|
document.querySelectorAll(selector).forEach(el => {
|
||||||
|
const card = el as HTMLElement;
|
||||||
|
if (hex) card.style.setProperty('--ch', hex);
|
||||||
|
else card.style.removeProperty('--ch');
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns inline style string for card border-left.
|
* Returns the inline style fragment for a card's accent override.
|
||||||
* Empty string when no color is set.
|
* Sets the --ch CSS variable so the existing ::before channel stripe
|
||||||
|
* picks up the user's color. Empty string when no color is set.
|
||||||
*/
|
*/
|
||||||
export function cardColorStyle(entityId: string): string {
|
export function cardColorStyle(entityId: string): string {
|
||||||
const c = getCardColor(entityId);
|
const c = getCardColor(entityId);
|
||||||
return c ? `border-left: 3px solid ${c}` : '';
|
return c ? `--ch: ${c}` : '';
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -59,12 +86,9 @@ export function cardColorButton(entityId: string, cardAttr: string): string {
|
|||||||
const pickerId = `cc-${entityId}`;
|
const pickerId = `cc-${entityId}`;
|
||||||
|
|
||||||
registerColorPicker(pickerId, (hex) => {
|
registerColorPicker(pickerId, (hex) => {
|
||||||
|
// setCardColor handles the DOM update on every card representing
|
||||||
|
// this entity (including dashboard mirrors). Nothing else to do.
|
||||||
setCardColor(entityId, hex);
|
setCardColor(entityId, hex);
|
||||||
// Find the card that contains this picker (not a global querySelector
|
|
||||||
// which could match a dashboard compact card first)
|
|
||||||
const wrapper = document.getElementById(`cp-wrap-${pickerId}`);
|
|
||||||
const card = wrapper?.closest(`[${cardAttr}]`) as HTMLElement | null;
|
|
||||||
if (card) card.style.borderLeft = hex ? `3px solid ${hex}` : '';
|
|
||||||
});
|
});
|
||||||
|
|
||||||
return createColorPicker({ id: pickerId, currentColor: color, onPick: undefined, anchor: 'left', showReset: true, resetColor: DEFAULT_SWATCH });
|
return createColorPicker({ id: pickerId, currentColor: color, onPick: undefined, anchor: 'left', showReset: true, resetColor: DEFAULT_SWATCH });
|
||||||
|
|||||||
@@ -65,13 +65,17 @@ const STYLE_PRESETS: readonly StylePreset[] = [
|
|||||||
fontHeading: "'Orbitron', sans-serif",
|
fontHeading: "'Orbitron', sans-serif",
|
||||||
accent: '#4CAF50',
|
accent: '#4CAF50',
|
||||||
fontUrl: '',
|
fontUrl: '',
|
||||||
|
// Color values mirror base.css so the preview swatch in Appearance
|
||||||
|
// matches what _applyThemeVars produces (which clears overrides for
|
||||||
|
// 'default' and lets base.css through — pure black on dark, pure
|
||||||
|
// white on light).
|
||||||
dark: {
|
dark: {
|
||||||
bgColor: '#1a1a1a', bgSecondary: '#242424', cardBg: '#2d2d2d',
|
bgColor: '#000000', bgSecondary: '#0a0b0d', cardBg: '#101216',
|
||||||
textColor: '#e0e0e0', textSecondary: '#999', textMuted: '#777',
|
textColor: '#e0e0e0', textSecondary: '#999', textMuted: '#777',
|
||||||
borderColor: '#404040', inputBg: '#1a1a2e',
|
borderColor: '#404040', inputBg: '#1a1a2e',
|
||||||
},
|
},
|
||||||
light: {
|
light: {
|
||||||
bgColor: '#f5f5f5', bgSecondary: '#eee', cardBg: '#ffffff',
|
bgColor: '#ffffff', bgSecondary: '#fafbfc', cardBg: '#f5f6f8',
|
||||||
textColor: '#333333', textSecondary: '#595959', textMuted: '#767676',
|
textColor: '#333333', textSecondary: '#595959', textMuted: '#767676',
|
||||||
borderColor: '#e0e0e0', inputBg: '#f0f0f0',
|
borderColor: '#e0e0e0', inputBg: '#f0f0f0',
|
||||||
},
|
},
|
||||||
@@ -500,9 +504,27 @@ export function getActiveBgEffect(): string {
|
|||||||
|
|
||||||
/** Apply theme color CSS variables for the current active theme (dark/light). */
|
/** Apply theme color CSS variables for the current active theme (dark/light). */
|
||||||
function _applyThemeVars(preset: StylePreset): void {
|
function _applyThemeVars(preset: StylePreset): void {
|
||||||
|
const root = document.documentElement.style;
|
||||||
|
|
||||||
|
if (preset.id === 'default') {
|
||||||
|
// Default preset = base.css palette (pure-black on dark, pure-white
|
||||||
|
// on light). Clear any inline overrides left behind by a previous
|
||||||
|
// preset so the base values come through, instead of stamping the
|
||||||
|
// muted greys this preset historically carried.
|
||||||
|
root.removeProperty('--bg-color');
|
||||||
|
root.removeProperty('--bg-secondary');
|
||||||
|
root.removeProperty('--card-bg');
|
||||||
|
root.removeProperty('--text-color');
|
||||||
|
root.removeProperty('--text-primary');
|
||||||
|
root.removeProperty('--text-secondary');
|
||||||
|
root.removeProperty('--text-muted');
|
||||||
|
root.removeProperty('--border-color');
|
||||||
|
root.removeProperty('--input-bg');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const theme = document.documentElement.getAttribute('data-theme') || 'dark';
|
const theme = document.documentElement.getAttribute('data-theme') || 'dark';
|
||||||
const vars = theme === 'dark' ? preset.dark : preset.light;
|
const vars = theme === 'dark' ? preset.dark : preset.light;
|
||||||
const root = document.documentElement.style;
|
|
||||||
|
|
||||||
root.setProperty('--bg-color', vars.bgColor);
|
root.setProperty('--bg-color', vars.bgColor);
|
||||||
root.setProperty('--bg-secondary', vars.bgSecondary);
|
root.setProperty('--bg-secondary', vars.bgSecondary);
|
||||||
|
|||||||
Reference in New Issue
Block a user