Timers: the gear chooses what a card shows, not which timers; a 24-hour switch; a date that fits
Which timers appear is the header's Timers ▾ menu's job, so the gear's source switches (ProPresenter / LTC / module timers, the Clock on/off) are gone. In their place the Show group offers the card's elements — Names, Status lines, Detail lines, Seconds, Leading zeros, Source headings; without seconds a countdown reads in whole minutes, rounded up. The clock's format select is a 24-hour switch (an older layout's "12h" choice is honoured); the clock card has no CLOCK label; Short day and Short month appear once Date is on, and the date fits its card — one line when it can, else a break after the day and a size that fits the longer half. Solo tiles get the same element switches. Shell: the settings window's grab bar is the accent colour and the gear that opened it wears the same colour while it is open; showWhen works in a tile's gear as it does on the admin page. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Fable 5.1
parent
1db6c2c89f
commit
efbcf960dd
@@ -197,6 +197,8 @@ for a clock's format switches — give it a schema of what applies.
|
|||||||
`showWhen: "<key>"` hides a field until the boolean/`switch` field named
|
`showWhen: "<key>"` hides a field until the boolean/`switch` field named
|
||||||
`<key>` is on — e.g. the audio device and channel appear only once the LTC
|
`<key>` is on — e.g. the audio device and channel appear only once the LTC
|
||||||
listener switch is enabled. The hidden field still keeps its stored value.
|
listener switch is enabled. The hidden field still keeps its stored value.
|
||||||
|
It works the same in a tile's gear (`instanceSchema`): "Short day" and
|
||||||
|
"Short month" appear once the clock's "Date" is on.
|
||||||
|
|
||||||
A `select` whose choices are only knowable at runtime (audio devices, serial
|
A `select` whose choices are only knowable at runtime (audio devices, serial
|
||||||
ports, discovered sources) sets `optionsRoute` instead of — or as a fallback
|
ports, discovered sources) sets `optionsRoute` instead of — or as a fallback
|
||||||
|
|||||||
@@ -41,8 +41,6 @@
|
|||||||
theme; left at their defaults they follow the theme's variables. */
|
theme; left at their defaults they follow the theme's variables. */
|
||||||
|
|
||||||
/** Which instance switch governs a source kind. */
|
/** Which instance switch governs a source kind. */
|
||||||
const SOURCE_SWITCH = { propresenter: 'showProPresenter', ltc: 'showLtc', clock: 'showClock', module: 'showModules' };
|
|
||||||
|
|
||||||
/** Colour settings → custom property, and the schema default (module.json) that means "follow the theme". */
|
/** Colour settings → custom property, and the schema default (module.json) that means "follow the theme". */
|
||||||
const COLOR_SETTINGS = [
|
const COLOR_SETTINGS = [
|
||||||
['runningColor', '--tm-ok', '#2ee59a'],
|
['runningColor', '--tm-ok', '#2ee59a'],
|
||||||
@@ -65,15 +63,17 @@ const LTC_KEY = 'ltc:ltc';
|
|||||||
switches default to on, the clock to 24-hour with seconds and no date —
|
switches default to on, the clock to 24-hour with seconds and no date —
|
||||||
the same values module.json gives the full "All timers" schema. */
|
the same values module.json gives the full "All timers" schema. */
|
||||||
const DEFAULTS = {
|
const DEFAULTS = {
|
||||||
showClock: true,
|
showNames: true,
|
||||||
showProPresenter: true,
|
|
||||||
showLtc: true,
|
|
||||||
showModules: true,
|
|
||||||
showHeadings: true,
|
showHeadings: true,
|
||||||
showStatus: true,
|
showStatus: true,
|
||||||
clockFormat: '24h',
|
showDetail: true,
|
||||||
|
showSeconds: true,
|
||||||
|
leadingZeros: false,
|
||||||
|
clock24h: true,
|
||||||
clockSeconds: true,
|
clockSeconds: true,
|
||||||
clockDate: false,
|
clockDate: false,
|
||||||
|
dateShortDay: false,
|
||||||
|
dateShortMonth: false,
|
||||||
};
|
};
|
||||||
|
|
||||||
/* ── SMPTE timecode math (module-scope, exported for tests) ─────────
|
/* ── SMPTE timecode math (module-scope, exported for tests) ─────────
|
||||||
@@ -140,14 +140,26 @@ export function valueAt(timer, sourceUpdatedAt, now) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/** Signed ms → "M:SS" / "H:MM:SS" (whole seconds, floor of the magnitude). */
|
/** Signed ms → "M:SS" / "H:MM:SS" (whole seconds, floor of the magnitude). */
|
||||||
export function fmtDuration(ms) {
|
/**
|
||||||
|
* A duration as digits. `seconds: false` shows minutes only — a countdown
|
||||||
|
* rounds up (3:59 left reads "4", 0:30 over reads "-1"), an elapsed timer
|
||||||
|
* rounds down; `zeros` pads the leading field to two digits ("05:07").
|
||||||
|
*/
|
||||||
|
export function fmtDuration(ms, { seconds = true, zeros = false, up = false } = {}) {
|
||||||
const sign = ms < 0 ? '-' : '';
|
const sign = ms < 0 ? '-' : '';
|
||||||
const s = Math.floor(Math.abs(ms) / 1000);
|
const total = Math.abs(ms) / 1000;
|
||||||
|
const pad = (v) => String(v).padStart(2, '0');
|
||||||
|
if (!seconds) {
|
||||||
|
const mins = up ? Math.ceil(total / 60) : Math.floor(total / 60);
|
||||||
|
const h = Math.floor(mins / 60);
|
||||||
|
const m = mins % 60;
|
||||||
|
return h ? `${sign}${zeros ? pad(h) : h}:${pad(m)}` : `${sign}${zeros ? pad(m) : m}`;
|
||||||
|
}
|
||||||
|
const s = Math.floor(total);
|
||||||
const h = Math.floor(s / 3600);
|
const h = Math.floor(s / 3600);
|
||||||
const m = Math.floor((s % 3600) / 60);
|
const m = Math.floor((s % 3600) / 60);
|
||||||
const sec = s % 60;
|
const sec = s % 60;
|
||||||
const pad = (v) => String(v).padStart(2, '0');
|
return h ? `${sign}${zeros ? pad(h) : h}:${pad(m)}:${pad(sec)}` : `${sign}${zeros ? pad(m) : m}:${pad(sec)}`;
|
||||||
return h ? `${sign}${h}:${pad(m)}:${pad(sec)}` : `${sign}${m}:${pad(sec)}`;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function warnWindow(targetMs) {
|
function warnWindow(targetMs) {
|
||||||
@@ -198,7 +210,10 @@ export default function create({ root, moduleApi }) {
|
|||||||
whatever its (possibly slimmer) schema would or wouldn't default. */
|
whatever its (possibly slimmer) schema would or wouldn't default. */
|
||||||
const pref = (key) => {
|
const pref = (key) => {
|
||||||
const v = settings()[key];
|
const v = settings()[key];
|
||||||
return v === undefined || v === null ? DEFAULTS[key] : v;
|
if (v !== undefined && v !== null) return v;
|
||||||
|
// A layout from before the 24-hour switch chose a "clockFormat".
|
||||||
|
if (key === 'clock24h' && settings().clockFormat === '12h') return false;
|
||||||
|
return DEFAULTS[key];
|
||||||
};
|
};
|
||||||
const keyOf = (source, timer) => `${source.id}:${timer.id}`;
|
const keyOf = (source, timer) => `${source.id}:${timer.id}`;
|
||||||
|
|
||||||
@@ -348,11 +363,6 @@ export default function create({ root, moduleApi }) {
|
|||||||
|
|
||||||
/* ── state → what this tile shows ───────────────────────────────── */
|
/* ── state → what this tile shows ───────────────────────────────── */
|
||||||
|
|
||||||
function sourceShown(source) {
|
|
||||||
const key = SOURCE_SWITCH[source.kind] || 'showModules';
|
|
||||||
return pref(key) !== false;
|
|
||||||
}
|
|
||||||
|
|
||||||
/** [{ id, source, items: [{ key, source, timer } | { key, missing }] }] */
|
/** [{ id, source, items: [{ key, source, timer } | { key, missing }] }] */
|
||||||
function visibleGroups() {
|
function visibleGroups() {
|
||||||
if (!state) return [];
|
if (!state) return [];
|
||||||
@@ -372,7 +382,6 @@ export default function create({ root, moduleApi }) {
|
|||||||
}
|
}
|
||||||
const groups = [];
|
const groups = [];
|
||||||
for (const source of state.sources) {
|
for (const source of state.sources) {
|
||||||
if (!sourceShown(source)) continue;
|
|
||||||
const items = [];
|
const items = [];
|
||||||
for (const timer of source.timers) {
|
for (const timer of source.timers) {
|
||||||
const key = keyOf(source, timer);
|
const key = keyOf(source, timer);
|
||||||
@@ -418,6 +427,7 @@ export default function create({ root, moduleApi }) {
|
|||||||
/** The parts of a card that only change with the snapshot (label, kind). */
|
/** The parts of a card that only change with the snapshot (label, kind). */
|
||||||
function updateCard(card, item) {
|
function updateCard(card, item) {
|
||||||
card.item = item;
|
card.item = item;
|
||||||
|
card.isClock = !item.missing && item.source?.kind === 'clock';
|
||||||
const isLtc = item.source?.kind === 'ltc';
|
const isLtc = item.source?.kind === 'ltc';
|
||||||
const badge = card.nameEl.querySelector('.tm-ltc-badge');
|
const badge = card.nameEl.querySelector('.tm-ltc-badge');
|
||||||
if (isLtc && !badge) {
|
if (isLtc && !badge) {
|
||||||
@@ -428,7 +438,7 @@ export default function create({ root, moduleApi }) {
|
|||||||
} else if (!isLtc && badge) {
|
} else if (!isLtc && badge) {
|
||||||
badge.remove();
|
badge.remove();
|
||||||
}
|
}
|
||||||
const label = item.missing ? (lastSoloLabel || 'Timer') : item.timer.label;
|
const label = item.missing ? (lastSoloLabel || 'Timer') : card.isClock ? '' : item.timer.label;
|
||||||
if (card.labelEl.textContent !== label) {
|
if (card.labelEl.textContent !== label) {
|
||||||
setText(card.labelEl, label);
|
setText(card.labelEl, label);
|
||||||
needFit = true; // the title's size follows its length
|
needFit = true; // the title's size follows its length
|
||||||
@@ -454,7 +464,7 @@ export default function create({ root, moduleApi }) {
|
|||||||
/* ── clocks ─────────────────────────────────────────────────────── */
|
/* ── clocks ─────────────────────────────────────────────────────── */
|
||||||
|
|
||||||
function clockOptions() {
|
function clockOptions() {
|
||||||
const h12 = pref('clockFormat') === '12h';
|
const h12 = pref('clock24h') === false;
|
||||||
return {
|
return {
|
||||||
hour12: h12,
|
hour12: h12,
|
||||||
hour: h12 ? 'numeric' : '2-digit',
|
hour: h12 ? 'numeric' : '2-digit',
|
||||||
@@ -467,8 +477,43 @@ export default function create({ root, moduleApi }) {
|
|||||||
return new Date().toLocaleTimeString([], clockOptions());
|
return new Date().toLocaleTimeString([], clockOptions());
|
||||||
}
|
}
|
||||||
|
|
||||||
function dateText() {
|
/** The date as the day and the rest — the break, when one is needed, falls between them. */
|
||||||
return new Date().toLocaleDateString([], { weekday: 'long', month: 'long', day: 'numeric' });
|
function dateParts() {
|
||||||
|
const d = new Date();
|
||||||
|
return {
|
||||||
|
day: d.toLocaleDateString([], { weekday: pref('dateShortDay') ? 'short' : 'long' }),
|
||||||
|
rest: d.toLocaleDateString([], { month: pref('dateShortMonth') ? 'short' : 'long', day: 'numeric' }),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The clock's date under its digits: one line when it fits the card,
|
||||||
|
* otherwise a line break after the day and a size that fits the longer
|
||||||
|
* half. Re-done only when the text or the card width changes.
|
||||||
|
*/
|
||||||
|
function renderDate(card, d) {
|
||||||
|
const cw = lastFit.cw;
|
||||||
|
const lineSize = lastFit.meta * LINE_SCALE;
|
||||||
|
const whole = `${d.day}, ${d.rest}`;
|
||||||
|
const key = `${whole}|${Math.round(cw)}|${lineSize}`;
|
||||||
|
if (card.dateKey === key) return;
|
||||||
|
card.dateKey = key;
|
||||||
|
let px = lineSize;
|
||||||
|
let wrapIt = false;
|
||||||
|
if (textWidth(whole, lineSize, 0.1) > cw) {
|
||||||
|
wrapIt = true;
|
||||||
|
const widest = Math.max(textWidth(`${d.day},`, lineSize, 0.1), textWidth(d.rest, lineSize, 0.1));
|
||||||
|
px = Math.max(8, Math.floor(lineSize * Math.min(1, cw / widest)));
|
||||||
|
}
|
||||||
|
const el = card.statusEl;
|
||||||
|
el.textContent = '';
|
||||||
|
const a = document.createElement('span');
|
||||||
|
a.textContent = `${d.day},`;
|
||||||
|
const b = document.createElement('span');
|
||||||
|
b.textContent = d.rest;
|
||||||
|
el.append(a, document.createTextNode(' '), b);
|
||||||
|
el.style.fontSize = px === lineSize ? '' : `${px}px`;
|
||||||
|
card.dateCls = wrapIt ? ' is-date is-wrap' : ' is-date';
|
||||||
}
|
}
|
||||||
|
|
||||||
/** A provider's kind:"clock" timer carries an ISO time in status.text as of its snapshot. */
|
/** A provider's kind:"clock" timer carries an ISO time in status.text as of its snapshot. */
|
||||||
@@ -559,6 +604,8 @@ export default function create({ root, moduleApi }) {
|
|||||||
if (!state) return;
|
if (!state) return;
|
||||||
const now = serverNow();
|
const now = serverNow();
|
||||||
const showStatus = pref('showStatus') !== false;
|
const showStatus = pref('showStatus') !== false;
|
||||||
|
const showDetail = pref('showDetail') !== false;
|
||||||
|
const digits = { seconds: pref('showSeconds') !== false, zeros: pref('leadingZeros') === true };
|
||||||
let charsChanged = false;
|
let charsChanged = false;
|
||||||
for (const card of cards.values()) {
|
for (const card of cards.values()) {
|
||||||
const { item } = card;
|
const { item } = card;
|
||||||
@@ -573,6 +620,7 @@ export default function create({ root, moduleApi }) {
|
|||||||
let statusTone = '';
|
let statusTone = '';
|
||||||
let detailText = '';
|
let detailText = '';
|
||||||
let extra = '';
|
let extra = '';
|
||||||
|
let date = null;
|
||||||
if (timer.kind === 'clock') {
|
if (timer.kind === 'clock') {
|
||||||
if (source.kind === 'ltc') {
|
if (source.kind === 'ltc') {
|
||||||
const l = timer.ltc || {};
|
const l = timer.ltc || {};
|
||||||
@@ -588,7 +636,8 @@ export default function create({ root, moduleApi }) {
|
|||||||
} else if (source.kind === 'clock') {
|
} else if (source.kind === 'clock') {
|
||||||
text = wallClockText();
|
text = wallClockText();
|
||||||
tone = 'plain';
|
tone = 'plain';
|
||||||
statusText = pref('clockDate') ? dateText() : '';
|
date = pref('clockDate') ? dateParts() : null;
|
||||||
|
statusText = date ? `${date.day}, ${date.rest}` : '';
|
||||||
statusTone = 'muted';
|
statusTone = 'muted';
|
||||||
} else {
|
} else {
|
||||||
text = providerClockText(timer, source, now);
|
text = providerClockText(timer, source, now);
|
||||||
@@ -598,7 +647,7 @@ export default function create({ root, moduleApi }) {
|
|||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
const value = valueAt(timer, source.updatedAt, now);
|
const value = valueAt(timer, source.updatedAt, now);
|
||||||
text = value === null ? '—' : fmtDuration(value);
|
text = value === null ? '—' : fmtDuration(value, { ...digits, up: timer.kind === 'countdown' });
|
||||||
tone = toneFor(timer, value);
|
tone = toneFor(timer, value);
|
||||||
statusText = timer.status?.text || defaultStatus(timer, value, tone);
|
statusText = timer.status?.text || defaultStatus(timer, value, tone);
|
||||||
statusTone = timer.status?.tone || tone;
|
statusTone = timer.status?.tone || tone;
|
||||||
@@ -615,12 +664,18 @@ export default function create({ root, moduleApi }) {
|
|||||||
}
|
}
|
||||||
// Notes that must survive the compact look: an unavailable LTC input.
|
// Notes that must survive the compact look: an unavailable LTC input.
|
||||||
const isNote = extra === ' is-unavailable';
|
const isNote = extra === ' is-unavailable';
|
||||||
setText(card.statusEl, statusText);
|
setText(card.statusEl, statusText); // a no-op over the date's two spans: same text
|
||||||
card.statusEl.hidden = !statusText || (!showStatus && !isNote);
|
card.statusEl.hidden = !statusText || (!showStatus && !isNote);
|
||||||
card.statusEl.className = 'tm-status' + (isNote ? ' is-note' : '');
|
if (date && !card.statusEl.hidden) renderDate(card, date);
|
||||||
|
else if (card.dateCls) {
|
||||||
|
card.dateCls = '';
|
||||||
|
card.dateKey = '';
|
||||||
|
card.statusEl.style.fontSize = '';
|
||||||
|
}
|
||||||
|
card.statusEl.className = 'tm-status' + (isNote ? ' is-note' : '') + (card.dateCls || '');
|
||||||
card.statusEl.dataset.tone = statusTone;
|
card.statusEl.dataset.tone = statusTone;
|
||||||
setText(card.detailEl, detailText);
|
setText(card.detailEl, detailText);
|
||||||
card.detailEl.hidden = !detailText || !showStatus;
|
card.detailEl.hidden = !detailText || !showDetail;
|
||||||
const kindCls = source.kind === 'ltc' ? ' tm-card-ltc' : source.kind === 'clock' ? ' tm-card-clock' : '';
|
const kindCls = source.kind === 'ltc' ? ' tm-card-ltc' : source.kind === 'clock' ? ' tm-card-clock' : '';
|
||||||
setCardClass(card, `tm-card is-${tone}${extra}${kindCls}`);
|
setCardClass(card, `tm-card is-${tone}${extra}${kindCls}`);
|
||||||
}
|
}
|
||||||
@@ -694,9 +749,17 @@ export default function create({ root, moduleApi }) {
|
|||||||
needFit = true;
|
needFit = true;
|
||||||
}
|
}
|
||||||
const showHeadings = !solo && pref('showHeadings') !== false;
|
const showHeadings = !solo && pref('showHeadings') !== false;
|
||||||
const showStatus = pref('showStatus') !== false;
|
|
||||||
wrap.classList.toggle('tm-headings', showHeadings);
|
wrap.classList.toggle('tm-headings', showHeadings);
|
||||||
wrap.classList.toggle('tm-nostatus', !showStatus);
|
for (const [cls, on] of [
|
||||||
|
['tm-nostatus', pref('showStatus') === false],
|
||||||
|
['tm-nodetail', pref('showDetail') === false],
|
||||||
|
['tm-nonames', pref('showNames') === false],
|
||||||
|
]) {
|
||||||
|
if (wrap.classList.contains(cls) !== on) {
|
||||||
|
wrap.classList.toggle(cls, on);
|
||||||
|
needFit = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
let hint = '';
|
let hint = '';
|
||||||
const groups = visibleGroups();
|
const groups = visibleGroups();
|
||||||
@@ -779,20 +842,23 @@ export default function create({ root, moduleApi }) {
|
|||||||
/** Status and detail lines are drawn at this share of the label's size (style.css agrees). */
|
/** Status and detail lines are drawn at this share of the label's size (style.css agrees). */
|
||||||
const LINE_SCALE = 0.8;
|
const LINE_SCALE = 0.8;
|
||||||
|
|
||||||
/* The title fits its card: measured off-screen, it shrinks (down to a
|
/* Text measured off-screen the way the CSS draws it (bold, uppercase,
|
||||||
floor) before the card would cut it off. Uppercase and letter-spaced
|
letter-spaced), so a title or the date can shrink before the card
|
||||||
like the CSS draws it. */
|
would cut it off. */
|
||||||
const measurer = document.createElement('canvas').getContext('2d');
|
const measurer = document.createElement('canvas').getContext('2d');
|
||||||
let labelFamily = '';
|
let textFamily = '';
|
||||||
function labelWidth(text, px) {
|
function textWidth(text, px, spacingEm) {
|
||||||
if (!labelFamily) labelFamily = getComputedStyle(root).fontFamily || 'sans-serif';
|
if (!textFamily) textFamily = getComputedStyle(root).fontFamily || 'sans-serif';
|
||||||
measurer.font = `700 ${px}px ${labelFamily}`;
|
measurer.font = `700 ${px}px ${textFamily}`;
|
||||||
const t = String(text || '').toUpperCase();
|
const t = String(text || '').toUpperCase();
|
||||||
return measurer.measureText(t).width + 0.08 * px * Math.max(0, t.length - 1);
|
return measurer.measureText(t).width + spacingEm * px * Math.max(0, t.length - 1);
|
||||||
}
|
}
|
||||||
|
/** The last fit's card width and label size — what the date is fitted against. */
|
||||||
|
let lastFit = { cw: 200, meta: 12 };
|
||||||
function fitLabel(card, meta, avail) {
|
function fitLabel(card, meta, avail) {
|
||||||
|
if (card.isClock) return; // the clock has no label line
|
||||||
const room = Math.max(24, avail - (card.nameEl.querySelector('.tm-ltc-badge') ? 34 : 0));
|
const room = Math.max(24, avail - (card.nameEl.querySelector('.tm-ltc-badge') ? 34 : 0));
|
||||||
const natural = labelWidth(card.labelEl.textContent, meta);
|
const natural = textWidth(card.labelEl.textContent, meta, 0.08);
|
||||||
const px = natural > room ? Math.max(8, Math.floor((meta * room) / natural)) : meta;
|
const px = natural > room ? Math.max(8, Math.floor((meta * room) / natural)) : meta;
|
||||||
card.labelEl.style.fontSize = px === meta ? '' : `${px}px`;
|
card.labelEl.style.fontSize = px === meta ? '' : `${px}px`;
|
||||||
}
|
}
|
||||||
@@ -810,15 +876,25 @@ export default function create({ root, moduleApi }) {
|
|||||||
const groups = [...groupEls.values()].filter((g) => g.el.isConnected);
|
const groups = [...groupEls.values()].filter((g) => g.el.isConnected);
|
||||||
const headings = wrap.classList.contains('tm-headings') ? groups.length : 0;
|
const headings = wrap.classList.contains('tm-headings') ? groups.length : 0;
|
||||||
const showStatus = !wrap.classList.contains('tm-nostatus');
|
const showStatus = !wrap.classList.contains('tm-nostatus');
|
||||||
// Lines under the digits besides the label: status, and detail if any card has one.
|
const showDetail = !wrap.classList.contains('tm-nodetail');
|
||||||
const metaLines = showStatus ? 1 + (list.some((c) => !c.detailEl.hidden) ? 1 : 0) : 0;
|
const namesOn = !wrap.classList.contains('tm-nonames');
|
||||||
|
// Lines under the digits: status, and detail if any card has one.
|
||||||
|
const metaLines = (showStatus ? 1 : 0) + (showDetail && list.some((c) => !c.detailEl.hidden) ? 1 : 0);
|
||||||
|
const date = showStatus && pref('clockDate') && list.some((c) => c.isClock) ? dateParts() : null;
|
||||||
|
const dateWhole = date ? `${date.day}, ${date.rest}` : '';
|
||||||
|
|
||||||
// For each column count, budget a card's full stack (label, digits,
|
// For each column count, budget a card's full stack (label, digits,
|
||||||
// lines) and score by the digit size it affords — the SMALLEST card's,
|
// lines) and score by the digit size it affords — the SMALLEST card's,
|
||||||
// so no timer ends up unreadable. Cards too short for the full stack
|
// so no timer ends up unreadable. Cards too short for the full stack
|
||||||
// drop to a compact look (label + digits, the colour carries the
|
// drop to a compact look (label + digits, the colour carries the
|
||||||
// state), but any layout with a readable full stack beats every
|
// state), but any layout with a readable full stack beats every
|
||||||
// compact one.
|
// compact one. The clock has no label line, and its date may take two.
|
||||||
|
const budgetFor = (c, ch, meta, mode, dateWrap) => {
|
||||||
|
const nameLine = namesOn && !c.isClock ? 1.3 * meta : 0;
|
||||||
|
if (mode === 'compact') return ch - nameLine;
|
||||||
|
const lines = metaLines + (c.isClock && dateWrap ? 1 : 0);
|
||||||
|
return ch - nameLine - lines * 1.3 * meta * LINE_SCALE;
|
||||||
|
};
|
||||||
let best = null;
|
let best = null;
|
||||||
for (let cols = 1; cols <= n; cols += 1) {
|
for (let cols = 1; cols <= n; cols += 1) {
|
||||||
const rows = headings
|
const rows = headings
|
||||||
@@ -828,19 +904,18 @@ export default function create({ root, moduleApi }) {
|
|||||||
const ch = (H - PAD * 2 - headings * HEAD_H - GAP * (rows - 1) - (headings ? (headings - 1) * GAP : 0)) / rows - CARD_PAD;
|
const ch = (H - PAD * 2 - headings * HEAD_H - GAP * (rows - 1) - (headings ? (headings - 1) * GAP : 0)) / rows - CARD_PAD;
|
||||||
if (cw <= 0 || ch <= 0) continue;
|
if (cw <= 0 || ch <= 0) continue;
|
||||||
const meta = Math.max(9, Math.min(22, Math.floor(ch * 0.16)));
|
const meta = Math.max(9, Math.min(22, Math.floor(ch * 0.16)));
|
||||||
const fullBudget = ch - 1.3 * meta - metaLines * 1.3 * meta * LINE_SCALE;
|
const dateWrap = Boolean(dateWhole) && textWidth(dateWhole, meta * LINE_SCALE, 0.1) > cw;
|
||||||
const compactBudget = ch - 1.3 * meta;
|
|
||||||
let full = Infinity;
|
let full = Infinity;
|
||||||
let compact = Infinity;
|
let compact = Infinity;
|
||||||
for (const c of list) {
|
for (const c of list) {
|
||||||
const byWidth = (cw * 1.55) / c.chars;
|
const byWidth = (cw * 1.55) / c.chars;
|
||||||
full = Math.min(full, byWidth, fullBudget);
|
full = Math.min(full, byWidth, budgetFor(c, ch, meta, 'full', dateWrap));
|
||||||
compact = Math.min(compact, byWidth, compactBudget);
|
compact = Math.min(compact, byWidth, budgetFor(c, ch, meta, 'compact', dateWrap));
|
||||||
}
|
}
|
||||||
const mode = full >= 14 ? 'full' : 'compact';
|
const mode = full >= 14 ? 'full' : 'compact';
|
||||||
const size = mode === 'full' ? full : compact;
|
const size = mode === 'full' ? full : compact;
|
||||||
const score = (mode === 'full' ? 1000 : 0) + size;
|
const score = (mode === 'full' ? 1000 : 0) + size;
|
||||||
if (!best || score > best.score) best = { cols, meta, mode, cw, budget: mode === 'full' ? fullBudget : compactBudget, score };
|
if (!best || score > best.score) best = { cols, meta, mode, cw, ch, dateWrap, score };
|
||||||
}
|
}
|
||||||
if (!best) return;
|
if (!best) return;
|
||||||
for (const g of groups) {
|
for (const g of groups) {
|
||||||
@@ -853,10 +928,13 @@ export default function create({ root, moduleApi }) {
|
|||||||
wrap.style.setProperty('--tm-gap', GAP + 'px');
|
wrap.style.setProperty('--tm-gap', GAP + 'px');
|
||||||
wrap.style.setProperty('--tm-meta-size', best.meta + 'px');
|
wrap.style.setProperty('--tm-meta-size', best.meta + 'px');
|
||||||
wrap.classList.toggle('tm-compact', best.mode === 'compact');
|
wrap.classList.toggle('tm-compact', best.mode === 'compact');
|
||||||
|
lastFit = { cw: best.cw, meta: best.meta };
|
||||||
for (const c of list) {
|
for (const c of list) {
|
||||||
const px = Math.max(11, Math.floor(Math.min((best.cw * 1.55) / c.chars, best.budget)));
|
const budget = budgetFor(c, best.ch, best.meta, best.mode, best.dateWrap);
|
||||||
|
const px = Math.max(11, Math.floor(Math.min((best.cw * 1.55) / c.chars, budget)));
|
||||||
c.timeEl.style.fontSize = px + 'px';
|
c.timeEl.style.fontSize = px + 'px';
|
||||||
fitLabel(c, best.meta, best.cw);
|
fitLabel(c, best.meta, best.cw);
|
||||||
|
c.dateKey = ''; // the date re-fits to the new card width on the next paint
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -4,16 +4,30 @@
|
|||||||
"version": "1.3.0",
|
"version": "1.3.0",
|
||||||
"proddash": ">=1.4.0",
|
"proddash": ">=1.4.0",
|
||||||
"description": "Timers, Clocks, and LTC From configured Modules",
|
"description": "Timers, Clocks, and LTC From configured Modules",
|
||||||
"provides": ["timers"],
|
"provides": [
|
||||||
|
"timers"
|
||||||
|
],
|
||||||
"permissions": [
|
"permissions": [
|
||||||
{ "kind": "microphone", "reason": "Decoding SMPTE LTC timecode from an audio input on this machine" },
|
{
|
||||||
{ "kind": "local-network", "reason": "Reaching ProPresenter on this network" }
|
"kind": "microphone",
|
||||||
|
"reason": "Decoding SMPTE LTC timecode from an audio input on this machine"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"kind": "local-network",
|
||||||
|
"reason": "Reaching ProPresenter on this network"
|
||||||
|
}
|
||||||
],
|
],
|
||||||
"client": "client.js",
|
"client": "client.js",
|
||||||
"server": "server.js",
|
"server": "server.js",
|
||||||
"style": "style.css",
|
"style": "style.css",
|
||||||
"minSize": { "w": 2, "h": 1 },
|
"minSize": {
|
||||||
"defaultSize": { "w": 4, "h": 3 },
|
"w": 2,
|
||||||
|
"h": 1
|
||||||
|
},
|
||||||
|
"defaultSize": {
|
||||||
|
"w": 4,
|
||||||
|
"h": 3
|
||||||
|
},
|
||||||
"configGroups": {
|
"configGroups": {
|
||||||
"ProPresenter": {
|
"ProPresenter": {
|
||||||
"toggle": "ppEnabled",
|
"toggle": "ppEnabled",
|
||||||
@@ -39,7 +53,10 @@
|
|||||||
"type": "endpoint",
|
"type": "endpoint",
|
||||||
"label": "ProPresenter",
|
"label": "ProPresenter",
|
||||||
"group": "ProPresenter",
|
"group": "ProPresenter",
|
||||||
"default": { "host": "", "port": 1025 }
|
"default": {
|
||||||
|
"host": "",
|
||||||
|
"port": 1025
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"password": {
|
"password": {
|
||||||
"type": "password",
|
"type": "password",
|
||||||
@@ -82,14 +99,26 @@
|
|||||||
"group": "Display",
|
"group": "Display",
|
||||||
"default": "default",
|
"default": "default",
|
||||||
"options": [
|
"options": [
|
||||||
{ "value": "default", "label": "Default (system)" },
|
{
|
||||||
{ "value": "monospace", "label": "Monospace — steadier changing digits" }
|
"value": "default",
|
||||||
|
"label": "Default (system)"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"value": "monospace",
|
||||||
|
"label": "Monospace — steadier changing digits"
|
||||||
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"instanceGroups": {
|
"instanceGroups": {
|
||||||
"Show": { "collapsed": false, "columns": 2 },
|
"Show": {
|
||||||
"Clock": { "collapsed": false, "toggle": "showClock" },
|
"collapsed": false,
|
||||||
|
"columns": 2
|
||||||
|
},
|
||||||
|
"Clock": {
|
||||||
|
"collapsed": false,
|
||||||
|
"columns": 2
|
||||||
|
},
|
||||||
"Colours": {
|
"Colours": {
|
||||||
"collapsed": true,
|
"collapsed": true,
|
||||||
"columns": 2,
|
"columns": 2,
|
||||||
@@ -97,27 +126,97 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"instanceSchema": {
|
"instanceSchema": {
|
||||||
"showProPresenter": { "type": "boolean", "label": "ProPresenter timers", "default": true, "group": "Show" },
|
"showNames": {
|
||||||
"showLtc": { "type": "boolean", "label": "LTC timecode", "default": true, "group": "Show" },
|
"type": "switch",
|
||||||
"showModules": { "type": "boolean", "label": "Module timers", "default": true, "group": "Show" },
|
"label": "Names",
|
||||||
"showHeadings": { "type": "boolean", "label": "Source headings", "default": true, "group": "Show" },
|
"default": true,
|
||||||
"showStatus": { "type": "boolean", "label": "Status lines", "default": true, "group": "Show" },
|
"group": "Show"
|
||||||
"showClock": { "type": "switch", "label": "Clock", "default": true, "group": "Clock" },
|
|
||||||
"clockFormat": {
|
|
||||||
"type": "select",
|
|
||||||
"label": "Clock format",
|
|
||||||
"default": "24h",
|
|
||||||
"group": "Clock",
|
|
||||||
"options": [
|
|
||||||
{ "value": "24h", "label": "24-hour" },
|
|
||||||
{ "value": "12h", "label": "12-hour" }
|
|
||||||
]
|
|
||||||
},
|
},
|
||||||
"clockSeconds": { "type": "boolean", "label": "Seconds", "default": true, "group": "Clock" },
|
"showStatus": {
|
||||||
"clockDate": { "type": "boolean", "label": "Date", "default": false, "group": "Clock" },
|
"type": "switch",
|
||||||
"runningColor": { "type": "color", "label": "Running", "default": "#2ee59a", "group": "Colours" },
|
"label": "Status lines",
|
||||||
"warningColor": { "type": "color", "label": "Warning", "default": "#f0b429", "group": "Colours" },
|
"default": true,
|
||||||
"overrunColor": { "type": "color", "label": "Overrun", "default": "#f4433c", "group": "Colours" },
|
"group": "Show"
|
||||||
"idleColor": { "type": "color", "label": "Idle", "default": "#8b98a5", "group": "Colours" }
|
},
|
||||||
|
"showDetail": {
|
||||||
|
"type": "switch",
|
||||||
|
"label": "Detail lines",
|
||||||
|
"default": true,
|
||||||
|
"group": "Show"
|
||||||
|
},
|
||||||
|
"showSeconds": {
|
||||||
|
"type": "switch",
|
||||||
|
"label": "Seconds",
|
||||||
|
"default": true,
|
||||||
|
"group": "Show"
|
||||||
|
},
|
||||||
|
"leadingZeros": {
|
||||||
|
"type": "switch",
|
||||||
|
"label": "Leading zeros",
|
||||||
|
"default": false,
|
||||||
|
"group": "Show"
|
||||||
|
},
|
||||||
|
"showHeadings": {
|
||||||
|
"type": "switch",
|
||||||
|
"label": "Source headings",
|
||||||
|
"default": true,
|
||||||
|
"group": "Show"
|
||||||
|
},
|
||||||
|
"clock24h": {
|
||||||
|
"type": "switch",
|
||||||
|
"label": "24-hour",
|
||||||
|
"default": true,
|
||||||
|
"group": "Clock"
|
||||||
|
},
|
||||||
|
"clockSeconds": {
|
||||||
|
"type": "switch",
|
||||||
|
"label": "Seconds",
|
||||||
|
"default": true,
|
||||||
|
"group": "Clock"
|
||||||
|
},
|
||||||
|
"clockDate": {
|
||||||
|
"type": "switch",
|
||||||
|
"label": "Date",
|
||||||
|
"default": false,
|
||||||
|
"group": "Clock"
|
||||||
|
},
|
||||||
|
"dateShortDay": {
|
||||||
|
"type": "switch",
|
||||||
|
"label": "Short day",
|
||||||
|
"default": false,
|
||||||
|
"group": "Clock",
|
||||||
|
"showWhen": "clockDate"
|
||||||
|
},
|
||||||
|
"dateShortMonth": {
|
||||||
|
"type": "switch",
|
||||||
|
"label": "Short month",
|
||||||
|
"default": false,
|
||||||
|
"group": "Clock",
|
||||||
|
"showWhen": "clockDate"
|
||||||
|
},
|
||||||
|
"runningColor": {
|
||||||
|
"type": "color",
|
||||||
|
"label": "Running",
|
||||||
|
"default": "#2ee59a",
|
||||||
|
"group": "Colours"
|
||||||
|
},
|
||||||
|
"warningColor": {
|
||||||
|
"type": "color",
|
||||||
|
"label": "Warning",
|
||||||
|
"default": "#f0b429",
|
||||||
|
"group": "Colours"
|
||||||
|
},
|
||||||
|
"overrunColor": {
|
||||||
|
"type": "color",
|
||||||
|
"label": "Overrun",
|
||||||
|
"default": "#f4433c",
|
||||||
|
"group": "Colours"
|
||||||
|
},
|
||||||
|
"idleColor": {
|
||||||
|
"type": "color",
|
||||||
|
"label": "Idle",
|
||||||
|
"default": "#8b98a5",
|
||||||
|
"group": "Colours"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -911,20 +911,23 @@ function gearOf(keys, { loose = [], groups = {} } = {}) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const COLOURS = ['runningColor', 'warningColor', 'overrunColor', 'idleColor'];
|
const COLOURS = ['runningColor', 'warningColor', 'overrunColor', 'idleColor'];
|
||||||
|
/** What a timer card can show or hide of itself (which timers appear is the header menu's job). */
|
||||||
|
const TIMER_ELEMENTS = ['showNames', 'showStatus', 'showDetail', 'showSeconds', 'leadingZeros'];
|
||||||
|
const CLOCK_ELEMENTS = ['clock24h', 'clockSeconds', 'clockDate', 'dateShortDay', 'dateShortMonth'];
|
||||||
/** With one card there is little else to see: the colours start unfolded. */
|
/** With one card there is little else to see: the colours start unfolded. */
|
||||||
const OPEN_COLOURS = { groups: { Colours: { collapsed: false } } };
|
const OPEN_COLOURS = { groups: { Colours: { collapsed: false } } };
|
||||||
|
|
||||||
const GEAR = {
|
const GEAR = {
|
||||||
/** Every source, the headings, the clock's format, the tones. */
|
/** Every card's elements, the headings, how the clock reads, the tones. */
|
||||||
all: { instanceSchema: FULL_SCHEMA, instanceGroups: FULL_GROUPS },
|
all: { instanceSchema: FULL_SCHEMA, instanceGroups: FULL_GROUPS },
|
||||||
/** One ProPresenter or module timer: its lines, and the tone its digits take per state. */
|
/** One ProPresenter or module timer: its elements, and the tone its digits take per state. */
|
||||||
timer: gearOf(['showStatus', ...COLOURS], { loose: ['showStatus'], ...OPEN_COLOURS }),
|
timer: gearOf([...TIMER_ELEMENTS, ...COLOURS], OPEN_COLOURS),
|
||||||
/** The timecode: running, no signal (warning) and stopped (idle) — it never overruns. */
|
/** The timecode: running, no signal (warning) and stopped (idle) — it never overruns, and frames are frames. */
|
||||||
ltc: gearOf(['showStatus', 'runningColor', 'warningColor', 'idleColor'], { loose: ['showStatus'], ...OPEN_COLOURS }),
|
ltc: gearOf(['showNames', 'showStatus', 'runningColor', 'warningColor', 'idleColor'], OPEN_COLOURS),
|
||||||
/** The time of day: plain digits, so only how they read. */
|
/** The time of day: plain digits, so only how they read. */
|
||||||
clock: gearOf(['clockFormat', 'clockSeconds', 'clockDate']),
|
clock: gearOf(CLOCK_ELEMENTS),
|
||||||
/** A clock another module offers: read like the clock, muted while it isn't running. */
|
/** A clock another module offers: read like the clock, muted while it isn't running. */
|
||||||
providerClock: gearOf(['showStatus', 'clockFormat', 'clockSeconds', 'idleColor'], { loose: ['showStatus'], ...OPEN_COLOURS }),
|
providerClock: gearOf(['showNames', 'showStatus', 'clock24h', 'clockSeconds', 'idleColor'], OPEN_COLOURS),
|
||||||
};
|
};
|
||||||
|
|
||||||
/** The gear a solo card of this timer gets — by what the card can show. */
|
/** The gear a solo card of this timer gets — by what the card can show. */
|
||||||
|
|||||||
@@ -132,7 +132,10 @@
|
|||||||
[data-module="propresenter-timers"] .tm-compact .tm-status:not(.is-note),
|
[data-module="propresenter-timers"] .tm-compact .tm-status:not(.is-note),
|
||||||
[data-module="propresenter-timers"] .tm-compact .tm-detail,
|
[data-module="propresenter-timers"] .tm-compact .tm-detail,
|
||||||
[data-module="propresenter-timers"] .tm-nostatus .tm-status:not(.is-note),
|
[data-module="propresenter-timers"] .tm-nostatus .tm-status:not(.is-note),
|
||||||
[data-module="propresenter-timers"] .tm-nostatus .tm-detail { display: none; }
|
[data-module="propresenter-timers"] .tm-nodetail .tm-detail { display: none; }
|
||||||
|
/* "Names" off hides every title; the clock never carries one — its digits say what it is */
|
||||||
|
[data-module="propresenter-timers"] .tm-nonames .tm-name,
|
||||||
|
[data-module="propresenter-timers"] .tm-card-clock .tm-name { display: none; }
|
||||||
|
|
||||||
/* ── one timer card ── */
|
/* ── one timer card ── */
|
||||||
|
|
||||||
@@ -221,6 +224,18 @@
|
|||||||
[data-module="propresenter-timers"] .tm-status[data-tone="danger"] { color: var(--tm-danger); }
|
[data-module="propresenter-timers"] .tm-status[data-tone="danger"] { color: var(--tm-danger); }
|
||||||
[data-module="propresenter-timers"] .tm-status[data-tone="muted"] { color: var(--tm-muted); }
|
[data-module="propresenter-timers"] .tm-status[data-tone="muted"] { color: var(--tm-muted); }
|
||||||
|
|
||||||
|
/* The clock's date: one line when it fits, else the day on its own line
|
||||||
|
(renderDate() sizes it to the longer half). */
|
||||||
|
[data-module="propresenter-timers"] .tm-status.is-date {
|
||||||
|
white-space: normal;
|
||||||
|
overflow: visible;
|
||||||
|
text-overflow: clip;
|
||||||
|
text-align: center;
|
||||||
|
line-height: 1.15;
|
||||||
|
}
|
||||||
|
[data-module="propresenter-timers"] .tm-status.is-date span { white-space: nowrap; }
|
||||||
|
[data-module="propresenter-timers"] .tm-status.is-date.is-wrap span { display: block; }
|
||||||
|
|
||||||
/* Explanatory notes read as sentences, not badges. */
|
/* Explanatory notes read as sentences, not badges. */
|
||||||
[data-module="propresenter-timers"] .tm-status.is-note {
|
[data-module="propresenter-timers"] .tm-status.is-note {
|
||||||
text-transform: none;
|
text-transform: none;
|
||||||
|
|||||||
+27
-3
@@ -286,7 +286,7 @@ function buildTile(tile) {
|
|||||||
gear.innerHTML = GEAR_SVG;
|
gear.innerHTML = GEAR_SVG;
|
||||||
gear.addEventListener('click', (e) => {
|
gear.addEventListener('click', (e) => {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
toggleSettingsPopover(tile, el);
|
toggleSettingsPopover(tile, el, gear);
|
||||||
});
|
});
|
||||||
head.appendChild(gear);
|
head.appendChild(gear);
|
||||||
}
|
}
|
||||||
@@ -1108,6 +1108,7 @@ function tileSchema(tile, man) {
|
|||||||
function closeSettingsPopovers() {
|
function closeSettingsPopovers() {
|
||||||
document.querySelectorAll('.tile-settings').forEach((p) => {
|
document.querySelectorAll('.tile-settings').forEach((p) => {
|
||||||
p._cleanup?.();
|
p._cleanup?.();
|
||||||
|
p._button?.classList.remove('is-open');
|
||||||
p.remove();
|
p.remove();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -1143,7 +1144,8 @@ function clampPopover(pop) {
|
|||||||
pop.style.top = Math.min(Math.max(pad, pop.offsetTop), maxTop) + 'px';
|
pop.style.top = Math.min(Math.max(pad, pop.offsetTop), maxTop) + 'px';
|
||||||
}
|
}
|
||||||
|
|
||||||
function toggleSettingsPopover(tile, el) {
|
/** Open (or close) the settings window for `tile`, beside its element `el`; `button` is the gear that asked. */
|
||||||
|
function toggleSettingsPopover(tile, el, button = null) {
|
||||||
const existing = document.querySelector(`.tile-settings[data-tile-id="${tile.id}"]`);
|
const existing = document.querySelector(`.tile-settings[data-tile-id="${tile.id}"]`);
|
||||||
closeSettingsPopovers();
|
closeSettingsPopovers();
|
||||||
if (existing) return;
|
if (existing) return;
|
||||||
@@ -1156,6 +1158,11 @@ function toggleSettingsPopover(tile, el) {
|
|||||||
pop.dataset.tileId = tile.id;
|
pop.dataset.tileId = tile.id;
|
||||||
pop.setAttribute('role', 'dialog');
|
pop.setAttribute('role', 'dialog');
|
||||||
pop.setAttribute('aria-label', `${tile.title || man.name} settings`);
|
pop.setAttribute('aria-label', `${tile.title || man.name} settings`);
|
||||||
|
// The gear that opened it wears the window's colour while it is open.
|
||||||
|
if (button) {
|
||||||
|
button.classList.add('is-open');
|
||||||
|
pop._button = button;
|
||||||
|
}
|
||||||
|
|
||||||
// A slim bar to move it by — nothing else. No title (the window sits beside
|
// A slim bar to move it by — nothing else. No title (the window sits beside
|
||||||
// the tile it belongs to) and no close button: a click anywhere else, or
|
// the tile it belongs to) and no close button: a click anywhere else, or
|
||||||
@@ -1168,6 +1175,7 @@ function toggleSettingsPopover(tile, el) {
|
|||||||
pop.append(bar, body);
|
pop.append(bar, body);
|
||||||
|
|
||||||
const inputs = new Map();
|
const inputs = new Map();
|
||||||
|
const shownWhen = []; // fields that appear only while another has a given value
|
||||||
|
|
||||||
/* Display choices apply as they change and save with the layout — nothing
|
/* Display choices apply as they change and save with the layout — nothing
|
||||||
to confirm, so no Apply. Typing is debounced because applying means
|
to confirm, so no Apply. Typing is debounced because applying means
|
||||||
@@ -1446,6 +1454,8 @@ function toggleSettingsPopover(tile, el) {
|
|||||||
field.appendChild(help);
|
field.appendChild(help);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (typeof spec?.showWhen === 'string' && spec.showWhen) shownWhen.push({ field, key: spec.showWhen });
|
||||||
|
|
||||||
if (current && switchInput && key === current.toggleKey) {
|
if (current && switchInput && key === current.toggleKey) {
|
||||||
// The group's on/off lives in its title row, never in the body, and a
|
// The group's on/off lives in its title row, never in the body, and a
|
||||||
// click on it must not fold a <details>.
|
// click on it must not fold a <details>.
|
||||||
@@ -1461,7 +1471,21 @@ function toggleSettingsPopover(tile, el) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Nothing to press: click away, press Escape, the ✕, or the gear again.
|
// "Short day" appears once "Date" is on: a field with showWhen:"K" follows
|
||||||
|
// K's switch, live — the same rule the admin page applies.
|
||||||
|
if (shownWhen.length) {
|
||||||
|
const refresh = () => {
|
||||||
|
for (const rule of shownWhen) {
|
||||||
|
const read = inputs.get(rule.key);
|
||||||
|
rule.field.hidden = read ? !read() : false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
refresh();
|
||||||
|
body.addEventListener('change', refresh);
|
||||||
|
body.addEventListener('input', refresh);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Nothing to press: click away, press Escape, or the gear again.
|
||||||
// (The gear's own click stops propagating, so it toggles rather than
|
// (The gear's own click stops propagating, so it toggles rather than
|
||||||
// closing and reopening.)
|
// closing and reopening.)
|
||||||
const closeOnClick = (e) => { if (!pop.contains(e.target)) closeSettingsPopovers(); };
|
const closeOnClick = (e) => { if (!pop.contains(e.target)) closeSettingsPopovers(); };
|
||||||
|
|||||||
+9
-6
@@ -306,6 +306,8 @@ body.menu-hidden #topbar {
|
|||||||
.tile-btn:hover { background: var(--border); color: var(--text); }
|
.tile-btn:hover { background: var(--border); color: var(--text); }
|
||||||
.tile-btn.close:hover { color: var(--danger); }
|
.tile-btn.close:hover { color: var(--danger); }
|
||||||
.tile-btn.active { color: var(--accent); }
|
.tile-btn.active { color: var(--accent); }
|
||||||
|
.tile-btn.is-open,
|
||||||
|
.tile-btn.is-open:hover { background: var(--accent); color: var(--accent-text); }
|
||||||
.tile-btn svg { width: 13px; height: 13px; display: block; }
|
.tile-btn svg { width: 13px; height: 13px; display: block; }
|
||||||
|
|
||||||
/* controls a module adds to its own title bar (moduleApi.header) */
|
/* controls a module adds to its own title bar (moduleApi.header) */
|
||||||
@@ -554,15 +556,15 @@ body.fullscreen #grid { padding-bottom: 0; }
|
|||||||
border-radius: 10px;
|
border-radius: 10px;
|
||||||
box-shadow: 0 12px 36px rgba(0, 0, 0, 0.55);
|
box-shadow: 0 12px 36px rgba(0, 0, 0, 0.55);
|
||||||
}
|
}
|
||||||
/* the grab bar: a slim strip with a grip mark, nothing to read or press */
|
/* the grab bar: a slim accent strip with a grip mark, nothing to read or
|
||||||
|
press — the gear that opened the window wears the same colour meanwhile */
|
||||||
.tile-settings-bar {
|
.tile-settings-bar {
|
||||||
flex: none;
|
flex: none;
|
||||||
height: 14px;
|
height: 14px;
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
background: rgba(255, 255, 255, 0.04);
|
background: var(--accent);
|
||||||
border-bottom: 1px solid var(--border);
|
|
||||||
cursor: grab;
|
cursor: grab;
|
||||||
user-select: none;
|
user-select: none;
|
||||||
-webkit-user-select: none;
|
-webkit-user-select: none;
|
||||||
@@ -573,11 +575,12 @@ body.fullscreen #grid { padding-bottom: 0; }
|
|||||||
width: 34px;
|
width: 34px;
|
||||||
height: 4px;
|
height: 4px;
|
||||||
border-radius: 2px;
|
border-radius: 2px;
|
||||||
background: var(--muted);
|
background: var(--accent-text);
|
||||||
opacity: 0.55;
|
opacity: 0.7;
|
||||||
transition: opacity 0.12s;
|
transition: opacity 0.12s;
|
||||||
}
|
}
|
||||||
.tile-settings-bar:hover::before { opacity: 0.9; }
|
.tile-settings-bar:hover::before { opacity: 1; }
|
||||||
|
.tile-settings .field[hidden] { display: none; }
|
||||||
.tile-settings-bar:active { cursor: grabbing; }
|
.tile-settings-bar:active { cursor: grabbing; }
|
||||||
.tile-settings-body {
|
.tile-settings-body {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
|
|||||||
Reference in New Issue
Block a user