gitoriaLog in with ident

tracker

All repositories: gitoria

ReadmeCodePull requestsReleasesTicketsSettings
Branchmain5cb85d75deploy.sh: a backup taken while a background job writes (tar exit 1) is a warning; archive checked with gzip -tmremain/theme/theme.hl

14.3 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
  34. layoutColumn = var('72rem')
  35. layoutGutter = var('1rem')
  36. layoutHeader = var('3.25rem')
  37. layoutSidebar = var('16rem')
  38. layoutList = var('24rem')
  39. // the corner radii — exactly two (docs/design-rules.md "Corner radius"): radiusSmall for every small item (buttons, pills,
  40. // fields, tags, menu items, chips, day cells), radiusLarge for everything else that is rounded (panels, modals, popovers,
  41. // dropdowns, foldables, tiles/posters); '50%' only for round things (avatars, the check circle). check-theme flags any other
  42. radiusSmall = var('5px')
  43. radiusLarge = var('10px')
  44. // ---- 2. plain elements -----------------------------------------------------------------------------------------
  45. '*,
  46. *::before,
  47. *::after' { boxSizing = 'border-box' }
  48. html { colorScheme = 'dark' scrollbarColor = colorBorder + ' transparent' }
  49. body { margin = '0' minHeight = '100dvh' font = '16px/1.5 system-ui, -apple-system, "Segoe UI", sans-serif' color = colorText background = colorBackground overflowWrap = 'anywhere' }
  50. '::selection' { color = colorAccentText background = colorAccent }
  51. '[hidden]' { display = 'none !important' }
  52. h1 { margin = '0 0 1rem' fontSize = '1.6rem' lineHeight = '1.25' color = colorTextStrong }
  53. h2 { margin = '1.5rem 0 .6rem' fontSize = '1.25rem' lineHeight = '1.3' color = colorTextStrong }
  54. h3 { margin = '1.25rem 0 .5rem' fontSize = '1.05rem' lineHeight = '1.3' color = colorTextStrong }
  55. h4 { margin = '1rem 0 .4rem' fontSize = '1rem' color = colorTextStrong }
  56. p { margin = '0 0 1rem' }
  57. strong { color = colorTextStrong }
  58. small { fontSize = '.85rem' }
  59. '.muted' { color = colorTextMuted }
  60. 'p.empty' { color = colorTextMuted fontStyle = 'italic' }
  61. '.visually-hidden' { position = 'absolute' width = '1px' height = '1px' margin = '-1px' padding = '0' overflow = 'hidden' clip = 'rect(0 0 0 0)' whiteSpace = 'nowrap' border = '0' }
  62. ul { margin = '0 0 1rem' paddingLeft = '1.25rem' }
  63. ol { margin = '0 0 1rem' paddingLeft = '1.5rem' }
  64. li { margin = '.15rem 0' }
  65. hr { height = '1px' margin = '1.5rem 0' border = '0' background = colorSurface }
  66. img { maxWidth = '100%' height = 'auto' }
  67. a { color = colorAccent textDecoration = 'none' textUnderlineOffset = '.2em' }
  68. 'a:hover' { textDecoration = 'underline' }
  69. ':focus-visible' { outline = '2px solid ' + colorAccent outlineOffset = '2px' }
  70. 'code,
  71. kbd,
  72. samp' { padding = '.05rem .3rem' font = '.9em ui-monospace, SFMono-Regular, Menlo, Consolas, monospace' background = colorSurface borderRadius = radiusSmall }
  73. 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 }
  74. 'pre code' { padding = '0' background = 'transparent' }
  75. // buttons: quiet by default (text on nothing, a surface on hover); `.primary` (and a form's submit) is the accent;
  76. // `.danger` says it destroys something — never decoration
  77. 'button,
  78. .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' }
  79. 'button:hover,
  80. .button:hover' { textDecoration = 'none' background = colorSurface }
  81. 'button.primary,
  82. .button.primary,
  83. button[type="submit"]' { color = colorAccentText background = colorAccent }
  84. 'button.primary:hover,
  85. .button.primary:hover,
  86. button[type="submit"]:hover' { background = colorAccent filter = 'brightness(1.1)' }
  87. 'button.danger,
  88. .button.danger' { color = colorDanger }
  89. 'button:disabled' { opacity = '.45' cursor = 'default' filter = 'none' }
  90. 'button svg,
  91. .button svg' { flex = 'none' width = '1.25rem' height = '1.25rem' }
  92. // fields: a sunken surface, no frame; the focus ring is the accent; an invalid field gets the danger ring
  93. label { display = 'block' marginBottom = '.25rem' fontWeight = '600' color = colorTextStrong }
  94. 'input,
  95. select,
  96. textarea' { maxWidth = '100%' padding = '.5rem .7rem' font = 'inherit' color = colorText background = colorBackgroundSunken border = '0' borderRadius = radiusSmall }
  97. 'input:focus,
  98. select:focus,
  99. textarea:focus' { outline = '2px solid ' + colorAccent outlineOffset = '0' }
  100. 'input[aria-invalid="true"],
  101. textarea[aria-invalid="true"]' { outline = '2px solid ' + colorDanger outlineOffset = '0' }
  102. 'input[type="checkbox"],
  103. input[type="radio"]' { width = '1.1rem' height = '1.1rem' padding = '0' accentColor = colorAccent }
  104. 'input::placeholder,
  105. textarea::placeholder' { color = colorTextMuted }
  106. textarea { width = '100%' minHeight = '6rem' resize = 'vertical' }
  107. fieldset { margin = '0 0 1rem' padding = '0' border = '0' }
  108. legend { padding = '0' marginBottom = '.5rem' fontWeight = '600' color = colorTextStrong }
  109. 'form p' { margin = '0 0 1rem' }
  110. progress { accentColor = colorAccent }
  111. // FOLDABLES: the border belongs to the foldable (the creator likes it on the tracker's season collapsibles)
  112. details { margin = '0 0 .75rem' border = '1px solid ' + colorBorder borderRadius = radiusLarge }
  113. summary { padding = '.6rem .9rem' fontWeight = '600' color = colorTextStrong cursor = 'pointer' }
  114. 'summary:hover' { background = colorSurface }
  115. 'details[open] > summary' { borderBottom = '1px solid ' + colorBorder }
  116. 'details > :not(summary)' { marginLeft = '.9rem' marginRight = '.9rem' }
  117. 'details > summary + *' { marginTop = '.75rem' }
  118. 'details > :last-child:not(summary)' { marginBottom = '.75rem' }
  119. // DROP ZONES: a dashed border says "drop here"; `.over` while something is dragged over it
  120. 'drop-zone,
  121. .drop-zone' { display = 'block' padding = '1.25rem' textAlign = 'center' color = colorTextMuted border = '2px dashed ' + colorBorder borderRadius = radiusLarge }
  122. 'drop-zone.over,
  123. .drop-zone.over' { color = colorText borderColor = colorAccent background = colorSurface }
  124. // tables: no grid; the header row is muted, a row lights up under the pointer
  125. table { width = '100%' margin = '0 0 1rem' borderCollapse = 'collapse' }
  126. th { padding = '.4rem .6rem' fontSize = '.85rem' fontWeight = '600' textAlign = 'left' color = colorTextMuted }
  127. td { padding = '.5rem .6rem' verticalAlign = 'top' }
  128. 'tbody tr:nth-child(odd)' { background = colorBackgroundSunken }
  129. 'tbody tr:hover' { background = colorSurface }
  130. // type colours and pills: `class="type-2"` colours a text with its slot; `.pill` turns it into an inverted label (text on
  131. // the colour); a pill without a type is the accent. Status words use the semantic roles: `.success`, `.warning`, `.danger`
  132. '.type-1' { color = colorType1 }
  133. '.type-2' { color = colorType2 }
  134. '.type-3' { color = colorType3 }
  135. '.type-4' { color = colorType4 }
  136. '.type-5' { color = colorType5 }
  137. '.type-6' { color = colorType6 }
  138. '.success' { color = colorSuccess }
  139. '.warning' { color = colorWarning }
  140. '.danger' { color = colorDanger }
  141. '.pill' { display = 'inline-block' padding = '.1rem .55rem' fontSize = '.8rem' fontWeight = '700' lineHeight = '1.5' color = colorAccentText background = colorAccent borderRadius = radiusSmall whiteSpace = 'nowrap' }
  142. '.pill.type-1' { color = colorAccentText background = colorType1 }
  143. '.pill.type-2' { color = colorAccentText background = colorType2 }
  144. '.pill.type-3' { color = colorAccentText background = colorType3 }
  145. '.pill.type-4' { color = colorAccentText background = colorType4 }
  146. '.pill.type-5' { color = colorAccentText background = colorType5 }
  147. '.pill.type-6' { color = colorAccentText background = colorType6 }
  148. '.pill.success' { color = colorAccentText background = colorSuccess }
  149. '.pill.warning' { color = colorAccentText background = colorWarning }
  150. '.pill.danger' { color = colorAccentText background = colorDanger }
  151. '.pill.quiet' { color = colorText background = colorSurface }
  152. // ---- 3. the components of components.hybriel.worldapi.org --------------------------------------------------------
  153. // Their own Style draws frames around buttons, fields, editors and day cells. The theme takes the decorative ones away
  154. // (the drop zone's dashed border and the focus rings stay — they mean something) and lets surfaces separate instead.
  155. // Their corners become the theme's two radii: small items radiusSmall, popups / windows / editors / day tiles radiusLarge.
  156. // pagination
  157. 'html pagination button' { border = '0' borderRadius = radiusSmall }
  158. 'html pagination li.current button' { border = '0' }
  159. // modal: the dimmed page around the window is the separation
  160. 'html modal-dialog > dialog' { background = colorSurface border = '0' borderRadius = radiusLarge boxShadow = '0 1rem 3rem ' + colorBackgroundSunken }
  161. 'html modal-dialog header' { borderBottom = '0' }
  162. 'html modal-dialog modal-body footer' { background = colorSurface borderTop = '0' }
  163. 'html modal-dialog button' { borderRadius = radiusSmall }
  164. 'html modal-dialog button:hover' { background = colorBackground }
  165. // month view: the day cells are tiles with a gap, no grid lines
  166. 'html month-view ol' { gap = '.25rem' }
  167. 'html month-view button' { border = '0' borderRadius = radiusSmall }
  168. 'html #month-days li' { border = '0' borderRadius = radiusLarge }
  169. // date picker + date range picker: borderless field, the popup is a raised surface
  170. 'html date-picker input' { border = '0' borderRadius = radiusSmall }
  171. 'html range-picker input' { border = '0' borderRadius = radiusSmall }
  172. 'html date-picker input[aria-invalid="true"]' { outline = '2px solid ' + colorDanger outlineOffset = '-2px' }
  173. 'html range-picker input[aria-invalid="true"]' { outline = '2px solid ' + colorDanger outlineOffset = '-2px' }
  174. 'html date-picker month-view' { background = colorSurface border = '0' borderRadius = radiusLarge boxShadow = '0 .75rem 2rem ' + colorBackgroundSunken }
  175. 'html range-picker month-view' { background = colorSurface border = '0' borderRadius = radiusLarge boxShadow = '0 .75rem 2rem ' + colorBackgroundSunken }
  176. 'html date-picker button' { border = '0' borderRadius = radiusSmall }
  177. 'html range-picker button' { border = '0' borderRadius = radiusSmall }
  178. 'html date-picker month-view ol' { gap = '.2rem' }
  179. 'html range-picker month-view ol' { gap = '.2rem' }
  180. 'html date-picker ol.days li' { background = colorBackground border = '0' borderRadius = radiusSmall }
  181. 'html range-picker ol.days li' { background = colorBackground border = '0' borderRadius = radiusSmall }
  182. 'html date-picker ol.days li.outside' { background = 'transparent' }
  183. 'html range-picker ol.days li.outside' { background = 'transparent' }
  184. 'html date-picker ol.days li.selected' { background = colorAccent }
  185. 'html range-picker ol.days li.selected' { background = colorAccent }
  186. 'html range-picker ol.days li.between' { background = colorBackgroundSunken outline = '0' }
  187. 'html range-picker ol.days li.between time' { color = colorAccent }
  188. // file upload: the drop zone keeps its dashed border (it is a drop zone); the choose button is a plain button
  189. 'html file-upload drop-zone' { borderRadius = radiusLarge }
  190. 'html file-upload label.choose' { fontWeight = '600' border = '0' borderRadius = radiusSmall }
  191. 'html file-upload label.choose:hover' { color = colorTextStrong filter = 'brightness(1.15)' }
  192. // Markdown editor: one sunken surface, the toolbar a surface strip; no lines between the parts
  193. 'html md-editor' { border = '0' borderRadius = radiusLarge }
  194. 'html md-editor nav' { borderBottom = '0' borderRadius = radiusLarge + ' ' + radiusLarge + ' 0 0' }
  195. 'html md-editor nav button' { borderRadius = radiusSmall }
  196. 'html md-editor ul.files' { borderBottom = '0' }
  197. 'html md-editor link-form' { borderBottom = '0' background = colorSurface }
  198. 'html md-editor link-form input' { border = '0' borderRadius = radiusSmall }
  199. 'html md-editor link-form button' { border = '0' borderRadius = radiusSmall }
  200. 'html md-editor article img' { borderRadius = radiusLarge }
  201. 'html md-editor article code' { borderRadius = radiusSmall }
  202. 'html md-editor article pre' { border = '0' borderRadius = radiusLarge }
  203. 'html md-editor footer' { borderTop = '0' }
  204. 'html md-editor footer button' { border = '0' borderRadius = radiusSmall }
  205. // code editor: the gutter is a surface beside the sunken text, no line between them
  206. 'html code-editor' { border = '0' borderRadius = radiusLarge }
  207. '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