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;
|
||||
text-transform: uppercase;
|
||||
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 {
|
||||
@@ -366,6 +389,12 @@
|
||||
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 {
|
||||
font-family: var(--font-mono, monospace);
|
||||
font-size: 0.65rem;
|
||||
|
||||
@@ -25,6 +25,18 @@ import { ICON_TRASH } from './icons.ts';
|
||||
const STORAGE_KEY = 'cardColors';
|
||||
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> {
|
||||
try { return JSON.parse(localStorage.getItem(STORAGE_KEY) ?? '{}') || {}; }
|
||||
catch { return {}; }
|
||||
@@ -38,15 +50,30 @@ export function setCardColor(id: string, hex: string): void {
|
||||
const m = _getAll();
|
||||
if (hex) m[id] = hex; else delete m[id];
|
||||
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.
|
||||
* Empty string when no color is set.
|
||||
* Returns the inline style fragment for a card's accent override.
|
||||
* 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 {
|
||||
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}`;
|
||||
|
||||
registerColorPicker(pickerId, (hex) => {
|
||||
// setCardColor handles the DOM update on every card representing
|
||||
// this entity (including dashboard mirrors). Nothing else to do.
|
||||
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 });
|
||||
|
||||
@@ -65,13 +65,17 @@ const STYLE_PRESETS: readonly StylePreset[] = [
|
||||
fontHeading: "'Orbitron', sans-serif",
|
||||
accent: '#4CAF50',
|
||||
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: {
|
||||
bgColor: '#1a1a1a', bgSecondary: '#242424', cardBg: '#2d2d2d',
|
||||
bgColor: '#000000', bgSecondary: '#0a0b0d', cardBg: '#101216',
|
||||
textColor: '#e0e0e0', textSecondary: '#999', textMuted: '#777',
|
||||
borderColor: '#404040', inputBg: '#1a1a2e',
|
||||
},
|
||||
light: {
|
||||
bgColor: '#f5f5f5', bgSecondary: '#eee', cardBg: '#ffffff',
|
||||
bgColor: '#ffffff', bgSecondary: '#fafbfc', cardBg: '#f5f6f8',
|
||||
textColor: '#333333', textSecondary: '#595959', textMuted: '#767676',
|
||||
borderColor: '#e0e0e0', inputBg: '#f0f0f0',
|
||||
},
|
||||
@@ -500,9 +504,27 @@ export function getActiveBgEffect(): string {
|
||||
|
||||
/** Apply theme color CSS variables for the current active theme (dark/light). */
|
||||
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 vars = theme === 'dark' ? preset.dark : preset.light;
|
||||
const root = document.documentElement.style;
|
||||
|
||||
root.setProperty('--bg-color', vars.bgColor);
|
||||
root.setProperty('--bg-secondary', vars.bgSecondary);
|
||||
|
||||
Reference in New Issue
Block a user