gitoriaLog in with ident

tracker

All repositories: gitoria

ReadmeCodePull requestsReleasesTicketsSettings
Commit7d4b293d7d4b293dtracker#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 0mre7d4b293d/theme/theme.hl

13.2 KB

  1. // theme/theme.hl — THE WORLDAPI THEME: how WorldAPI makes the black/white components of components.hybriel.worldapi.org
  2. // look for itself. A styles file (hl:web): an app inherits it from its own styles.hl and sets its accent and type colours:
  3. //
  4. // inherit './theme/theme.hl'
  5. // import { var } from 'hl:web/css'
  6. // import { purple, yellow } from './shared/tokens.hl'
  7. // colorAccent = var(purple)
  8. // colorType1 = var(yellow)
  9. //
  10. // What is in here, in this order:
  11. // 1. layout tokens and the type-colour slots (the palette and the semantic roles are shared/tokens.hl)
  12. // 2. plain HTML elements: text, links, buttons, fields, foldables, drop zones, tables, pills
  13. // 3. the components of components.hybriel.worldapi.org, themed WITHOUT forking them: every rule repeats the component's
  14. // own selector with `html ` in front, so it wins over the component's Style (which comes later in the one stylesheet)
  15. // and never emits the same selector twice (hl:web refuses that)
  16. // The shells (header, sidebar, content-first, list-first, three-pane) carry their own Style in theme/shell/*.hl.
  17. //
  18. // Design rules (docs/design-rules.md): borderless — a border only where it means something (foldables, drop zones,
  19. // focus); no boxed cards, no grey frames; colours only through the named tokens; titles white; mobile first, wider
  20. // layouts in `@media (min-width: 60rem)` (the one breakpoint of the theme, also used by theme/shell/*.hl).
  21. import { var } from 'hl:web/css'
  22. import { yellow, greenDark, purple, blueDark, orange, blueLight, colorBackground, colorBackgroundSunken, colorSurface, colorBorder, colorText, colorTextStrong, colorTextMuted, colorSuccess, colorWarning, colorDanger, colorAccent, colorAccentText } from '../shared/tokens.hl'
  23. // ---- 1. tokens -------------------------------------------------------------------------------------------------
  24. // the type colours: an app names what each slot means (tracker: 1 Movie, 2 Series, 3 Show, 4 Genre, 5 Person) and may
  25. // point a slot at another palette colour in its styles.hl (`colorType1 = var(yellow)`)
  26. colorType1 = var(yellow)
  27. colorType2 = var(greenDark)
  28. colorType3 = var(purple)
  29. colorType4 = var(blueDark)
  30. colorType5 = var(orange)
  31. colorType6 = var(blueLight)
  32. // the layout: the centred content column (header contents end flush with it), the side gutter, the header height, the
  33. // sidebar and list-pane widths of the shells, the corner radius
  34. layoutColumn = var('72rem')
  35. layoutGutter = var('1rem')
  36. layoutHeader = var('3.25rem')
  37. layoutSidebar = var('16rem')
  38. layoutList = var('24rem')
  39. layoutRadius = var('.4rem')
  40. // ---- 2. plain elements -----------------------------------------------------------------------------------------
  41. '*,
  42. *::before,
  43. *::after' { boxSizing = 'border-box' }
  44. html { colorScheme = 'dark' scrollbarColor = colorBorder + ' transparent' }
  45. body { margin = '0' minHeight = '100dvh' font = '16px/1.5 system-ui, -apple-system, "Segoe UI", sans-serif' color = colorText background = colorBackground overflowWrap = 'anywhere' }
  46. '::selection' { color = colorAccentText background = colorAccent }
  47. '[hidden]' { display = 'none !important' }
  48. h1 { margin = '0 0 1rem' fontSize = '1.6rem' lineHeight = '1.25' color = colorTextStrong }
  49. h2 { margin = '1.5rem 0 .6rem' fontSize = '1.25rem' lineHeight = '1.3' color = colorTextStrong }
  50. h3 { margin = '1.25rem 0 .5rem' fontSize = '1.05rem' lineHeight = '1.3' color = colorTextStrong }
  51. h4 { margin = '1rem 0 .4rem' fontSize = '1rem' color = colorTextStrong }
  52. p { margin = '0 0 1rem' }
  53. strong { color = colorTextStrong }
  54. small { fontSize = '.85rem' }
  55. '.muted' { color = colorTextMuted }
  56. 'p.empty' { color = colorTextMuted fontStyle = 'italic' }
  57. '.visually-hidden' { position = 'absolute' width = '1px' height = '1px' margin = '-1px' padding = '0' overflow = 'hidden' clip = 'rect(0 0 0 0)' whiteSpace = 'nowrap' border = '0' }
  58. ul { margin = '0 0 1rem' paddingLeft = '1.25rem' }
  59. ol { margin = '0 0 1rem' paddingLeft = '1.5rem' }
  60. li { margin = '.15rem 0' }
  61. hr { height = '1px' margin = '1.5rem 0' border = '0' background = colorSurface }
  62. img { maxWidth = '100%' height = 'auto' }
  63. a { color = colorAccent textDecoration = 'none' textUnderlineOffset = '.2em' }
  64. 'a:hover' { textDecoration = 'underline' }
  65. ':focus-visible' { outline = '2px solid ' + colorAccent outlineOffset = '2px' }
  66. 'code,
  67. kbd,
  68. samp' { padding = '.05rem .3rem' font = '.9em ui-monospace, SFMono-Regular, Menlo, Consolas, monospace' background = colorSurface borderRadius = '.25rem' }
  69. 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 }
  70. 'pre code' { padding = '0' background = 'transparent' }
  71. // buttons: quiet by default (text on nothing, a surface on hover); `.primary` (and a form's submit) is the accent;
  72. // `.danger` says it destroys something — never decoration
  73. 'button,
  74. .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' }
  75. 'button:hover,
  76. .button:hover' { textDecoration = 'none' background = colorSurface }
  77. 'button.primary,
  78. .button.primary,
  79. button[type="submit"]' { color = colorAccentText background = colorAccent }
  80. 'button.primary:hover,
  81. .button.primary:hover,
  82. button[type="submit"]:hover' { background = colorAccent filter = 'brightness(1.1)' }
  83. 'button.danger,
  84. .button.danger' { color = colorDanger }
  85. 'button:disabled' { opacity = '.45' cursor = 'default' filter = 'none' }
  86. 'button svg,
  87. .button svg' { flex = 'none' width = '1.25rem' height = '1.25rem' }
  88. // fields: a sunken surface, no frame; the focus ring is the accent; an invalid field gets the danger ring
  89. label { display = 'block' marginBottom = '.25rem' fontWeight = '600' color = colorTextStrong }
  90. 'input,
  91. select,
  92. textarea' { maxWidth = '100%' padding = '.5rem .7rem' font = 'inherit' color = colorText background = colorBackgroundSunken border = '0' borderRadius = layoutRadius }
  93. 'input:focus,
  94. select:focus,
  95. textarea:focus' { outline = '2px solid ' + colorAccent outlineOffset = '0' }
  96. 'input[aria-invalid="true"],
  97. textarea[aria-invalid="true"]' { outline = '2px solid ' + colorDanger outlineOffset = '0' }
  98. 'input[type="checkbox"],
  99. input[type="radio"]' { width = '1.1rem' height = '1.1rem' padding = '0' accentColor = colorAccent }
  100. 'input::placeholder,
  101. textarea::placeholder' { color = colorTextMuted }
  102. textarea { width = '100%' minHeight = '6rem' resize = 'vertical' }
  103. fieldset { margin = '0 0 1rem' padding = '0' border = '0' }
  104. legend { padding = '0' marginBottom = '.5rem' fontWeight = '600' color = colorTextStrong }
  105. 'form p' { margin = '0 0 1rem' }
  106. progress { accentColor = colorAccent }
  107. // FOLDABLES: the border belongs to the foldable (the creator likes it on the tracker's season collapsibles)
  108. details { margin = '0 0 .75rem' border = '1px solid ' + colorBorder borderRadius = layoutRadius }
  109. summary { padding = '.6rem .9rem' fontWeight = '600' color = colorTextStrong cursor = 'pointer' }
  110. 'summary:hover' { background = colorSurface }
  111. 'details[open] > summary' { borderBottom = '1px solid ' + colorBorder }
  112. 'details > :not(summary)' { marginLeft = '.9rem' marginRight = '.9rem' }
  113. 'details > summary + *' { marginTop = '.75rem' }
  114. 'details > :last-child:not(summary)' { marginBottom = '.75rem' }
  115. // DROP ZONES: a dashed border says "drop here"; `.over` while something is dragged over it
  116. 'drop-zone,
  117. .drop-zone' { display = 'block' padding = '1.25rem' textAlign = 'center' color = colorTextMuted border = '2px dashed ' + colorBorder borderRadius = layoutRadius }
  118. 'drop-zone.over,
  119. .drop-zone.over' { color = colorText borderColor = colorAccent background = colorSurface }
  120. // tables: no grid; the header row is muted, a row lights up under the pointer
  121. table { width = '100%' margin = '0 0 1rem' borderCollapse = 'collapse' }
  122. th { padding = '.4rem .6rem' fontSize = '.85rem' fontWeight = '600' textAlign = 'left' color = colorTextMuted }
  123. td { padding = '.5rem .6rem' verticalAlign = 'top' }
  124. 'tbody tr:nth-child(odd)' { background = colorBackgroundSunken }
  125. 'tbody tr:hover' { background = colorSurface }
  126. // type colours and pills: `class="type-2"` colours a text with its slot; `.pill` turns it into an inverted label (text on
  127. // the colour); a pill without a type is the accent. Status words use the semantic roles: `.success`, `.warning`, `.danger`
  128. '.type-1' { color = colorType1 }
  129. '.type-2' { color = colorType2 }
  130. '.type-3' { color = colorType3 }
  131. '.type-4' { color = colorType4 }
  132. '.type-5' { color = colorType5 }
  133. '.type-6' { color = colorType6 }
  134. '.success' { color = colorSuccess }
  135. '.warning' { color = colorWarning }
  136. '.danger' { color = colorDanger }
  137. '.pill' { display = 'inline-block' padding = '.1rem .55rem' fontSize = '.8rem' fontWeight = '700' lineHeight = '1.5' color = colorAccentText background = colorAccent borderRadius = '1rem' whiteSpace = 'nowrap' }
  138. '.pill.type-1' { color = colorAccentText background = colorType1 }
  139. '.pill.type-2' { color = colorAccentText background = colorType2 }
  140. '.pill.type-3' { color = colorAccentText background = colorType3 }
  141. '.pill.type-4' { color = colorAccentText background = colorType4 }
  142. '.pill.type-5' { color = colorAccentText background = colorType5 }
  143. '.pill.type-6' { color = colorAccentText background = colorType6 }
  144. '.pill.success' { color = colorAccentText background = colorSuccess }
  145. '.pill.warning' { color = colorAccentText background = colorWarning }
  146. '.pill.danger' { color = colorAccentText background = colorDanger }
  147. '.pill.quiet' { color = colorText background = colorSurface }
  148. // ---- 3. the components of components.hybriel.worldapi.org --------------------------------------------------------
  149. // Their own Style draws frames around buttons, fields, editors and day cells. The theme takes the decorative ones away
  150. // (the drop zone's dashed border and the focus rings stay — they mean something) and lets surfaces separate instead.
  151. // pagination
  152. 'html pagination button' { border = '0' }
  153. 'html pagination li.current button' { border = '0' }
  154. // modal: the dimmed page around the window is the separation
  155. 'html modal-dialog > dialog' { background = colorSurface border = '0' boxShadow = '0 1rem 3rem ' + colorBackgroundSunken }
  156. 'html modal-dialog header' { borderBottom = '0' }
  157. 'html modal-dialog modal-body footer' { background = colorSurface borderTop = '0' }
  158. 'html modal-dialog button:hover' { background = colorBackground }
  159. // month view: the day cells are tiles with a gap, no grid lines
  160. 'html month-view ol' { gap = '.25rem' }
  161. 'html month-view button' { border = '0' }
  162. 'html #month-days li' { border = '0' borderRadius = '.3rem' }
  163. // date picker + date range picker: borderless field, the popup is a raised surface
  164. 'html date-picker input' { border = '0' }
  165. 'html range-picker input' { border = '0' }
  166. 'html date-picker input[aria-invalid="true"]' { outline = '2px solid ' + colorDanger outlineOffset = '-2px' }
  167. 'html range-picker input[aria-invalid="true"]' { outline = '2px solid ' + colorDanger outlineOffset = '-2px' }
  168. 'html date-picker month-view' { background = colorSurface border = '0' boxShadow = '0 .75rem 2rem ' + colorBackgroundSunken }
  169. 'html range-picker month-view' { background = colorSurface border = '0' boxShadow = '0 .75rem 2rem ' + colorBackgroundSunken }
  170. 'html date-picker button' { border = '0' }
  171. 'html range-picker button' { border = '0' }
  172. 'html date-picker month-view ol' { gap = '.2rem' }
  173. 'html range-picker month-view ol' { gap = '.2rem' }
  174. 'html date-picker ol.days li' { background = colorBackground border = '0' borderRadius = '.3rem' }
  175. 'html range-picker ol.days li' { background = colorBackground border = '0' borderRadius = '.3rem' }
  176. 'html date-picker ol.days li.outside' { background = 'transparent' }
  177. 'html range-picker ol.days li.outside' { background = 'transparent' }
  178. 'html date-picker ol.days li.selected' { background = colorAccent }
  179. 'html range-picker ol.days li.selected' { background = colorAccent }
  180. 'html range-picker ol.days li.between' { background = colorBackgroundSunken outline = '0' }
  181. 'html range-picker ol.days li.between time' { color = colorAccent }
  182. // file upload: the drop zone keeps its dashed border (it is a drop zone); the choose button is a plain button
  183. 'html file-upload label.choose' { fontWeight = '600' border = '0' borderRadius = layoutRadius }
  184. 'html file-upload label.choose:hover' { color = colorTextStrong filter = 'brightness(1.15)' }
  185. // Markdown editor: one sunken surface, the toolbar a surface strip; no lines between the parts
  186. 'html md-editor' { border = '0' borderRadius = layoutRadius }
  187. 'html md-editor nav' { borderBottom = '0' borderRadius = layoutRadius + ' ' + layoutRadius + ' 0 0' }
  188. 'html md-editor ul.files' { borderBottom = '0' }
  189. 'html md-editor link-form' { borderBottom = '0' background = colorSurface }
  190. 'html md-editor link-form input' { border = '0' }
  191. 'html md-editor link-form button' { border = '0' }
  192. 'html md-editor article pre' { border = '0' }
  193. 'html md-editor footer' { borderTop = '0' }
  194. 'html md-editor footer button' { border = '0' }
  195. // code editor: the gutter is a surface beside the sunken text, no line between them
  196. 'html code-editor' { border = '0' borderRadius = layoutRadius }
  197. 'html code-editor code-gutter' { borderRight = '0' }

Branches

Latest commits

  • 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
  • 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
  • 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
  • 186079b0tracker mission 028 (code order) 1/5 move: every root .hl except project.hl into lib/ (styles.hl into components/), import paths only; gates 342/0, 32/0, 52/0; real-copy pages identicalmre
  • 4f47f181tracker: report 027mre
  • dc1d4be4tracker mission 027: Hybriel master 06617221 vendored (plugin allocator fixes 3a781359 + 413f60e4); real copy RSS through first-start jobs + 400 loads flat ~2.55 GB (190aa11d 2.3 -> 5.6 GB), page times <= 1.1x; gates 342/0, 32/0, 52/0mre