Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 12 additions & 3 deletions dist/app.js
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
Expand All @@ -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;
Expand Down
5 changes: 3 additions & 2 deletions dist/style.css
Original file line number Diff line number Diff line change
Expand Up @@ -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))}
Expand Down Expand Up @@ -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}
Expand Down
11 changes: 9 additions & 2 deletions dist/tokens.css
Original file line number Diff line number Diff line change
Expand Up @@ -31,13 +31,17 @@
}

/* The same in both themes. The sky is only a default: app.js paints --sky-* on <html> 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);
Expand All @@ -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. */
Expand All @@ -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. */
Expand All @@ -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);
}
2 changes: 1 addition & 1 deletion site/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -313,7 +313,7 @@ <h3>No Gingham cloud</h3>
<section class="room" id="lists">
<div class="words">
<h2 class="h2">Lists, at the wall or from a phone.</h2>
<p class="lede">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.</p>
<p class="lede">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.</p>
</div>
<!-- A crop is a picture of the wall: its label says what it shows, and what is drawn inside it is inert, so its
buttons are never reached or read as buttons. The inert sits inside the picture so the label is still read. -->
Expand Down
16 changes: 16 additions & 0 deletions test/wall.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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');
});
Loading