Sync repo to deployed state: SEO recovery, Publishing Desk, Play games, emoji picker
The deploy pipeline runs from the working tree, so a wave of shipped features
had never been committed. This snapshots git to what's actually running.
SEO impression recovery (live + verified):
- Duplicate /a/{id} now 301-redirect to their canonical twin instead of 404
(a hard 404 silently dropped already-indexed URLs and tanked impressions).
- Dedup representative selection reworked: accepted/serveable -> established
rep (URL stability) -> quality score, so an accepted page never retires to a
rejected rep and an indexed canonical doesn't churn when a newer twin arrives.
- HEAD /a/{id} returns the same status as GET (api_route GET+HEAD) instead of
falling through to the static mount and 404ing.
- `dedup --force-recluster`: cycle-locked, model-free re-cluster to re-apply the
policy to the existing corpus (shared cycle_lock context manager).
- CLI honors GOODNEWS_DB for its default --db (was silently ignored).
Publishing Desk (admin tool to post highlights to X via Web Intents):
- publishing.py queue/rank/handle-resolution; admin UI; full searchable emoji
picker (bundled data, no CDN) for the blurb editor.
Play games + site:
- Bloom (word-wheel), Memory Match, daily ritual set, Zen Den (dev-gated).
- English-only language gate; source prospecting; paywall + dedup hardening.
Tests: full suite green (349). Ignores tightened (node_modules, data/*.db).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,50 @@
|
||||
// Color Match palette — a hand-curated set of calm-but-distinct colors. The board
|
||||
// builder picks a perceptually-spaced subset (see board.js) so two near-identical
|
||||
// shades never land on the same board. Each color is NAMED (drives the aria-label
|
||||
// and the optional colorblind "assist" glyph), and varies in LIGHTNESS as well as
|
||||
// hue so confusable hue pairs (e.g. green/teal) still differ by brightness.
|
||||
//
|
||||
// `assist` is a short letter shown in the corner when the player turns assist on —
|
||||
// kept to 1–2 chars, unique enough to disambiguate the swatch without reading as
|
||||
// a full label. Default play is pure color; assist is opt-in.
|
||||
|
||||
const RAW = [
|
||||
{ key: 'color-rose', name: 'Rose', hex: '#d76a86', assist: 'Ro' },
|
||||
{ key: 'color-coral', name: 'Coral', hex: '#e07a52', assist: 'Co' },
|
||||
{ key: 'color-amber', name: 'Amber', hex: '#e3a32f', assist: 'Am' },
|
||||
{ key: 'color-gold', name: 'Gold', hex: '#cdb63c', assist: 'Go' },
|
||||
{ key: 'color-lime', name: 'Lime', hex: '#8fb24a', assist: 'Li' },
|
||||
{ key: 'color-green', name: 'Green', hex: '#4a9b5c', assist: 'Gr' },
|
||||
{ key: 'color-teal', name: 'Teal', hex: '#2f9c8e', assist: 'Te' },
|
||||
{ key: 'color-cyan', name: 'Cyan', hex: '#3aa6c4', assist: 'Cy' },
|
||||
{ key: 'color-sky', name: 'Sky', hex: '#5b8fd4', assist: 'Sk' },
|
||||
{ key: 'color-blue', name: 'Blue', hex: '#4f63c6', assist: 'Bl' },
|
||||
{ key: 'color-indigo', name: 'Indigo', hex: '#6a59b2', assist: 'In' },
|
||||
{ key: 'color-violet', name: 'Violet', hex: '#9460ba', assist: 'Vi' },
|
||||
{ key: 'color-plum', name: 'Plum', hex: '#b25a95', assist: 'Pl' },
|
||||
{ key: 'color-brown', name: 'Brown', hex: '#9c6b45', assist: 'Br' },
|
||||
{ key: 'color-sand', name: 'Sand', hex: '#cdb38c', assist: 'Sa' },
|
||||
{ key: 'color-slate', name: 'Slate', hex: '#5d6b78', assist: 'Sl' },
|
||||
{ key: 'color-charcoal', name: 'Charcoal', hex: '#3a4250', assist: 'Ch' },
|
||||
{ key: 'color-cream', name: 'Cream', hex: '#e6dfca', assist: 'Cm' },
|
||||
];
|
||||
|
||||
function srgbToLin(c) { c /= 255; return c <= 0.04045 ? c / 12.92 : Math.pow((c + 0.055) / 1.055, 2.4); }
|
||||
function hexToLab(hex) {
|
||||
const r = srgbToLin(parseInt(hex.slice(1, 3), 16));
|
||||
const g = srgbToLin(parseInt(hex.slice(3, 5), 16));
|
||||
const b = srgbToLin(parseInt(hex.slice(5, 7), 16));
|
||||
const X = r * 0.4124 + g * 0.3576 + b * 0.1805;
|
||||
const Y = r * 0.2126 + g * 0.7152 + b * 0.0722;
|
||||
const Z = r * 0.0193 + g * 0.1192 + b * 0.9505;
|
||||
const f = (t) => (t > 0.008856 ? Math.cbrt(t) : 7.787 * t + 16 / 116);
|
||||
const fx = f(X / 0.95047), fy = f(Y / 1), fz = f(Z / 1.08883);
|
||||
return [116 * fy - 16, 500 * (fx - fy), 200 * (fy - fz)];
|
||||
}
|
||||
|
||||
// CIE76 ΔE — adequate for "are these two swatches obviously different".
|
||||
export function deltaE(a, b) { return Math.hypot(a[0] - b[0], a[1] - b[1], a[2] - b[2]); }
|
||||
|
||||
export const COLORS = RAW.map((c) => ({ ...c, lab: hexToLab(c.hex) }));
|
||||
export const COLOR_BY_KEY = Object.fromEntries(COLORS.map((c) => [c.key, c]));
|
||||
export const COLOR_KEYS = COLORS.map((c) => c.key);
|
||||
Reference in New Issue
Block a user