Skip to main content

max / goingson

Status badges take makeover's data-tone instead of seven domain classes Seven hand-coloured .badge.status-* rules shadowed a tone axis GoingsOn already had and was not using. Two of the seven matched nothing: status-on_hold never occurs (OnHold lowercases to onhold) and no Inactive status exists at all, import.rs maps that word to Archived. Two live statuses had no rule, so the contact timeline rendered Pending and Started flat. The mapping is a table in one place, GoingsOn.utils.statusTone, rather than a class per status spelled into the stylesheet: completed reads success, active and started read info, on-hold reads warning, and everything else is neutral. Archived and Pending are deliberately neutral; neither is news. Text tone only, so the tinted pill goes with the classes; data-color keeps its pill, since a tag's colour is the content rather than a state.
Co-Authored-By
Claude Opus 5 (1M context) <noreply@anthropic.com>
Author: Max Johnson <me@maxj.phd> · 2026-08-06 16:49 UTC
Signed with PGP, not checked
Commit: 2251d2e9f2ddfeeec058a86c977f9da1e000f554
Parent: f6b3fdd
8 files changed, +77 insertions, -51 deletions
@@ -57,9 +57,9 @@
57 57 ### Badge: `.badge`
58 58 There is no `.tag`. It was a second name for the same thing and collapsed into `.badge`.
59 59 Color variant: `[data-color="green|yellow|red|cyan|purple|blue|muted"]`.
60 - Status variant: `.status-active | .status-onhold | .status-archived | .status-inactive | .status-completed`, plus `.badge-shared`.
60 + Status variant: `[data-tone="info|success|warning|danger"]`, from the generated stylesheet. Which tone a status takes is `GoingsOn.utils.statusTone(status)`; an unmapped status gets no attribute and stays neutral. The hand-coloured `.status-*` and `.badge-shared` classes are gone (2026-08-06).
61 61 Sizes and intent: `.badge--xs`, `.badge--filled`.
62 - A plain badge is flat: no fill, no edge, text at `--content-muted`, all from the generated `.badge`. An edge on a label says it can be pressed, and no badge in GO is interactive. A badge carrying `[data-color]` or a status class is a pill and declares its own fill and border.
62 + A plain badge is flat: no fill, no edge, text at `--content-muted`, all from the generated `.badge`. An edge on a label says it can be pressed, and no badge in GO is interactive. `[data-tone]` tones the text and leaves the box alone. `[data-color]` is the other axis and does make a pill: it names GO's own per-tag palette, where the colour is the content rather than a state.
63 63
64 64 ### Modal: `.modal-overlay` (single global)
65 65 Open via `GoingsOn.ui.openModal(title, html, opts)`. Sub-parts: `.modal-container` (+ `.modal-large`), `.modal-header`, `.modal-title`, `.modal-content`, `.modal-close`. Visibility via `.hidden` / `.closing`. **There is only one modal overlay in the DOM** (`#modal-overlay` in `index.html`); never create another.
@@ -242,7 +242,7 @@
242 242 <p class="card-description">Description text</p>
243 243 <div class="card-meta">
244 244 <span class="badge">Job</span>
245 - <span class="badge status-active">Active</span>
245 + <span class="badge" data-tone="info">Active</span>
246 246 </div>
247 247 </div>
248 248 ```
@@ -264,8 +264,8 @@
264 264 <span class="badge">Label</span>
265 265 ```
266 266
267 - **Colour makes it a pill.** A badge carrying `[data-color]` or a status class
268 - declares its own fill and border:
267 + **Colour makes it a pill.** A badge carrying `[data-color]` declares its own
268 + fill and border. This is the per-tag palette, where the colour is the content:
269 269
270 270 ```html
271 271 <span class="badge" data-color="green">Success</span>
@@ -277,12 +277,15 @@
277 277 <span class="badge" data-color="muted">Muted</span>
278 278 ```
279 279
280 - **Status classes:**
280 + **Status is a tone, not a class.** `[data-tone]` tones the text and leaves the
281 + box flat. Call `GoingsOn.utils.statusTone(status)` for the mapping rather than
282 + picking a tone at the render site; a status with no tone gets no attribute:
281 283
282 284 ```html
283 - <span class="badge status-active">Active</span>
284 - <span class="badge status-completed">Completed</span>
285 - <span class="badge badge-shared">Shared</span>
285 + <span class="badge" data-tone="success">Completed</span>
286 + <span class="badge" data-tone="info">Active</span>
287 + <span class="badge" data-tone="warning">On Hold</span>
288 + <span class="badge">Archived</span>
286 289 ```
287 290
288 291 **Size and intent:** `.badge--xs` for a smaller pill, `.badge--filled` for a
@@ -1049,45 +1049,13 @@
1049 1049 line-height: 1;
1050 1050 }
1051 1051
1052 - /* Project status colours. Same pill treatment as [data-color]: a status badge
1053 - declares its own fill and edge, so it has to declare the border shorthand
1054 - too, since the base .badge has none. Applied by template interpolation
1055 - (js/projects.js, js/contact-dashboard.js build status-${...}), so a literal
1056 - grep for these class names finds nothing. */
1057 - .badge.status-active,
1058 - .badge.status-onhold, .badge.status-on_hold,
1059 - .badge.status-archived,
1060 - .badge.status-inactive,
1061 - .badge.status-completed,
1062 - .badge.badge-shared {
1063 - border: var(--border-width-sm) solid var(--border);
1064 - color: var(--content);
1065 - }
1066 - .badge.status-active {
1067 - background-color: color-mix(in srgb, var(--success) 20%, var(--surface-raised));
1068 - border-color: var(--success);
1069 - }
1070 - .badge.status-onhold, .badge.status-on_hold {
1071 - background-color: color-mix(in srgb, var(--warning) 20%, var(--surface-raised));
1072 - border-color: var(--warning);
1073 - }
1074 - .badge.status-archived {
1075 - background-color: var(--surface-sunken);
1076 - border-color: var(--content-muted);
1077 - }
1078 - .badge.status-inactive {
1079 - background-color: color-mix(in srgb, var(--danger) 20%, var(--surface-raised));
1080 - border-color: var(--danger);
1081 - }
1082 - .badge.status-completed {
1083 - background-color: color-mix(in srgb, var(--category-six) 20%, var(--surface-raised));
1084 - border-color: var(--category-six);
1085 - }
1086 - /* Groups: a project shared into a group. */
1087 - .badge.badge-shared {
1088 - background-color: color-mix(in srgb, var(--info) 20%, var(--surface-raised));
1089 - border-color: var(--info);
1090 - }
1052 + /* Status badges carry no rule here. They are `.badge[data-tone]`, and the
1053 + generated layout.css tones the text; the tinted fill and matching edge these
1054 + seven domain classes used to draw came out 2026-08-06 (goingson task
1055 + 724a0667). data-color above is a different axis and stays: it names GO's own
1056 + per-tag palette, where the colour is the content, not a state.
1057 +
1058 + The tone each status takes is decided in js/badge.js, not here. */
1091 1059 /* Inline marker on a task row whose project is shared. */
1092 1060 .shared-badge {
1093 1061 display: inline-block;
@@ -55,8 +55,9 @@
55 55 const icon = item.kind === 'task' ? '&#x2611;'
56 56 : item.kind === 'event' ? '&#x1F4C5;'
57 57 : (item.isOutgoing ? '&#x2709;&#xFE0E;&rarr;' : '&larr;&#x2709;&#xFE0E;');
58 + const tone = GoingsOn.utils.statusTone(item.status);
58 59 const badge = item.kind === 'task' && item.status
59 - ? `<span class="badge status-${(item.status || '').toLowerCase()}">${esc(item.status)}</span>`
60 + ? `<span class="badge"${tone ? ` data-tone="${tone}"` : ''}>${esc(item.status)}</span>`
60 61 : '';
61 62
62 63 let act = '';
@@ -59,7 +59,7 @@
59 59 if (!project || !project.groupId) return '';
60 60 const name = nameFor(project.groupId);
61 61 const label = name ? `Shared · ${name}` : 'Shared';
62 - return `<span class="badge badge-shared" title="${escAttr(label)}">${esc(label)}</span>`;
62 + return `<span class="badge" data-tone="info" title="${escAttr(label)}">${esc(label)}</span>`;
63 63 }
64 64
65 65 /**
@@ -114,6 +114,7 @@
114 114 * @returns {string} Card markup
115 115 */
116 116 function renderProjectCard(p) {
117 + const tone = GoingsOn.utils.statusTone(p.status || 'Active');
117 118 return `
118 119 <div class="card project-card" data-act="projects.open" data-a1="${escAttr(p.id)}"
119 120 data-contextmenu="contextMenus.showProject" data-a1="@event" data-a2="${escAttr(p.id)}"
@@ -125,7 +126,7 @@
125 126 <div class="card-description markdown-content">${p.descriptionHtml || ''}</div>
126 127 <div class="card-meta">
127 128 <span class="badge type-${(p.projectType || 'other').toLowerCase()}">${esc(p.projectTypeDisplay || p.projectType || 'Other')}</span>
128 - <span class="badge status-${(p.status || 'active').toLowerCase()}">${esc(p.statusDisplay || p.status || 'Active')}</span>
129 + <span class="badge"${tone ? ` data-tone="${tone}"` : ''}>${esc(p.statusDisplay || p.status || 'Active')}</span>
129 130 ${GoingsOn.groups.projectCardBadge(p)}
130 131 </div>
131 132 </div>
@@ -621,8 +621,32 @@
621 621 : '··· Show quoted text';
622 622 }
623 623
624 + /** The tone a status badge wears, for `.badge[data-tone]`.
625 + *
626 + * Domain statuses are many and tones are four, so this is where the mapping
627 + * lives: one table rather than a class per status spelled into the stylesheet
628 + * (decided 2026-08-06, task 724a0667). Takes a project status (Active, OnHold,
629 + * Completed, Archived) or a task status (Pending, Started, Completed); both
630 + * spaces are keyed here because both render the same badge.
631 + *
632 + * An unmapped or absent status returns '' and the badge stays neutral, which
633 + * is also the deliberate answer for Archived and Pending: neither is news.
634 + * @param {string|null|undefined} status
635 + * @returns {string} a tone name, or '' for none
636 + */
637 + function statusTone(status) {
638 + switch (String(status || '').toLowerCase().replace(/[\s_-]/g, '')) {
639 + case 'completed': return 'success';
640 + case 'active': return 'info';
641 + case 'started': return 'info';
642 + case 'onhold': return 'warning';
643 + default: return '';
644 + }
645 + }
646 +
624 647 GoingsOn.utils = {
625 648 toggleQuoted,
649 + statusTone,
626 650 // HTML escaping. escapeJsString is intentionally NOT exported: the only
627 651 // sound attribute escapers are escapeAttrValue (plain attrs) and
628 652 // escapeHandlerArg (a value inside a quoted JS string in an inline handler).
@@ -354,6 +354,35 @@
354 354 });
355 355 });
356 356
357 + describe('GoingsOn.utils.statusTone', () => {
358 + test('maps the project statuses', () => {
359 + assertEqual(GoingsOn.utils.statusTone('Active'), 'info');
360 + assertEqual(GoingsOn.utils.statusTone('OnHold'), 'warning');
361 + assertEqual(GoingsOn.utils.statusTone('Completed'), 'success');
362 + assertEqual(GoingsOn.utils.statusTone('Archived'), '');
363 + });
364 +
365 + test('maps the task statuses', () => {
366 + assertEqual(GoingsOn.utils.statusTone('Pending'), '');
367 + assertEqual(GoingsOn.utils.statusTone('Started'), 'info');
368 + assertEqual(GoingsOn.utils.statusTone('Completed'), 'success');
369 + });
370 +
371 + test('one status, however it is spelled', () => {
372 + // The old CSS carried status-onhold and status-on_hold as two rules.
373 + assertEqual(GoingsOn.utils.statusTone('on_hold'), 'warning');
374 + assertEqual(GoingsOn.utils.statusTone('On Hold'), 'warning');
375 + assertEqual(GoingsOn.utils.statusTone('ON-HOLD'), 'warning');
376 + });
377 +
378 + test('unknown or absent status is neutral, never a stray attribute', () => {
379 + assertEqual(GoingsOn.utils.statusTone('Inactive'), '');
380 + assertEqual(GoingsOn.utils.statusTone(''), '');
381 + assertEqual(GoingsOn.utils.statusTone(null), '');
382 + assertEqual(GoingsOn.utils.statusTone(undefined), '');
383 + });
384 + });
385 +
357 386 // Test: PaginationManager
358 387
359 388 describe('PaginationManager', () => {