From cc2affc9457353672df62c537ecd1feed41b2d54 Mon Sep 17 00:00:00 2001 From: Riley Sheehan Date: Tue, 27 Oct 2026 13:35:00 -0500 Subject: [PATCH] The site stops promising Microsoft To Do, and the rain chance and monograms reach the wall's 4.8:1 --- dist/app.js | 15 ++++++++++++--- dist/style.css | 5 +++-- dist/tokens.css | 11 +++++++++-- site/index.html | 2 +- test/wall.test.js | 16 ++++++++++++++++ 5 files changed, 41 insertions(+), 8 deletions(-) diff --git a/dist/app.js b/dist/app.js index 80f5048..6e55a87 100644 --- a/dist/app.js +++ b/dist/app.js @@ -572,10 +572,19 @@ var found = (tasks && tasks.lists || []).filter(function (l) { return l.name === name; })[0]; return found || { name: name, icon: 'list', person: false, color: '' }; } + // A person's initial on their colour. The letter takes whichever ink reads better on that colour, white or the night + // surface's near-black (--mono-on-dark, --mono-on-light in tokens.css): white on every person colour fell to 2.8:1 on + // the palette's orange. With no colour it sits on --ink-2 in --mono-ink. + var MONO_ON_DARK = [255, 255, 255], MONO_ON_LIGHT = [12, 17, 29]; + function monogram(name, color) { + var c = /^#[0-9a-f]{6}$/i.test(color || '') ? [1, 3, 5].map(function (i) { return parseInt(color.slice(i, i + 2), 16); }) : null; + var a = node('span', c ? 'avatar ' + (contrast(MONO_ON_DARK, c) >= contrast(MONO_ON_LIGHT, c) ? 'on-dark' : 'on-light') : 'avatar', name.charAt(0).toUpperCase()); + if (c) a.style.backgroundColor = color; + return a; + } // A person, as their initial in the color their calendar uses: the mark their own list carries. function personMark(name, color, label) { - var a = node('span', 'avatar', name.charAt(0).toUpperCase()); - if (color) a.style.backgroundColor = color; + var a = monogram(name, color); a.setAttribute('title', name); a.setAttribute('aria-label', label); return a; } @@ -593,7 +602,7 @@ } function listMark(name) { var l = listOf(name), svg; - if (l.person) { var a = node('span', 'avatar', l.name.charAt(0).toUpperCase()); if (l.color) a.style.backgroundColor = l.color; a.setAttribute('aria-hidden', 'true'); return a; } + if (l.person) { var a = monogram(l.name, l.color); a.setAttribute('aria-hidden', 'true'); return a; } svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg'); svg.setAttribute('class', 'glyph'); svg.setAttribute('viewBox', '0 0 24 24'); svg.setAttribute('aria-hidden', 'true'); (GLYPHS[l.icon] || GLYPHS.list).forEach(function (d) { var p = document.createElementNS('http://www.w3.org/2000/svg', 'path'); p.setAttribute('d', d); svg.appendChild(p); }); return svg; diff --git a/dist/style.css b/dist/style.css index cc1f0c6..2d18f4d 100644 --- a/dist/style.css +++ b/dist/style.css @@ -319,7 +319,7 @@ body[data-rest] .photo-card{border-radius:0} .forecast{display:flex;align-items:center;height:3.2rem;margin:.7rem 0 1.1rem;padding-bottom:1rem;box-sizing:content-box;border-bottom:.0625rem solid var(--line);font-size:1.45rem;color:var(--ink-2);font-variant-numeric:tabular-nums;white-space:nowrap} .forecast .wx,.agenda-wx .wx{flex:none;width:2.6rem;margin-right:.6rem;display:flex;flex-direction:column;align-items:center} .forecast .wx-icon,.agenda-wx .wx-icon{width:2.4rem;height:2.4rem;color:var(--ink-2)} -.forecast small,.agenda-wx small{font-size:1.05rem;line-height:1;font-weight:600;color:var(--rain);margin-top:-.1rem} +.forecast small,.agenda-wx small{font-size:1.05rem;line-height:1;font-weight:600;color:var(--rain-ink);margin-top:-.1rem} .forecast b{color:var(--ink);font-weight:600;margin-right:.45rem} .day-items{flex:1;min-height:0;overflow:hidden} .day .item{margin-bottom:calc(1.15rem*var(--ts))} @@ -440,7 +440,8 @@ button.item:active{opacity:.6} .day .candle{width:.511em;height:.876em;margin-right:.36em;vertical-align:-.073em} /* A list's mark: a glyph for a household list, a monogram in the person's color for a person's list. */ .glyph{flex:none;width:1.75rem;height:1.75rem;fill:none;stroke:currentColor;stroke-width:1.9;stroke-linecap:round;stroke-linejoin:round} -.avatar{flex:none;display:flex;align-items:center;justify-content:center;width:1.9rem;height:1.9rem;border-radius:50%;background:var(--ink-2);color:#fff;font:700 1.05rem/1 var(--text);letter-spacing:0} +.avatar{flex:none;display:flex;align-items:center;justify-content:center;width:1.9rem;height:1.9rem;border-radius:50%;background:var(--ink-2);color:var(--mono-ink);font:700 1.05rem/1 var(--text);letter-spacing:0} +.avatar.on-dark{color:var(--mono-on-dark)}.avatar.on-light{color:var(--mono-on-light)} .tabs .label{margin-left:.6rem} .item-title .avatar{display:inline-flex;width:1.7rem;height:1.7rem;font-size:.95rem;vertical-align:.1em} .item-title .avatar+.avatar{margin-left:.3rem} diff --git a/dist/tokens.css b/dist/tokens.css index 935a755..12c8484 100644 --- a/dist/tokens.css +++ b/dist/tokens.css @@ -31,13 +31,17 @@ } /* The same in both themes. The sky is only a default: app.js paints --sky-* on every minute. --sky-rule is the - hairline drawn on the sky: a mid-grey, so it reads on any sky. */ + hairline drawn on the sky: a mid-grey, so it reads on any sky. --rain is the drops of a rain icon. A monogram on a + person's colour takes whichever of the two --mono-on-* inks reads better there (app.js decides, DESIGN.md → Rain and + monograms reach 4.8:1). */ :root{ --sky-top:var(--sky-day-top);--sky-bottom:var(--sky-day-bottom);--sky-ink:var(--navy-900);--sky-rule:var(--grey-a35); --sun:var(--yellow-sun);--rain:var(--blue-rain);--moon:var(--cream-moon); + --mono-on-dark:var(--mist-0);--mono-on-light:var(--slate-900); } -/* Light. --selected, --shade, --tint and --star are the wall's; --bad and --good are the phone's (the wall has no +/* Light. --selected, --shade, --tint, --star, --rain-ink (the rain chance as text) and --mono-ink (the letter of a + monogram with no colour of its own, on --ink-2) are the wall's; --bad and --good are the phone's (the wall has no error colour). The phone's card shadow is its own, rgba(16,38,63,.08) in setup.css, not --shade. */ :root,.theme-light{ --surface:var(--mist-100);--card:var(--mist-0);--ink:var(--navy-800);--ink-2:var(--navy-600);--line:var(--mist-300); @@ -46,6 +50,7 @@ --selected:var(--mist-0); --shade:rgba(16,38,63,.14);--tint:rgba(16,38,63,.028); --star:var(--ember-600);--bad:var(--red-600);--good:var(--green-600); + --rain-ink:var(--blue-600);--mono-ink:var(--mist-0); } /* Dark, for the phone. Keep in step with .theme-dark below. */ @@ -55,6 +60,7 @@ --selected:var(--slate-500); --shade:rgba(0,0,0,.5);--tint:rgba(255,255,255,.025); --star:var(--ember-300);--bad:var(--red-300);--good:var(--green-300); + --rain-ink:var(--blue-rain);--mono-ink:var(--slate-900); }} /* Dark, for the wall. Keep in step with the block above. */ @@ -64,4 +70,5 @@ --selected:var(--slate-500); --shade:rgba(0,0,0,.5);--tint:rgba(255,255,255,.025); --star:var(--ember-300);--bad:var(--red-300);--good:var(--green-300); + --rain-ink:var(--blue-rain);--mono-ink:var(--slate-900); } diff --git a/site/index.html b/site/index.html index bdeacb7..b2cbfd6 100644 --- a/site/index.html +++ b/site/index.html @@ -313,7 +313,7 @@

No Gingham cloud

Lists, at the wall or from a phone.

-

Kept by Gingham, or in Todoist, Google Tasks, Microsoft To Do, Home Assistant or a CalDAV server such as Nextcloud or Fastmail. Check things off with a moment to undo. A phone reaches Gingham’s own lists on the home Wi‑Fi, or from anywhere when they live on a server with https. The others go wherever their own apps go.

+

Kept by Gingham, or in Todoist, Home Assistant, a CalDAV server such as Nextcloud or Fastmail, or Google Tasks once you register a Google app of your own. Check things off with a moment to undo. A phone reaches Gingham’s own lists on the home Wi‑Fi, or from anywhere when they live on a server with https. The others go wherever their own apps go.

diff --git a/test/wall.test.js b/test/wall.test.js index 6c51526..93c7db4 100644 --- a/test/wall.test.js +++ b/test/wall.test.js @@ -833,3 +833,19 @@ test('Offline, the note sits in the middle of the card, and Today says when the // With the lists answering, Today names only the calendar. assert.equal(load({fixture: 'empty', now: '2026-09-23T09:10:00', overrides: {'/api/calendar': null}}).$('today-list').textContent, 'Can’t reach the calendar'); }); + +test('A monogram\'s letter takes the ink that reads on its person\'s colour, and the rain chance has its own ink', () => { + // Mara on the palette's indigo, the one colour white still wins on; June on its orange, which takes the near-black; and + // Theo with no colour at all, who sits on --ink-2 in --mono-ink. + const base = require('./fixtures').stress.tasks; + const tasks = {...base, people: {'1': {name: 'Mara', color: '#5b6ee1'}, '2': {name: 'Theo', color: ''}}}; + const w = load({now: '2026-09-23T15:40:00', overrides: {'/api/tasks': tasks}}); + const marks = [...w.document.querySelectorAll('.avatar')]; + const ink = letter => [...new Set(marks.filter(a => a.textContent === letter).map(a => a.classes.filter(c => /^on-/.test(c)).join(' ') || 'none'))]; + assert.deepEqual(ink('M'), ['on-dark'], 'white on #5b6ee1'); + assert.deepEqual(ink('J'), ['on-light'], 'near-black on June\'s #e0823d'); + assert.deepEqual(ink('T'), ['none'], 'no colour: --mono-ink on --ink-2'); + const css = require('node:fs').readFileSync(require('node:path').join(__dirname, '../dist/style.css'), 'utf8'); + assert.match(css, /\.forecast small,\.agenda-wx small\{[^}]*color:var\(--rain-ink\)/, 'the rain chance is text in --rain-ink'); + assert.match(css, /\.wx-icon \.drop\{stroke:var\(--rain\)\}/, 'the drops keep --rain'); +});