tabs: extract scheduleField() helper — collapse 8 label/caption sites

Every field on the new- and edit-schedule forms (plus buildIntervalComposer
and buildTargetChips) builds the same wrapper shape:

    el('label', { class: 'schedule-field' },
      el('span', { class: 'schedule-field-label' }, '…'))

then appends an input. Eight identical-modulo-text sites. Extracting one
small helper:

    function scheduleField(labelText, ...children) {
      return el('label', { class: 'schedule-field' },
        el('span', { class: 'schedule-field-label' }, labelText),
        ...children);
    }

drops the per-site cost to a single `form_.append(scheduleField('caption',
input));` line. -10 LOC net; no behaviour change.
This commit is contained in:
iris 2026-05-28 12:36:01 +02:00 committed by Mara
commit bc1c910044

View file

@ -2004,6 +2004,18 @@ window.marked = marked;
} }
return n; return n;
} }
// Label + caption wrapper shared by every field on the schedule
// forms — `<label class="schedule-field"><span
// class="schedule-field-label">…</span>…children…</label>`. Variadic
// children land inside the label after the caption span, which is
// what every caller wants (the input + any preset/parts/preview
// sub-rows live next to the caption).
function scheduleField(labelText, ...children) {
return el('label', { class: 'schedule-field' },
el('span', { class: 'schedule-field-label' }, labelText),
...children);
}
// Interval composer (#466) — shared between the new-schedule form // Interval composer (#466) — shared between the new-schedule form
// and the edit-schedule form (#474). Builds the preset chip row + // and the edit-schedule form (#474). Builds the preset chip row +
// d/h/m/s sub-fields + live preview, and returns helpers to read // d/h/m/s sub-fields + live preview, and returns helpers to read
@ -2017,8 +2029,7 @@ window.marked = marked;
namePrefix = 'interval_', namePrefix = 'interval_',
initialSeconds = 0, initialSeconds = 0,
} = {}) { } = {}) {
const wrapper = el('label', { class: 'schedule-field' }, const wrapper = scheduleField(label);
el('span', { class: 'schedule-field-label' }, label));
const presets = [ const presets = [
['1m', 60], ['5m', 300], ['15m', 900], ['30m', 1800], ['1m', 60], ['5m', 300], ['15m', 900], ['30m', 1800],
['1h', 3600], ['6h', 21600], ['12h', 43200], ['1h', 3600], ['6h', 21600], ['12h', 43200],
@ -2142,8 +2153,6 @@ window.marked = marked;
// explicitly uncheckable. New-schedule callers pass `[]`. // explicitly uncheckable. New-schedule callers pass `[]`.
for (const n of extraNames) if (!candidates.includes(n)) candidates.push(n); for (const n of extraNames) if (!candidates.includes(n)) candidates.push(n);
const label = el('label', { class: 'schedule-field' },
el('span', { class: 'schedule-field-label' }, 'targets'));
const box = el('div', { class: 'schedule-targets' }); const box = el('div', { class: 'schedule-targets' });
for (const name of candidates) { for (const name of candidates) {
const id_ = idPrefix + name; const id_ = idPrefix + name;
@ -2154,8 +2163,7 @@ window.marked = marked;
box.append(el('label', { class: 'schedule-target-chip', for: id_ }, box.append(el('label', { class: 'schedule-target-chip', for: id_ },
cb, el('span', {}, name))); cb, el('span', {}, name)));
} }
label.append(box); return scheduleField('targets', box);
return label;
} }
function renderScheduleNewForm() { function renderScheduleNewForm() {
@ -2181,17 +2189,12 @@ window.marked = marked;
checked: new Set(carry.targets), checked: new Set(carry.targets),
})); }));
const bodyLabel = el('label', { class: 'schedule-field' },
el('span', { class: 'schedule-field-label' }, 'prompt body'));
const bodyInput = el('textarea', { const bodyInput = el('textarea', {
name: 'body', rows: '4', required: 'required', placeholder: 'message delivered to each target at fire time', name: 'body', rows: '4', required: 'required', placeholder: 'message delivered to each target at fire time',
}); });
bodyInput.value = carry.body; bodyInput.value = carry.body;
bodyLabel.append(bodyInput); form_.append(scheduleField('prompt body', bodyInput));
form_.append(bodyLabel);
const firstFireLabel = el('label', { class: 'schedule-field' },
el('span', { class: 'schedule-field-label' }, 'first fire'));
const firstFireInput = el('input', { const firstFireInput = el('input', {
type: 'datetime-local', name: 'first_fire', required: 'required', type: 'datetime-local', name: 'first_fire', required: 'required',
}); });
@ -2200,8 +2203,7 @@ window.marked = marked;
// immediately on a stale-clock accident). // immediately on a stale-clock accident).
firstFireInput.value = carry.first_fire firstFireInput.value = carry.first_fire
|| isoForDatetimeLocal(new Date(Date.now() + 5 * 60 * 1000)); || isoForDatetimeLocal(new Date(Date.now() + 5 * 60 * 1000));
firstFireLabel.append(firstFireInput); form_.append(scheduleField('first fire', firstFireInput));
form_.append(firstFireLabel);
// Interval composer (#466). See `buildIntervalComposer` below — // Interval composer (#466). See `buildIntervalComposer` below —
// shared between the new-schedule form and the edit-schedule form // shared between the new-schedule form and the edit-schedule form
@ -2227,14 +2229,11 @@ window.marked = marked;
} }
intervalCx.updatePreview(); intervalCx.updatePreview();
const descLabel = el('label', { class: 'schedule-field' },
el('span', { class: 'schedule-field-label' }, 'description (optional)'));
const descInput = el('input', { const descInput = el('input', {
type: 'text', name: 'description', placeholder: 'shown on the schedule card', type: 'text', name: 'description', placeholder: 'shown on the schedule card',
}); });
descInput.value = carry.description; descInput.value = carry.description;
descLabel.append(descInput); form_.append(scheduleField('description (optional)', descInput));
form_.append(descLabel);
const actions = el('div', { class: 'schedule-actions' }); const actions = el('div', { class: 'schedule-actions' });
const submit = el('button', { type: 'submit', class: 'btn btn-spawn' }, ' qu3ue prompt'); const submit = el('button', { type: 'submit', class: 'btn btn-spawn' }, ' qu3ue prompt');
@ -2470,32 +2469,23 @@ window.marked = marked;
const carry = scheduleEditCarry.get(s.id) || {}; const carry = scheduleEditCarry.get(s.id) || {};
const bodyLabel = el('label', { class: 'schedule-field' },
el('span', { class: 'schedule-field-label' }, 'body'));
const bodyInput = el('textarea', { name: 'body', rows: '4', required: 'required' }); const bodyInput = el('textarea', { name: 'body', rows: '4', required: 'required' });
bodyInput.value = carry.body !== undefined ? carry.body : s.body; bodyInput.value = carry.body !== undefined ? carry.body : s.body;
bodyLabel.append(bodyInput); form_.append(scheduleField('body', bodyInput));
form_.append(bodyLabel);
const descLabel = el('label', { class: 'schedule-field' },
el('span', { class: 'schedule-field-label' }, 'description (blank to clear)'));
const descInput = el('input', { type: 'text', name: 'description' }); const descInput = el('input', { type: 'text', name: 'description' });
descInput.value = carry.description !== undefined descInput.value = carry.description !== undefined
? carry.description ? carry.description
: (s.description || ''); : (s.description || '');
descLabel.append(descInput); form_.append(scheduleField('description (blank to clear)', descInput));
form_.append(descLabel);
const firstFireLabel = el('label', { class: 'schedule-field' },
el('span', { class: 'schedule-field-label' }, 'next fire'));
const firstFireInput = el('input', { const firstFireInput = el('input', {
type: 'datetime-local', name: 'next_fire', required: 'required', type: 'datetime-local', name: 'next_fire', required: 'required',
}); });
firstFireInput.value = carry.next_fire !== undefined firstFireInput.value = carry.next_fire !== undefined
? carry.next_fire ? carry.next_fire
: isoForDatetimeLocal(new Date(s.next_fire_at_unix * 1000)); : isoForDatetimeLocal(new Date(s.next_fire_at_unix * 1000));
firstFireLabel.append(firstFireInput); form_.append(scheduleField('next fire', firstFireInput));
form_.append(firstFireLabel);
const intervalCx = buildIntervalComposer({ const intervalCx = buildIntervalComposer({
label: 'interval (blank / all-zero = flip to one-shot)', label: 'interval (blank / all-zero = flip to one-shot)',