tracker
All repositories: gitoria
6.9 KB
#!/usr/bin/env node// theme/check-theme.mjs — THE THEME GATE HELPER an app runs: it flags what bypasses the WorldAPI theme in the app's own code.// node theme/check-theme.mjs [appDir] [--ignore <path prefix>]…// 1. HARD-CODED COLOURS: #hex, rgb()/rgba(), hsl()/hsla(), oklch(), and named colours (white, black, red …) in .hl and .css// files. Colours come from the theme's named tokens (shared/tokens.hl, `colorText`, 'var(--color-text)'). A fallback inside// `var(--token, …)` is allowed (the components of components.hybriel carry those so a copy works without the theme).// 2. DECORATIVE BORDERS: any border / borderTop … / borderColor that is not 0 / none. Allowed only where a border MEANS// something (docs/design-rules.md): on a line about details/summary (foldables), drop zones, focus or an invalid field,// or on a line that says so with the comment `// functional: <why>`.// 3. CORNER RADII: every borderRadius / borderTopLeftRadius … (css: border-radius, border-top-left-radius …) is one of the two// theme radii — radiusSmall (5px: buttons, pills, fields, tags, menu items, chips) or radiusLarge (10px: panels, modals,// popovers, dropdowns, tiles/posters), in css 'var(--radius-small)' / 'var(--radius-large)' — or 0, or '50%' for round// things (avatars, check circles). Each corner of a list counts ('radiusLarge + ' ' + radiusLarge + ' 0 0'' is fine).// Never scanned: plugins/ bin/ node_modules/ storage/ .scratch/ .git/ .sessions/ and shared/tokens.hl (the one place for raw// values). More paths to skip (e.g. a vendored component folder whose frames the theme removes): --ignore, or one prefix per// line in `.check-theme-ignore` in the app folder.// Output: one `file:line: problem — text` per finding, then `check-theme: N files, M problems`; exit 1 when M > 0.import { readFileSync, readdirSync, statSync, existsSync } from 'node:fs';import { join, relative, resolve } from 'node:path';const args = process.argv.slice(2);let dir = '.';const ignore = ['plugins/', 'bin/', 'node_modules/', 'storage/', '.scratch/', '.git/', '.sessions/', 'shared/tokens.hl'];for (let i = 0; i < args.length; i++) {if (args[i] === '--ignore') ignore.push(args[++i]);else dir = args[i];}const APP = resolve(dir);const ignoreFile = join(APP, '.check-theme-ignore');if (existsSync(ignoreFile)) {for (const line of readFileSync(ignoreFile, 'utf8').split('\n')) {const p = line.replace(/#.*/, '').trim();if (p) ignore.push(p);}}function files(d) {const out = [];for (const name of readdirSync(d)) {const full = join(d, name);const rel = relative(APP, full);const st = statSync(full);if (ignore.some(p => rel === p.replace(/\/$/, '') || rel.startsWith(p.endsWith('/') ? p : p + '/') || rel === p)) continue;if (st.isDirectory()) out.push(...files(full));else if (/\.(hl|css)$/.test(name)) out.push(full);}return out;}const NAMED = 'white|black|red|green|blue|gray|grey|yellow|orange|purple|pink|brown|silver|navy|teal|maroon|olive|lime|aqua|fuchsia|cyan|magenta|gold|indigo|violet';const COLOUR = new RegExp(`(#[0-9a-fA-F]{3}(?:[0-9a-fA-F]{3}(?:[0-9a-fA-F]{2})?)?|#[0-9a-fA-F]{4})(?![0-9a-zA-Z_-])|\\b(?:rgba?|hsla?|oklch|oklab|lab|lch|hwb)\\(`);const NAMED_RE = new RegExp(`[=:]\\s*['"]?[^'";]*\\b(${NAMED})\\b`, 'i');// a css declaration that sets a colour: hl (`color = '…'`, `background = …`) or css (`color: …`)const COLOUR_PROP = /\b(color|background(?:Color|-color)?|fill|stroke|borderColor|border-color|outlineColor|outline-color|boxShadow|box-shadow|accentColor|accent-color|caretColor|caret-color|textDecorationColor)\s*[=:]/i;const BORDER = /\b(border(?:Top|Right|Bottom|Left|Block|Inline)?(?:Start|End)?(?:Color|Width|Style)?|border(?:-(?:top|right|bottom|left|block|inline))?(?:-(?:start|end))?(?:-(?:color|width|style))?)\s*([=:])\s*(?:'([^']*)'|"([^"]*)"|([^;}\s]+))/g;const FUNCTIONAL = /details|summary|drop-zone|dropZone|\.drop|:focus|focus-visible|aria-invalid|\/\/\s*functional/i;const NO_BORDER = /^\s*(0|0px|none|0 none|transparent|inherit|initial|unset)\s*$/i;// a radius: hl `borderRadius = <quoted strings / names joined by +>`, css `border-radius: <value>`const RADIUS_NAME = 'border(?:Top|Bottom|Start|End)?(?:Left|Right|Start|End)?Radius|border(?:-(?:top|bottom|start|end)-(?:left|right|start|end))?-radius';const RADIUS_HL = new RegExp(`\\b(${RADIUS_NAME})'?\\s*=\\s*((?:'[^']*'|"[^"]*"|[\\w.$-]+)(?:\\s*\\+\\s*(?:'[^']*'|"[^"]*"|[\\w.$-]+))*)`, 'g');const RADIUS_CSS = new RegExp(`\\b(${RADIUS_NAME})\\s*:\\s*([^;}'"]+)`, 'g');const RADIUS_OK = /^(0|0px|50%|radiusSmall|radiusLarge|var\(--radius-(?:small|large)\)|inherit|initial|unset)$/;// the corner values of one radius that are not the theme's two (a token's fallback `var(--radius-small, 5px)` is fine)function badRadii(value) {const words = value.replace(/var\(\s*--radius-(small|large)\s*(?:,(?:[^()]|\([^()]*\))*)?\)/g, 'var(--radius-$1)').replace(/['"+]/g, ' ').split(/[\s/]+/);return words.filter(w => w && !RADIUS_OK.test(w));}// strip comments (whole-line `//`, trailing ` // …`, /* … */ on one line)function uncomment(line) {const s = line.replace(/\/\*.*?\*\//g, '');if (/^\s*\/\//.test(s)) return '';return s.replace(/\s\/\/\s.*$/, '');}// … and the allowed var(--x, fallback) fallbacksfunction code(line) {return uncomment(line).replace(/var\(\s*--[\w-]+\s*,\s*(?:[^()]|\([^()]*\))*\)/g, 'var()');}let problems = 0;const list = files(APP);for (const f of list) {const lines = readFileSync(f, 'utf8').split('\n');lines.forEach((raw, i) => {const s = code(raw);if (!s.trim()) return;const where = `${relative(APP, f)}:${i + 1}`;const m = s.match(COLOUR);if (m && !/^\s*import\b/.test(s)) { problems++; console.log(`${where}: hard-coded colour '${m[0]}' — use a theme token — ${raw.trim().slice(0, 140)}`); }else if (COLOUR_PROP.test(s)) {const n = s.match(NAMED_RE);if (n && !/currentColor|transparent/i.test(n[0].slice(-12))) { problems++; console.log(`${where}: named colour '${n[1]}' — use a theme token — ${raw.trim().slice(0, 140)}`); }}for (const b of s.matchAll(BORDER)) {const value = (b[3] ?? b[4] ?? b[5] ?? '').trim();if (b[1].toLowerCase().includes('radius')) continue;if (NO_BORDER.test(value)) continue;if (FUNCTIONAL.test(raw)) continue;problems++;console.log(`${where}: decorative border '${b[1]} ${b[2]} ${value || '…'}' — borderless unless it means something (docs/design-rules.md), else mark it '// functional: why' — ${raw.trim().slice(0, 140)}`);}const plain = uncomment(raw);for (const r of [...plain.matchAll(RADIUS_HL), ...plain.matchAll(RADIUS_CSS)]) {const bad = badRadii(r[2]);if (!bad.length) continue;problems++;console.log(`${where}: corner radius '${r[1]} ${r[2].trim()}' — only radiusSmall (5px, small items), radiusLarge (10px, panels, popups, tiles), '50%' for round things — ${raw.trim().slice(0, 140)}`);}});}console.log(`check-theme: ${list.length} files, ${problems} problems`);process.exit(problems ? 1 : 0);
Branches
- mainmain branch
Latest commits
- 5cb85d75deploy.sh: a backup taken while a background job writes (tar exit 1) is a warning; archive checked with gzip -tmre
- 9abda75dtracker: report 035mre
- 12595e47mission 035: theme re-vendored from layouts.worldapi.org 0222f67 (two corner radii: radiusSmall 5px, radiusLarge 10px); the tracker's 18 own radii -> radiusSmall/radiusLarge (--layout-radius is gone); check-theme 0; gates 379/0, 32/0, 53/0, 229/0, 26/0; real copy: every computed radius in {0, 5px, 10px, 50%}mre
- e7305014tracker: report 032 (art + photos)mre
- fbb903cctracker#33/#37 (mission 032): a title without a TMDB poster gets its backdrop (w780, posterFromBackdrop, shown 2:3 centre-cropped); movies store runtime, the page shows release date + runtime; art backfill (public titles without poster file / movies without runtime) and person photo backfill (tmdbProfile / photoCheck) as the last start jobs (TRACKER_ART, TRACKER_PHOTOS; off in every gate start); gates 379/0, 32/0, 53/0, 229/0, 26/0, check-theme 0; live copy: art 4977 titles in 42 min (115 posters, 15 backdrops, 4609 runtimes), The Remaining shows its backdrop + 7 minmre
- 4ecc67b2tracker: STATUS/LOG for the t38 + t40 merge (gates 374/0, 32/0, 53/0, 229/0, 26/0, check-theme 0; live copy checks)mre
- e5945d2fMerge t40 (tracker#40 curated franchises, Franchises menu, superseded collections) into main: deploy.sh lists all six gates (browser, kinds, franchises, pager, franchiseseed, check-theme); search.hl keeps #37's personPhotoOf + #40's importWithCredits; pager gate runs with TRACKER_FRANCHISE_SEED=0; gates 374/0, 32/0, 53/0, 229/0, 26/0, check-theme 0mre
- b638e99dMerge t38 (tracker#38 pagination, #35 Returning/Airing label) into main: LOG/STATUS keep both sides; pager gate follows #37's /people (everyone, last updated first: seed updatedAt); gates pager 229/0, browser 374/0mre
- f8ffa4dbtracker: report 032 + The Remaining + #39mre
- 7565a863tracker: LOG timemre
- b10f00c8tracker#39: double episodes — migrated episodes whose TMDB id TMDB replaced are adopted by their number in the sync (old id -> migratedTmdbId); merge.hl step 3 merges each season's doubles at start (keeper: most watches > synced > first; watches moved/parked; tombstones into mergedEpisodes, nothing deleted); tools/count-duplicate-episodes.hl; gate fixture + paths-m039; live copy 850 -> 0 in 64 s; gates 373/0, 32/0, 52/0mre
- 8f1d4542tracker#40: superseded collections — a TMDB collection timeline whose titles are all in one curated timeline is hidden (supersededBy; kept: own page + editor finder), set by the collection seed when it makes one and by the curated build (lifted when the cover is gone); partly covered ones join that franchise; no second widget (First Contact: only Star Trek — Prime); gate franchiseseed 26/0, browser 365/0, kinds 32/0, franchises 53/0, check-theme 0; real copy 24 supersededmre
- 9448d643tracker#35 follow-up (mission 033): TVmaze 'Running' is labelled 'Returning', or 'Airing' while a non-special episode of the two newest seasons is released within today +-7 days (data unchanged); gate fixtures Running/Airing/Aired + a special; browser 366/0, kinds 32/0, franchises 52/0, pager 229/0, check-theme 0mre
- 7d4b293dtracker#40: "Franchises" in the main menu (desktop header after People, phone sidebar) → /franchises, marked on franchise and timeline pages; gates 365/0, 32/0, 53/0, 24/0, check-theme 0mre
- 8751adb8tracker: report 032mre
- 9bce1f65tracker mission 032: STATUS gate files + the hour-boundary flakemre
- 718bfb89tracker#37 (mission 032): /people = everyone, last updated first (updatedAt stamped by the person fill; view built at boot, touched people first at once), photo + name tiles (person colour) with the /movies pagination, /people/<letter> removed; photo = our file, tmdbProfile, a cast/crew entry's profile (in-memory map at boot), else the new 'no photo' placeholder; new cast/crew/created_by people keep tmdbProfile; search people rows with the photo; /settings = the heading only; util.hl sortDesc starts from sorted runs (same result, 105k: 1.6 s -> 0.15 s); gates 369/0, 32/0, 52/0, check-theme 0; README/STATUS/LOGmre
- 93dfb0batracker#40 (mission 034): the curated franchises — data/franchises.json (17 franchises, 31 timelines, 285 TMDB titles, movies + series, in-universe/release order, 12 TMDB collections attached); lib/franchiseseed.hl + jobs.hl franchiseSeedTick (last start job, imports missing titles via details.hl importWithCredits = the search's Add, one per step paced, then one build; franchiseseed.db: editor changes win, the creator's same-name franchise adopted / timeline left alone, 404 remembered, resumable, idempotent); timeline heads 'N titles · in-universe order' (orderKind) and wrap on a phone; series pages show the widget; new gate tests/franchiseseed.mjs (5th in deploy.sh), the others run with TRACKER_FRANCHISE_SEED=0; gates 365/0, 32/0, 52/0, 24/0, check-theme 0; real copy 196 imported, 0 failed, 7 min, restart unchanged=31mre
- 03ec792ftracker#38 (mission 033): pagination goes exactly to the clicked page — tilelist read the clicked button's text after pagination.hl's own handler had rebuilt the buttons (real clicks only); now li.current, else the button's own text; new gate tests/pager.mjs (5 lists x 11 pages, 390/1280, real + script clicks, Back/Forward) 229/0; browser 365/0, kinds 32/0, franchises 52/0, check-theme 0mre
- 96ba683adeploy.sh: a gate without a 'passed,' line (check-theme) no longer ends the scriptmre