tracker
All repositories: gitoria
14.3 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 shellslayoutColumn = var('72rem')layoutGutter = var('1rem')layoutHeader = var('3.25rem')layoutSidebar = var('16rem')layoutList = var('24rem')// the corner radii — exactly two (docs/design-rules.md "Corner radius"): radiusSmall for every small item (buttons, pills,// fields, tags, menu items, chips, day cells), radiusLarge for everything else that is rounded (panels, modals, popovers,// dropdowns, foldables, tiles/posters); '50%' only for round things (avatars, the check circle). check-theme flags any otherradiusSmall = var('5px')radiusLarge = var('10px')// ---- 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 = radiusSmall }pre { margin = '0 0 1rem' padding = '.75rem 1rem' overflow = 'auto' font = '.9rem/1.5 ui-monospace, SFMono-Regular, Menlo, Consolas, monospace' background = colorBackgroundSunken borderRadius = radiusLarge }'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 = radiusSmall 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 = radiusSmall }'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 = radiusLarge }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 = radiusLarge }'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 = radiusSmall 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.// Their corners become the theme's two radii: small items radiusSmall, popups / windows / editors / day tiles radiusLarge.// pagination'html pagination button' { border = '0' borderRadius = radiusSmall }'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' borderRadius = radiusLarge 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' { borderRadius = radiusSmall }'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' borderRadius = radiusSmall }'html #month-days li' { border = '0' borderRadius = radiusLarge }// date picker + date range picker: borderless field, the popup is a raised surface'html date-picker input' { border = '0' borderRadius = radiusSmall }'html range-picker input' { border = '0' borderRadius = radiusSmall }'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' borderRadius = radiusLarge boxShadow = '0 .75rem 2rem ' + colorBackgroundSunken }'html range-picker month-view' { background = colorSurface border = '0' borderRadius = radiusLarge boxShadow = '0 .75rem 2rem ' + colorBackgroundSunken }'html date-picker button' { border = '0' borderRadius = radiusSmall }'html range-picker button' { border = '0' borderRadius = radiusSmall }'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 = radiusSmall }'html range-picker ol.days li' { background = colorBackground border = '0' borderRadius = radiusSmall }'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 drop-zone' { borderRadius = radiusLarge }'html file-upload label.choose' { fontWeight = '600' border = '0' borderRadius = radiusSmall }'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 = radiusLarge }'html md-editor nav' { borderBottom = '0' borderRadius = radiusLarge + ' ' + radiusLarge + ' 0 0' }'html md-editor nav button' { borderRadius = radiusSmall }'html md-editor ul.files' { borderBottom = '0' }'html md-editor link-form' { borderBottom = '0' background = colorSurface }'html md-editor link-form input' { border = '0' borderRadius = radiusSmall }'html md-editor link-form button' { border = '0' borderRadius = radiusSmall }'html md-editor article img' { borderRadius = radiusLarge }'html md-editor article code' { borderRadius = radiusSmall }'html md-editor article pre' { border = '0' borderRadius = radiusLarge }'html md-editor footer' { borderTop = '0' }'html md-editor footer button' { border = '0' borderRadius = radiusSmall }// code editor: the gutter is a surface beside the sunken text, no line between them'html code-editor' { border = '0' borderRadius = radiusLarge }'html code-editor code-gutter' { borderRight = '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