tracker
All repositories: gitoria
13.2 KB
// theme/theme.hl — THE WORLDAPI THEME: how WorldAPI makes the black/white components of components.hybriel.worldapi.org// look for itself. A styles file (hl:web): an app inherits it from its own styles.hl and sets its accent and type colours://// inherit './theme/theme.hl'// import { var } from 'hl:web/css'// import { purple, yellow } from './shared/tokens.hl'// colorAccent = var(purple)// colorType1 = var(yellow)//// What is in here, in this order:// 1. layout tokens and the type-colour slots (the palette and the semantic roles are shared/tokens.hl)// 2. plain HTML elements: text, links, buttons, fields, foldables, drop zones, tables, pills// 3. the components of components.hybriel.worldapi.org, themed WITHOUT forking them: every rule repeats the component's// own selector with `html ` in front, so it wins over the component's Style (which comes later in the one stylesheet)// and never emits the same selector twice (hl:web refuses that)// The shells (header, sidebar, content-first, list-first, three-pane) carry their own Style in theme/shell/*.hl.//// Design rules (docs/design-rules.md): borderless — a border only where it means something (foldables, drop zones,// focus); no boxed cards, no grey frames; colours only through the named tokens; titles white; mobile first, wider// layouts in `@media (min-width: 60rem)` (the one breakpoint of the theme, also used by theme/shell/*.hl).import { var } from 'hl:web/css'import { yellow, greenDark, purple, blueDark, orange, blueLight, colorBackground, colorBackgroundSunken, colorSurface, colorBorder, colorText, colorTextStrong, colorTextMuted, colorSuccess, colorWarning, colorDanger, colorAccent, colorAccentText } from '../shared/tokens.hl'// ---- 1. tokens -------------------------------------------------------------------------------------------------// the type colours: an app names what each slot means (tracker: 1 Movie, 2 Series, 3 Show, 4 Genre, 5 Person) and may// point a slot at another palette colour in its styles.hl (`colorType1 = var(yellow)`)colorType1 = var(yellow)colorType2 = var(greenDark)colorType3 = var(purple)colorType4 = var(blueDark)colorType5 = var(orange)colorType6 = var(blueLight)// the layout: the centred content column (header contents end flush with it), the side gutter, the header height, the// sidebar and list-pane widths of the shells, the corner radiuslayoutColumn = var('72rem')layoutGutter = var('1rem')layoutHeader = var('3.25rem')layoutSidebar = var('16rem')layoutList = var('24rem')layoutRadius = var('.4rem')// ---- 2. plain elements -----------------------------------------------------------------------------------------'*,*::before,*::after' { boxSizing = 'border-box' }html { colorScheme = 'dark' scrollbarColor = colorBorder + ' transparent' }body { margin = '0' minHeight = '100dvh' font = '16px/1.5 system-ui, -apple-system, "Segoe UI", sans-serif' color = colorText background = colorBackground overflowWrap = 'anywhere' }'::selection' { color = colorAccentText background = colorAccent }'[hidden]' { display = 'none !important' }h1 { margin = '0 0 1rem' fontSize = '1.6rem' lineHeight = '1.25' color = colorTextStrong }h2 { margin = '1.5rem 0 .6rem' fontSize = '1.25rem' lineHeight = '1.3' color = colorTextStrong }h3 { margin = '1.25rem 0 .5rem' fontSize = '1.05rem' lineHeight = '1.3' color = colorTextStrong }h4 { margin = '1rem 0 .4rem' fontSize = '1rem' color = colorTextStrong }p { margin = '0 0 1rem' }strong { color = colorTextStrong }small { fontSize = '.85rem' }'.muted' { color = colorTextMuted }'p.empty' { color = colorTextMuted fontStyle = 'italic' }'.visually-hidden' { position = 'absolute' width = '1px' height = '1px' margin = '-1px' padding = '0' overflow = 'hidden' clip = 'rect(0 0 0 0)' whiteSpace = 'nowrap' border = '0' }ul { margin = '0 0 1rem' paddingLeft = '1.25rem' }ol { margin = '0 0 1rem' paddingLeft = '1.5rem' }li { margin = '.15rem 0' }hr { height = '1px' margin = '1.5rem 0' border = '0' background = colorSurface }img { maxWidth = '100%' height = 'auto' }a { color = colorAccent textDecoration = 'none' textUnderlineOffset = '.2em' }'a:hover' { textDecoration = 'underline' }':focus-visible' { outline = '2px solid ' + colorAccent outlineOffset = '2px' }'code,kbd,samp' { padding = '.05rem .3rem' font = '.9em ui-monospace, SFMono-Regular, Menlo, Consolas, monospace' background = colorSurface borderRadius = '.25rem' }pre { margin = '0 0 1rem' padding = '.75rem 1rem' overflow = 'auto' font = '.9rem/1.5 ui-monospace, SFMono-Regular, Menlo, Consolas, monospace' background = colorBackgroundSunken borderRadius = layoutRadius }'pre code' { padding = '0' background = 'transparent' }// buttons: quiet by default (text on nothing, a surface on hover); `.primary` (and a form's submit) is the accent;// `.danger` says it destroys something — never decoration'button,.button' { display = 'inline-flex' alignItems = 'center' justifyContent = 'center' gap = '.4rem' minHeight = '2.25rem' padding = '.4rem .9rem' font = 'inherit' fontWeight = '600' lineHeight = '1.2' color = colorText textDecoration = 'none' background = 'transparent' border = '0' borderRadius = layoutRadius cursor = 'pointer' }'button:hover,.button:hover' { textDecoration = 'none' background = colorSurface }'button.primary,.button.primary,button[type="submit"]' { color = colorAccentText background = colorAccent }'button.primary:hover,.button.primary:hover,button[type="submit"]:hover' { background = colorAccent filter = 'brightness(1.1)' }'button.danger,.button.danger' { color = colorDanger }'button:disabled' { opacity = '.45' cursor = 'default' filter = 'none' }'button svg,.button svg' { flex = 'none' width = '1.25rem' height = '1.25rem' }// fields: a sunken surface, no frame; the focus ring is the accent; an invalid field gets the danger ringlabel { display = 'block' marginBottom = '.25rem' fontWeight = '600' color = colorTextStrong }'input,select,textarea' { maxWidth = '100%' padding = '.5rem .7rem' font = 'inherit' color = colorText background = colorBackgroundSunken border = '0' borderRadius = layoutRadius }'input:focus,select:focus,textarea:focus' { outline = '2px solid ' + colorAccent outlineOffset = '0' }'input[aria-invalid="true"],textarea[aria-invalid="true"]' { outline = '2px solid ' + colorDanger outlineOffset = '0' }'input[type="checkbox"],input[type="radio"]' { width = '1.1rem' height = '1.1rem' padding = '0' accentColor = colorAccent }'input::placeholder,textarea::placeholder' { color = colorTextMuted }textarea { width = '100%' minHeight = '6rem' resize = 'vertical' }fieldset { margin = '0 0 1rem' padding = '0' border = '0' }legend { padding = '0' marginBottom = '.5rem' fontWeight = '600' color = colorTextStrong }'form p' { margin = '0 0 1rem' }progress { accentColor = colorAccent }// FOLDABLES: the border belongs to the foldable (the creator likes it on the tracker's season collapsibles)details { margin = '0 0 .75rem' border = '1px solid ' + colorBorder borderRadius = layoutRadius }summary { padding = '.6rem .9rem' fontWeight = '600' color = colorTextStrong cursor = 'pointer' }'summary:hover' { background = colorSurface }'details[open] > summary' { borderBottom = '1px solid ' + colorBorder }'details > :not(summary)' { marginLeft = '.9rem' marginRight = '.9rem' }'details > summary + *' { marginTop = '.75rem' }'details > :last-child:not(summary)' { marginBottom = '.75rem' }// DROP ZONES: a dashed border says "drop here"; `.over` while something is dragged over it'drop-zone,.drop-zone' { display = 'block' padding = '1.25rem' textAlign = 'center' color = colorTextMuted border = '2px dashed ' + colorBorder borderRadius = layoutRadius }'drop-zone.over,.drop-zone.over' { color = colorText borderColor = colorAccent background = colorSurface }// tables: no grid; the header row is muted, a row lights up under the pointertable { width = '100%' margin = '0 0 1rem' borderCollapse = 'collapse' }th { padding = '.4rem .6rem' fontSize = '.85rem' fontWeight = '600' textAlign = 'left' color = colorTextMuted }td { padding = '.5rem .6rem' verticalAlign = 'top' }'tbody tr:nth-child(odd)' { background = colorBackgroundSunken }'tbody tr:hover' { background = colorSurface }// type colours and pills: `class="type-2"` colours a text with its slot; `.pill` turns it into an inverted label (text on// the colour); a pill without a type is the accent. Status words use the semantic roles: `.success`, `.warning`, `.danger`'.type-1' { color = colorType1 }'.type-2' { color = colorType2 }'.type-3' { color = colorType3 }'.type-4' { color = colorType4 }'.type-5' { color = colorType5 }'.type-6' { color = colorType6 }'.success' { color = colorSuccess }'.warning' { color = colorWarning }'.danger' { color = colorDanger }'.pill' { display = 'inline-block' padding = '.1rem .55rem' fontSize = '.8rem' fontWeight = '700' lineHeight = '1.5' color = colorAccentText background = colorAccent borderRadius = '1rem' whiteSpace = 'nowrap' }'.pill.type-1' { color = colorAccentText background = colorType1 }'.pill.type-2' { color = colorAccentText background = colorType2 }'.pill.type-3' { color = colorAccentText background = colorType3 }'.pill.type-4' { color = colorAccentText background = colorType4 }'.pill.type-5' { color = colorAccentText background = colorType5 }'.pill.type-6' { color = colorAccentText background = colorType6 }'.pill.success' { color = colorAccentText background = colorSuccess }'.pill.warning' { color = colorAccentText background = colorWarning }'.pill.danger' { color = colorAccentText background = colorDanger }'.pill.quiet' { color = colorText background = colorSurface }// ---- 3. the components of components.hybriel.worldapi.org --------------------------------------------------------// Their own Style draws frames around buttons, fields, editors and day cells. The theme takes the decorative ones away// (the drop zone's dashed border and the focus rings stay — they mean something) and lets surfaces separate instead.// pagination'html pagination button' { border = '0' }'html pagination li.current button' { border = '0' }// modal: the dimmed page around the window is the separation'html modal-dialog > dialog' { background = colorSurface border = '0' boxShadow = '0 1rem 3rem ' + colorBackgroundSunken }'html modal-dialog header' { borderBottom = '0' }'html modal-dialog modal-body footer' { background = colorSurface borderTop = '0' }'html modal-dialog button:hover' { background = colorBackground }// month view: the day cells are tiles with a gap, no grid lines'html month-view ol' { gap = '.25rem' }'html month-view button' { border = '0' }'html #month-days li' { border = '0' borderRadius = '.3rem' }// date picker + date range picker: borderless field, the popup is a raised surface'html date-picker input' { border = '0' }'html range-picker input' { border = '0' }'html date-picker input[aria-invalid="true"]' { outline = '2px solid ' + colorDanger outlineOffset = '-2px' }'html range-picker input[aria-invalid="true"]' { outline = '2px solid ' + colorDanger outlineOffset = '-2px' }'html date-picker month-view' { background = colorSurface border = '0' boxShadow = '0 .75rem 2rem ' + colorBackgroundSunken }'html range-picker month-view' { background = colorSurface border = '0' boxShadow = '0 .75rem 2rem ' + colorBackgroundSunken }'html date-picker button' { border = '0' }'html range-picker button' { border = '0' }'html date-picker month-view ol' { gap = '.2rem' }'html range-picker month-view ol' { gap = '.2rem' }'html date-picker ol.days li' { background = colorBackground border = '0' borderRadius = '.3rem' }'html range-picker ol.days li' { background = colorBackground border = '0' borderRadius = '.3rem' }'html date-picker ol.days li.outside' { background = 'transparent' }'html range-picker ol.days li.outside' { background = 'transparent' }'html date-picker ol.days li.selected' { background = colorAccent }'html range-picker ol.days li.selected' { background = colorAccent }'html range-picker ol.days li.between' { background = colorBackgroundSunken outline = '0' }'html range-picker ol.days li.between time' { color = colorAccent }// file upload: the drop zone keeps its dashed border (it is a drop zone); the choose button is a plain button'html file-upload label.choose' { fontWeight = '600' border = '0' borderRadius = layoutRadius }'html file-upload label.choose:hover' { color = colorTextStrong filter = 'brightness(1.15)' }// Markdown editor: one sunken surface, the toolbar a surface strip; no lines between the parts'html md-editor' { border = '0' borderRadius = layoutRadius }'html md-editor nav' { borderBottom = '0' borderRadius = layoutRadius + ' ' + layoutRadius + ' 0 0' }'html md-editor ul.files' { borderBottom = '0' }'html md-editor link-form' { borderBottom = '0' background = colorSurface }'html md-editor link-form input' { border = '0' }'html md-editor link-form button' { border = '0' }'html md-editor article pre' { border = '0' }'html md-editor footer' { borderTop = '0' }'html md-editor footer button' { border = '0' }// code editor: the gutter is a surface beside the sunken text, no line between them'html code-editor' { border = '0' borderRadius = layoutRadius }'html code-editor code-gutter' { borderRight = '0' }
Branches
- mainmain branch
Latest commits
- 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
- 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
- 96ba683adeploy.sh: a gate without a 'passed,' line (check-theme) no longer ends the scriptmre
- eb3b9205tracker: report 031mre
- 9b5d2e89tracker mission 031: README (What it does, Test: four gates + the #32 checks, Files: theme/, new pages), STATUS (real copy, A/B load, how to repeat, open points), LOGmre
- 39950e4ctracker#32 (mission 031): the WorldAPI theme (theme/ vendored verbatim from layouts.worldapi.org 85b5654; styles.hl inherits it: accent green-dark, type colours 1-6; own base/header rules, row lines, genre-pill and inverted-button frames removed, the season foldable keeps its line; check-theme 21 -> 0, 4th deploy gate; main actions class primary) and the #32 header (theme AppHeader/MainMenu/UserMenu/Sidebar/ContentFirst: desktop brand, search, Series|Shows|Movies|Genres|People, user icon with Unwatched..Settings, Logout; signed out the ident selector, phone the iD icon dropdown; phone menu in the sidebar overlay; marked entry by :has); /find -> /search/<q>, /genres, /people(/<letter>), /settings; main { ContentFirst { slot } } works around the hl:web one-line slot bug; gates 365/0, 32/0, 52/0, check-theme 0mre
- a386dc92tracker: reports 029 + 030mre
- 71e0fd7dtracker missions 029 + 030: README (What it does, Files, gate count), STATUS (real-copy numbers, how to repeat, open points), LOGmre
- d36ea6eatracker#34 + #35 (mission 030): Follow directly under the poster, as wide as the poster (show.hl, styles.hl); the status pill next to a series' title — TVmaze's status (new tvmazeStatus, stored by the sync's TVmaze merge) else TMDB's, TVmaze Ended + TMDB Canceled = Canceled, inverted (filled, dark text, no border), green running / yellow pending / red canceled / muted ended (shows.hl statusOf); the daily delta asks TVmaze's status of an unfollowed series TVmaze's change list names (dailysync.hl syncRunStep, sync.hl syncTvmazeStatus); the status backfill after the details repair (backfill.hl, jobs.hl statusTick; resumable, 550 ms per TVmaze request); gates 354/0, 32/0, 52/0mre
- 7d7d4487tracker#33 (mission 029): reduced titles — every title TMDB's details never went through this app (no detailsAt, no tmdbSync) is incomplete (shows.hl isIncomplete; the old tracker's migrated rows passed #26's test: 5,697 non-adult on the live copy, 691 series without seasons); the repair job does the visibly reduced first (shows.hl missingParts), the page completes one on open; a title TMDB has no poster for (The Remaining) shows the placeholder; tools/count-incomplete.hl; gate fixtures stand for synced titles (tmdbSync), tests/seed-reduced.hl + #33 checks; gates 347/0, 32/0, 52/0mre
- 661c2592tracker: report 028mre
- 27c916fatracker mission 028: README ("Code order", the new file map), STATUS (counts before/after, tests, how to repeat, open), LOGmre
- d924f398tracker mission 028: comments name the new files (sync.hl, dailysync.hl, backfill.hl, credits.hl, jobs.hl, images.hl …); tools/ref-params.py + tools/lambda-audit.py also scan lib/ (they globbed the root only), lambda-audit counts a plain `x = p` alias like `let x = p`mre
- 2e89b968tracker mission 028 (code order) 5/5 let: `let` only where a variable is reassigned — 667 never-reassigned lets became plain declarations (project.hl, lib/, components/, tools/, tests/); kept: 264 in loop bodies (a plain declaration there is 'Cannot reassign' on the 2nd pass), 234 reassigned, 27 whose name is also a member/outer/free name (a plain write would rebind it); tools/let-audit.py decides and fixes (README 'Code order'); tests/realdata-m028.{sh,mjs} = the page-output diff on a real copy; gates 342/0, 32/0, 52/0, real-copy pages identicalmre
- 54796ff2tracker mission 028 (code order) 4/5 thin faces + last copies: the show page's check/follow faces call lib/watches.hl toggleWatched / toggleSeasonWatched (seasonAllWatched moved there) and lib/follows.hl toggleFollowed; both logins (header selector face, /login/callback) share lib/users.hl userOfCode; todayStr/listOf copies in components and the export readers copied into tools/migrate.hl + tools/old-short-ids.hl now once (lib/util.hl, lib/export.hl); gates 342/0, 32/0, 52/0; old-short-ids output byte-identical, migrate output identicalmre
- 06b078e3tracker mission 028 (code order) 3/5 project.hl is the map: config, routes, wiring and a feature → file index (914 → 258 lines); the background jobs (daily sync run, backfills, details repair, credits job, merge, short ids, collection seed) moved unchanged into lib/jobs.hl (a class: their state is reassigned every step, a static cannot be; one instance made after the server), the login callback into lib/users.hl, poster/photo serving into lib/images.hl, the /shows/<slug> rule into lib/shows.hl showsMovedPath; route handlers are thin wrappers; gates 342/0, 32/0, 52/0, real-copy pages identicalmre
- 94716fd2tracker mission 028 (code order) 2/5 util + topics: lib/util.hl holds envOr, storageDir, postersDir, profilesDir, newId, hexDigits, todayStr, dateOr, textOr, hasId, listOr, firstOf, sortDesc once (were copied into up to 5 files); tmdbsync.hl split into tmdb.hl (TMDB/TVmaze requests), sync.hl (one title's sync), sync-helpers.hl, backfill.hl; details.hl split into details.hl, credits.hl, credits-helpers.hl (isIncomplete to shows.hl); search-helpers.hl (words, query, ranking, slugs); collections.hl (the TMDB collection seed, out of franchises.hl); deltasync.hl renamed dailysync.hl; no behaviour change: gates 342/0, 32/0, 52/0, real-copy pages identicalmre