Skip to main content

max / goingson

Adopt the generated badge, collapse .tag into it Last primitive of adoption 1. Token::Badge is Depth::Flat, so the fill, the edge and the text colour now come from the generated .badge in layout.css and the hand-written ones are gone. A plain badge loses its raised bevel: an edge on a label says it can be pressed, and all 44 call sites are non-interactive labels with no data-act, no onclick and no remove affordance. .tag was a second name for the same thing, sharing one rule with .badge. It could never receive a generated rule, so keeping it guaranteed the two would drift the moment makeover changed what a badge is. The 11 .tag call sites are now .badge, .tag.tag-shared is .badge.badge-shared, and .tag.status-* is .badge.status-*. Colour keeps the pill. data-color is GO's own per-tag palette rather than a makeover Tone, so the coloured variants stay hand-written, and they now declare the border shorthand themselves instead of inheriting it from a base that no longer has one. Same for the project status colours. .badge--filled had to gain a [data-color] qualifier to still out-specify that edge. Visible change, wants an eyeball: plain badges are flat text at --content-muted; coloured and status badges look as they did.
Author: Max Johnson <me@maxj.phd> · 2026-07-29 23:57 UTC
Signed with PGP, not checked
Commit: eb3dd1199f9c7523011882d26585f57f335df782
Parent: 8e38884
7 files changed, +56 insertions, -39 deletions
@@ -10,7 +10,7 @@
10 10 .stack-{peer,group,section})
11 11 3. Use a COMPONENT PRIMITIVE for a UI element (.card, .button, .field,
12 12 .modal, .badge,
13 - .tag, .toggle-switch)
13 + .toggle-switch)
14 14 4. Extend a primitive with a modifier (.button--primary, .button--sm,
15 15 .field--compact,
16 16 .subtask-item--linked)
@@ -154,8 +154,7 @@
154 154 .field + .field--ghost / --compact (kind rides on the element:
155 155 input / select / textarea)
156 156 .modal + .modal-overlay
157 - .badge + variants ([data-color], --xs / --sm / --filled)
158 - .tag + variants ([data-color])
157 + .badge + variants ([data-color], .status-*, --xs / --sm / --filled)
159 158 .toggle-switch
160 159 .avatar + .avatar--sm / --lg / --unknown
161 160 .toast + variants
@@ -417,7 +416,6 @@
417 416 .mobile-tab-bar,
418 417 .mobile-tab-create,
419 418 .badge,
420 - .tag,
421 419 .toggle-switch,
422 420 label,
423 421 button {
@@ -881,45 +879,51 @@
881 879
882 880 /* 12. Tags & Badges */
883 881
884 - .tag, .badge {
882 + /* Shape only. Fill, edge and text colour come from the generated .badge in
883 + layout.css, where Token::Badge is Depth::Flat: a label is not pressable, so
884 + it carries no bevel. Do not add background, border or box-shadow here. */
885 + .badge {
885 886 display: inline-flex;
886 887 align-items: center;
887 888 padding: var(--step-tight) var(--step-roomy);
888 - border: var(--border-width-sm) solid var(--border);
889 889 border-radius: var(--radius-sm);
890 890 font-size: var(--font-size-md);
891 891 font-weight: 600;
892 - background: var(--surface-raised);
893 - color: var(--content);
894 - box-shadow: var(--bevel-raised);
895 892 }
896 893
897 - /* Color variants using data attributes */
898 - .badge[data-color="green"], .tag[data-color="green"] {
894 + /* Colour variants. data-color is GO's own per-tag palette, not a makeover
895 + Tone, so these stay hand-written. A coloured badge is a pill: it declares
896 + its own tinted fill and matching edge, because the colour carries meaning
897 + the flat base cannot. */
898 + .badge[data-color] {
899 + border: var(--border-width-sm) solid var(--border);
900 + color: var(--content);
901 + }
902 + .badge[data-color="green"] {
899 903 background-color: color-mix(in srgb, var(--category-two) 20%, var(--surface-raised));
900 904 border-color: var(--category-two);
901 905 }
902 - .badge[data-color="yellow"], .tag[data-color="yellow"] {
906 + .badge[data-color="yellow"] {
903 907 background-color: color-mix(in srgb, var(--category-four) 20%, var(--surface-raised));
904 908 border-color: var(--category-four);
905 909 }
906 - .badge[data-color="red"], .tag[data-color="red"] {
910 + .badge[data-color="red"] {
907 911 background-color: color-mix(in srgb, var(--category-one) 20%, var(--surface-raised));
908 912 border-color: var(--category-one);
909 913 }
910 - .badge[data-color="cyan"], .tag[data-color="cyan"] {
914 + .badge[data-color="cyan"] {
911 915 background-color: color-mix(in srgb, var(--category-six) 20%, var(--surface-raised));
912 916 border-color: var(--category-six);
913 917 }
914 - .badge[data-color="purple"], .tag[data-color="purple"] {
918 + .badge[data-color="purple"] {
915 919 background-color: color-mix(in srgb, var(--category-five) 20%, var(--surface-raised));
916 920 border-color: var(--category-five);
917 921 }
918 - .badge[data-color="muted"], .tag[data-color="muted"] {
922 + .badge[data-color="muted"] {
919 923 background-color: var(--surface-sunken);
920 924 border-color: var(--content-muted);
921 925 }
922 - .badge[data-color="blue"], .tag[data-color="blue"] {
926 + .badge[data-color="blue"] {
923 927 background-color: color-mix(in srgb, var(--category-three) 20%, var(--surface-raised));
924 928 border-color: var(--category-three);
925 929 }
@@ -927,10 +931,10 @@
927 931 /* Size modifiers */
928 932 .badge--xs { padding: var(--step-hair) var(--step-base); font-size: var(--font-size-sm); }
929 933
930 - /* Filled intent, solid accent fill, no border. Flat by intent, so no bevel
931 - either: the fill is the signal and an edge would fight it. Pairs with
932 - data-color. */
933 - .badge--filled { border: none; box-shadow: none; }
934 + /* Filled intent, solid accent fill, no border: the fill is the signal and an
935 + edge would fight it. Pairs with data-color, and has to out-specify the
936 + [data-color] edge above to drop it. */
937 + .badge--filled[data-color] { border: none; }
934 938 .badge--filled[data-color="green"] { background: var(--category-two); color: var(--content-on-action); }
935 939 .badge--filled[data-color="yellow"] { background: var(--category-four); color: var(--content); }
936 940 .badge--filled[data-color="red"] { background: var(--category-one); color: var(--content-on-action); }
@@ -955,29 +959,42 @@
955 959 line-height: 1;
956 960 }
957 961
958 - /* Project status tag colors */
959 - .tag.status-active {
962 + /* Project status colours. Same pill treatment as [data-color]: a status badge
963 + declares its own fill and edge, so it has to declare the border shorthand
964 + too, since the base .badge has none. Applied by template interpolation
965 + (js/projects.js, js/contact-dashboard.js build status-${...}), so a literal
966 + grep for these class names finds nothing. */
967 + .badge.status-active,
968 + .badge.status-onhold, .badge.status-on_hold,
969 + .badge.status-archived,
970 + .badge.status-inactive,
971 + .badge.status-completed,
972 + .badge.badge-shared {
973 + border: var(--border-width-sm) solid var(--border);
974 + color: var(--content);
975 + }
976 + .badge.status-active {
960 977 background-color: color-mix(in srgb, var(--success) 20%, var(--surface-raised));
961 978 border-color: var(--success);
962 979 }
963 - .tag.status-onhold, .tag.status-on_hold {
980 + .badge.status-onhold, .badge.status-on_hold {
964 981 background-color: color-mix(in srgb, var(--warning) 20%, var(--surface-raised));
965 982 border-color: var(--warning);
966 983 }
967 - .tag.status-archived {
984 + .badge.status-archived {
968 985 background-color: var(--surface-sunken);
969 986 border-color: var(--content-muted);
970 987 }
971 - .tag.status-inactive {
988 + .badge.status-inactive {
972 989 background-color: color-mix(in srgb, var(--danger) 20%, var(--surface-raised));
973 990 border-color: var(--danger);
974 991 }
975 - .tag.status-completed {
992 + .badge.status-completed {
976 993 background-color: color-mix(in srgb, var(--category-six) 20%, var(--surface-raised));
977 994 border-color: var(--category-six);
978 995 }
979 996 /* Groups: a project shared into a group. */
980 - .tag.tag-shared {
997 + .badge.badge-shared {
981 998 background-color: color-mix(in srgb, var(--info) 20%, var(--surface-raised));
982 999 border-color: var(--info);
983 1000 }
@@ -56,7 +56,7 @@
56 56 : item.kind === 'event' ? '&#x1F4C5;'
57 57 : (item.isOutgoing ? '&#x2709;&#xFE0E;&rarr;' : '&larr;&#x2709;&#xFE0E;');
58 58 const badge = item.kind === 'task' && item.status
59 - ? `<span class="tag status-${(item.status || '').toLowerCase()}">${esc(item.status)}</span>`
59 + ? `<span class="badge status-${(item.status || '').toLowerCase()}">${esc(item.status)}</span>`
60 60 : '';
61 61
62 62 let act = '';
@@ -95,7 +95,7 @@
95 95 const title = contact.title ? `<span class="text-secondary">${esc(contact.title)}</span>` : '';
96 96 const companyTitle = [company, title].filter(Boolean).join(' &middot; ');
97 97
98 - const tags = (contact.tags || []).map(t => `<span class="tag">${esc(t)}</span>`).join(' ');
98 + const tags = (contact.tags || []).map(t => `<span class="badge">${esc(t)}</span>`).join(' ');
99 99
100 100 let headerHtml = `
101 101 <div class="contact-header-card">
@@ -112,10 +112,10 @@
112 112 let infoHtml = '';
113 113 const infoItems = [];
114 114 for (const e of contact.emails || []) {
115 - infoItems.push(`<span class="text-secondary">Email:</span> ${esc(e.address)}${e.label ? ' <span class="tag">' + esc(e.label) + '</span>' : ''}`);
115 + infoItems.push(`<span class="text-secondary">Email:</span> ${esc(e.address)}${e.label ? ' <span class="badge">' + esc(e.label) + '</span>' : ''}`);
116 116 }
117 117 for (const p of contact.phones || []) {
118 - infoItems.push(`<span class="text-secondary">Phone:</span> ${esc(p.number)}${p.label ? ' <span class="tag">' + esc(p.label) + '</span>' : ''}`);
118 + infoItems.push(`<span class="text-secondary">Phone:</span> ${esc(p.number)}${p.label ? ' <span class="badge">' + esc(p.label) + '</span>' : ''}`);
119 119 }
120 120 for (const s of contact.socialHandles || contact.social_handles || []) {
121 121 infoItems.push(`<span class="text-secondary">${esc(s.platform)}:</span> ${esc(s.handle)}`);
@@ -36,7 +36,7 @@
36 36 const company = c.company ? `<span class="contact-company">${esc(c.company)}</span>` : '';
37 37 const emailLine = primaryEmail ? `<span class="contact-email">${esc(primaryEmail)}</span>` : '';
38 38 const tagPills = (c.tags || []).map(t =>
39 - `<span class="tag">${esc(t)}</span>`
39 + `<span class="badge">${esc(t)}</span>`
40 40 ).join('');
41 41
42 42 return `
@@ -79,7 +79,7 @@
79 79
80 80 // Tags
81 81 const tagPills = (contact.tags || []).map(t =>
82 - `<span class="tag">${esc(t)}</span>`
82 + `<span class="badge">${esc(t)}</span>`
83 83 ).join(' ');
84 84
85 85 // Emails
@@ -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="tag tag-shared" title="${escAttr(label)}">${esc(label)}</span>`;
62 + return `<span class="badge badge-shared" title="${escAttr(label)}">${esc(label)}</span>`;
63 63 }
64 64
65 65 /**
@@ -105,7 +105,7 @@
105 105 function rowHtml(p) {
106 106 const settled = p.status !== 'Open';
107 107 const tags = (p.tags || []).map(t =>
108 - `<span class="tag badge--xs" data-color="muted">${esc(t)}</span>`).join('');
108 + `<span class="badge badge--xs" data-color="muted">${esc(t)}</span>`).join('');
109 109
110 110 // The score leads the row: it is the reason this problem is where it is
111 111 // in the list, so it should be readable before the title.
@@ -124,8 +124,8 @@
124 124 </div>
125 125 <div class="card-description markdown-content">${p.descriptionHtml || ''}</div>
126 126 <div class="card-meta">
127 - <span class="tag type-${(p.projectType || 'other').toLowerCase()}">${esc(p.projectTypeDisplay || p.projectType || 'Other')}</span>
128 - <span class="tag status-${(p.status || 'active').toLowerCase()}">${esc(p.statusDisplay || p.status || 'Active')}</span>
127 + <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 129 ${GoingsOn.groups.projectCardBadge(p)}
130 130 </div>
131 131 </div>
@@ -335,7 +335,7 @@
335 335 if (t.recurrence && t.recurrence !== 'None') details.push(`<strong>Recurrence:</strong> ${esc(t.recurrence)}`);
336 336 if (t.contactName) details.push(`<strong>Contact:</strong> ${esc(t.contactName)}`);
337 337 if (t.tags && t.tags.length > 0) {
338 - details.push(`<strong>Tags:</strong> ${t.tags.map(tag => `<span class="tag">${esc(tag)}</span>`).join(' ')}`);
338 + details.push(`<strong>Tags:</strong> ${t.tags.map(tag => `<span class="badge">${esc(tag)}</span>`).join(' ')}`);
339 339 }
340 340
341 341 if (details.length > 0) {