max / goingson
- Co-Authored-By
- Claude Opus 5 (1M context) <noreply@anthropic.com>
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' ? '☑' | |
| 56 | 56 | : item.kind === 'event' ? '📅' | |
| 57 | 57 | : (item.isOutgoing ? '✉︎→' : '←✉︎'); | |
| 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', () => { |