gitoriaLog in with ident

tracker

All repositories: gitoria

ReadmeCodePull requestsReleasesTicketsSettings
Commit65c694a865c694a8tracker#14: search — header magnifier, /search/<text> (in-memory word-prefix index over titles + people), Fetch from web (TMDB search/multi, ours left out), Add = import via syncShow; gate +25 checks, real-data scriptmre65c694a8/styles.hl

14.6 KB

  1. // styles.hl — all of the app's CSS, served as /__hl/app.css. Components carry no Style
  2. // blocks: markup is domain tags, this file lays them out.
  3. //
  4. // TOKENS: the WorldAPI palette + semantic tokens are declared in `shared/tokens.hl` as
  5. // hl:web css variables, vendored verbatim from ident.worldapi.org (see that project's
  6. // README "Design tokens"). This file imports the tokens it uses and writes them as members;
  7. // the app sets only its accent (greenDark, per CONCEPT.md / antcolony README "look and style").
  8. //
  9. // In a styles file a bare name is a TAG (camelCase -> kebab: `emptyState` is `empty-state`),
  10. // a quoted name is a selector written verbatim. MOBILE FIRST (mission 047, the creator: "style is
  11. // supposed to be mobile first"): every rule outside a block is the PHONE's (390 px wide); the
  12. // `@media (min-width: 40rem)` block at the bottom only ADDS what a wider screen changes. Nothing
  13. // here may make a 390 px page scroll sideways, and the header stays ONE row (the gate checks both).
  14. import { var } from 'hl:web/css'
  15. import { greenDark, colorText, colorBackground, colorTextStrong, colorBackgroundSunken, colorTextMuted, colorAccentText, colorBorder, colorDanger, colorSurface, orange, blueDark } from './shared/tokens.hl'
  16. colorAccent = var(greenDark) // the app's accent: the only token this app sets
  17. // tracker.worldapi.org#4: genre pills in blue; artists orange (mission 047, the creator: "orange, not red") — the raw
  18. // palette tokens (like the accent above), not the semantic `colorDanger`/`colorAction` (this isn't a
  19. // warning or an action, just the creator's chosen colour for these two kinds of link/pill)
  20. colorArtist = var(orange)
  21. colorGenre = var(blueDark)
  22. '*,
  23. *::before,
  24. *::after' { boxSizing = 'border-box' }
  25. html { colorScheme = 'dark' }
  26. body { margin = '0' font = '16px/1.5 system-ui, sans-serif' color = colorText background = colorBackground }
  27. a { color = colorAccent textDecoration = 'none' }
  28. 'a:hover' { textDecoration = 'underline' }
  29. h1 { margin = '0 0 .8rem' fontSize = '1.2rem' lineHeight = '1.3' color = colorTextStrong }
  30. button { font = 'inherit' fontWeight = '600' padding = '.4rem .9rem' color = colorAccentText background = colorAccent border = '0' borderRadius = '.4rem' cursor = 'pointer' }
  31. 'button:hover' { filter = 'brightness(1.1)' }
  32. 'button.quiet' { color = colorText background = 'transparent' border = '0' }
  33. // the ONLY buttons with a border are inverted ones (creator: "no borders for the buttons unless they are inverted")
  34. 'button.inverted' { color = colorAccent background = 'transparent' border = '1px solid ' + colorAccent }
  35. 'button.quiet:hover' { background = colorSurface filter = 'none' }
  36. // the header is ONE row on a phone: brand left, ident selector + "Log out" right, never wrapping — the
  37. // user box may shrink (minWidth 0), the brand may not
  38. applicationHeader { position = 'sticky' top = '0' zIndex = '5' display = 'flex' flexWrap = 'nowrap' alignItems = 'center' gap = '.5rem' padding = '.45rem .75rem' background = colorBackgroundSunken }
  39. 'application-header .brand' { flex = 'none' fontWeight = '100' fontSize = '1.15rem' lineHeight = '1.2' color = colorAccent }
  40. main { display = 'block' maxWidth = '60rem' margin = '0 auto' padding = '.8rem .75rem' }
  41. // ---- login (top right of the header), copied from calendar.worldapi.org's styles.hl -------------
  42. userBox { display = 'flex' alignItems = 'center' gap = '.25rem' marginLeft = 'auto' minWidth = '0' }
  43. '#logout' { flex = 'none' padding = '.3rem .5rem' fontSize = '.9rem' }
  44. 'a.button' { display = 'inline-block' padding = '.35rem .8rem' fontWeight = '600' color = colorAccentText background = colorAccent borderRadius = '.4rem' }
  45. 'a.button:hover' { textDecoration = 'none' filter = 'brightness(1.1)' }
  46. identSelector { '--ident-accent' = colorAccent '--ident-accent-text' = colorAccentText '--ident-background' = colorSurface '--ident-border' = colorBorder '--ident-font' = '600 .85rem/1.4 system-ui, sans-serif' minWidth = '0' }
  47. 'ident-selector::part(button)' { padding = '.3rem .6rem' border = 'none' }
  48. 'ident-selector::part(status)' { padding = '.3rem .6rem' border = 'none' }
  49. 'ident-selector::part(identity)' { border = 'none' }
  50. '.message' { color = colorDanger }
  51. '.muted' { color = colorTextMuted }
  52. '.message.banner' { margin = '0' padding = '.5rem .75rem' background = colorBackgroundSunken }
  53. 'p.message:empty' { display = 'none' }
  54. // ---- offline (tracker.worldapi.org#10, components/main.hl): the note under the header, and the probe whose endless
  55. // animation is the shell's 1 s tick (it must be rendered for the animation to run — not display none — so: invisible)
  56. offlineNote { display = 'block' padding = '.5rem .75rem' color = colorTextStrong background = colorSurface }
  57. netProbe { position = 'fixed' left = '0' top = '0' width = '1px' height = '1px' opacity = '0' pointerEvents = 'none' animation = 'tracker-net-tick 1s linear infinite' }
  58. '@keyframes tracker-net-tick' = {
  59. 'from' = { opacity = '0' }
  60. to = { opacity = '0' }
  61. }
  62. // ---- /shows/:slug (tracker.worldapi.org#4) --------------------------------------------------
  63. showPage { display = 'block' }
  64. // phone (mission 047, the creator): the title (+ Follow) first, then the poster at the full content width, then genres,
  65. // plot, cast — `showMeta` dissolves (display contents) so its title row can go before the poster (order -1). From 40rem
  66. // the creator's desktop layout: poster left (10rem), right of it the title and the rest.
  67. showHeader { display = 'flex' flexDirection = 'column' gap = '.6rem' marginBottom = '1.2rem' }
  68. showPoster { display = 'block' width = '100%' }
  69. 'show-poster img' { display = 'block' width = '100%' height = 'auto' borderRadius = '.4rem' aspectRatio = '2 / 3' objectFit = 'cover' background = colorSurface }
  70. showMeta { display = 'contents' overflowWrap = 'anywhere' }
  71. showTitle { order = '-1' display = 'flex' alignItems = 'center' gap = '.6rem' }
  72. 'show-title h1' { flex = '1' minWidth = '0' margin = '0' }
  73. 'button.follow' { flex = 'none' padding = '.3rem .8rem' fontSize = '.9rem' }
  74. 'button.follow.inverted' { padding = 'calc(.3rem - 1px) calc(.8rem - 1px)' }
  75. // tracker.worldapi.org#12: the show on TMDB / IMDb / TheTVDB / TVmaze — small monochrome badges (CSS only, no copied
  76. // logos), muted like the other secondary text, the accent on hover. Phone: right under the title row (order -1, after it
  77. // in the markup), above the full-width poster.
  78. showLinks { order = '-1' display = 'flex' flexWrap = 'wrap' gap = '.4rem' }
  79. 'a.extlink' { display = 'inline-flex' alignItems = 'center' height = '1.5rem' padding = '0 .45rem' borderRadius = '.3rem' font = '700 .75rem/1 system-ui, sans-serif' letterSpacing = '.02em' color = colorBackground background = colorTextMuted }
  80. 'a.extlink:hover' { textDecoration = 'none' background = colorAccent }
  81. showGenres { display = 'flex' flexWrap = 'wrap' gap = '.4rem' margin = '.5rem 0' }
  82. '.genre-pill' { display = 'inline-block' padding = '.15rem .6rem' borderRadius = '1rem' fontSize = '.85rem' color = colorGenre border = '1px solid ' + colorGenre }
  83. showSummary { display = 'block' margin = '.5rem 0' fontSize = '.95rem' color = colorText }
  84. showCast { display = 'block' margin = '.5rem 0' }
  85. '.artist-link' { color = colorArtist }
  86. '.artist-link:not(:last-child)::after' { content = "', '" color = colorTextMuted }
  87. showSeasons { display = 'block' }
  88. seasonRow { display = 'flex' alignItems = 'center' gap = '.6rem' padding = '.6rem 0' borderTop = '1px solid ' + colorBorder }
  89. 'button.season-toggle' { flex = '1' minWidth = '0' display = 'flex' alignItems = 'baseline' gap = '.6rem' justifyContent = 'flex-start' padding = '0' color = colorText background = 'transparent' border = '0' fontWeight = '600' cursor = 'pointer' textAlign = 'left' }
  90. seasonCount { fontWeight = '400' fontSize = '.85rem' color = colorTextMuted }
  91. // the caret at the header's right edge: down while open, turned to the right while collapsed (mission 047)
  92. seasonCaret { flex = 'none' alignSelf = 'center' display = 'grid' marginLeft = 'auto' color = colorTextMuted transition = 'transform .15s' }
  93. 'season-caret svg' { display = 'block' fill = 'none' stroke = 'currentColor' strokeWidth = '2' strokeLinecap = 'round' strokeLinejoin = 'round' }
  94. 'season-row.collapsed season-caret' { transform = 'rotate(-90deg)' }
  95. // episodes are NOT indented (mission 047): an episode's check sits under its season's check
  96. episodeRow { display = 'flex' alignItems = 'center' gap = '.6rem' padding = '.3rem 0' }
  97. episodeNumber { flex = 'none' width = '1.6rem' color = colorTextMuted fontVariantNumeric = 'tabular-nums' }
  98. episodeTitle { flex = '1' minWidth = '0' overflowWrap = 'anywhere' }
  99. // the check: "a disc with a check in it" (drawn in CSS/SVG, no icon font) — solid (filled accent disc, dark check)
  100. // once watched; while not, its INVERSION (mission 047, the creator): an accent outline circle AND an accent outline check
  101. 'button.checkicon' { flex = 'none' display = 'grid' placeItems = 'center' width = '2rem' height = '2rem' padding = '0' background = 'transparent' border = '0' borderRadius = '50%' cursor = 'pointer' }
  102. '.checkicon circle' { fill = 'transparent' stroke = colorAccent strokeWidth = '1.5' }
  103. '.checkicon path' { fill = 'none' stroke = colorAccent strokeWidth = '2' strokeLinecap = 'round' strokeLinejoin = 'round' }
  104. '.checkicon.solid circle' { fill = colorAccent stroke = colorAccent }
  105. '.checkicon.solid path' { stroke = colorAccentText }
  106. // ---- /unwatched (tracker.worldapi.org#5) ----------------------------------------------------
  107. unwatchedPage { display = 'block' }
  108. unwatchedRows { display = 'block' }
  109. // phone: two lines next to the check — show title + date, below it the episode; one line from 40rem
  110. unwatchedRow { display = 'grid' gridTemplateColumns = '2rem minmax(0, 1fr) auto' alignItems = 'center' gap = '0 .6rem' padding = '.5rem 0' borderTop = '1px solid ' + colorBorder }
  111. 'unwatched-row button.checkicon' { gridRow = 'span 2' }
  112. '.unwatched-show' { fontWeight = '600' overflowWrap = 'anywhere' }
  113. unwatchedEpisode { gridColumn = '2 / 4' gridRow = '2' color = colorText overflowWrap = 'anywhere' }
  114. unwatchedDate { gridRow = '1' gridColumn = '3' fontSize = '.85rem' color = colorTextMuted fontVariantNumeric = 'tabular-nums' }
  115. // ---- /schedule (tracker.worldapi.org#6) — same row layout as /unwatched, minus the check icon -----
  116. schedulePage { display = 'block' }
  117. scheduleRows { display = 'block' }
  118. scheduleRow { display = 'grid' gridTemplateColumns = 'minmax(0, 1fr) auto' alignItems = 'center' gap = '0 .6rem' padding = '.5rem 0' borderTop = '1px solid ' + colorBorder }
  119. '.schedule-show' { fontWeight = '600' overflowWrap = 'anywhere' }
  120. scheduleEpisode { gridColumn = '1 / 3' gridRow = '2' color = colorText overflowWrap = 'anywhere' }
  121. scheduleDate { gridRow = '1' gridColumn = '2' fontSize = '.85rem' color = colorTextMuted fontVariantNumeric = 'tabular-nums' }
  122. // ---- /my/shows (tracker.worldapi.org#7) — small poster, title, last watched episode ------------
  123. myShowsPage { display = 'block' }
  124. myShowsRows { display = 'block' }
  125. myShowRow { display = 'flex' alignItems = 'center' gap = '.8rem' padding = '.5rem 0' borderTop = '1px solid ' + colorBorder }
  126. 'img.myshow-poster' { flex = 'none' display = 'block' width = '2.5rem' height = 'auto' aspectRatio = '2 / 3' objectFit = 'cover' borderRadius = '.25rem' background = colorSurface }
  127. '.myshow-title' { flex = '1' minWidth = '0' fontWeight = '600' overflowWrap = 'anywhere' }
  128. myShowEpisode { flex = 'none' fontSize = '.9rem' color = colorTextMuted fontVariantNumeric = 'tabular-nums' }
  129. // ---- the search (tracker.worldapi.org#14) — the header's magnifier and /search ------------------------------------
  130. 'a.searchlink' { flex = 'none' display = 'grid' placeItems = 'center' width = '2rem' height = '2rem' color = colorText borderRadius = '.4rem' }
  131. 'a.searchlink:hover' { color = colorAccent background = colorSurface }
  132. 'a.searchlink svg' { display = 'block' fill = 'none' stroke = 'currentColor' strokeWidth = '2' strokeLinecap = 'round' }
  133. searchPage { display = 'block' }
  134. searchBar { display = 'block' marginBottom = '.8rem' }
  135. '#searchinput' { display = 'block' width = '100%' padding = '.55rem .75rem' font = 'inherit' color = colorTextStrong background = colorSurface border = '0' borderRadius = '.4rem' outline = 'none' }
  136. '#searchinput:focus' { boxShadow = '0 0 0 2px ' + colorAccent }
  137. searchSection { display = 'block' marginBottom = '1rem' }
  138. 'search-section h2' { margin = '.4rem 0 .2rem' fontSize = '.85rem' fontWeight = '600' letterSpacing = '.04em' textTransform = 'uppercase' color = colorTextMuted }
  139. searchRow { display = 'flex' alignItems = 'center' gap = '.8rem' padding = '.4rem 0' borderTop = '1px solid ' + colorBorder }
  140. webRow { display = 'flex' alignItems = 'center' gap = '.8rem' padding = '.4rem 0' borderTop = '1px solid ' + colorBorder }
  141. 'img.search-poster' { flex = 'none' display = 'block' width = '2.5rem' height = 'auto' aspectRatio = '2 / 3' objectFit = 'cover' borderRadius = '.25rem' background = colorSurface }
  142. '.search-title' { flex = '1' minWidth = '0' fontWeight = '600' overflowWrap = 'anywhere' }
  143. '.search-person' { flex = '1' minWidth = '0' color = colorArtist overflowWrap = 'anywhere' }
  144. searchType { flex = 'none' fontSize = '.8rem' color = colorTextMuted }
  145. webText { flex = '1' minWidth = '0' display = 'flex' flexDirection = 'column' }
  146. webTitle { fontWeight = '600' color = colorTextStrong overflowWrap = 'anywhere' }
  147. 'button.add' { flex = 'none' padding = 'calc(.3rem - 1px) calc(.8rem - 1px)' fontSize = '.9rem' }
  148. webBar { display = 'flex' alignItems = 'center' gap = '.8rem' margin = '.6rem 0' }
  149. // ---- wider screens (≥ 40rem = 640 px): only what changes from the phone -------------------------
  150. '@media (min-width: 40rem)' = {
  151. applicationHeader = { gap = '1rem' padding = '.6rem 1rem' }
  152. userBox = { gap = '.5rem' }
  153. '#logout' = { padding = '.4rem .9rem' fontSize = '1rem' }
  154. identSelector = { '--ident-font' = '600 .9rem/1.4 system-ui, sans-serif' }
  155. 'ident-selector::part(button)' = { padding = '.35rem .8rem' }
  156. 'ident-selector::part(status)' = { padding = '.35rem .8rem' }
  157. main = { padding = '1rem' }
  158. h1 = { margin = '0 0 1rem' fontSize = '1.3rem' }
  159. showHeader = { flexDirection = 'row' alignItems = 'flex-start' gap = '1rem' marginBottom = '1.5rem' }
  160. showPoster = { flex = 'none' width = '10rem' }
  161. showMeta = { display = 'block' flex = '1' minWidth = '0' }
  162. showTitle = { marginBottom = '1rem' }
  163. showSummary = { fontSize = '1rem' }
  164. episodeRow = { padding = '.4rem 0' }
  165. unwatchedRow = { display = 'flex' }
  166. '.unwatched-show' = { flex = 'none' }
  167. unwatchedEpisode = { flex = '1' }
  168. unwatchedDate = { flex = 'none' }
  169. scheduleRow = { display = 'flex' }
  170. '.schedule-show' = { flex = 'none' }
  171. scheduleEpisode = { flex = '1' }
  172. scheduleDate = { flex = 'none' }
  173. }

Branches

Latest commits

  • 65c694a8tracker#14: search — header magnifier, /search/<text> (in-memory word-prefix index over titles + people), Fetch from web (TMDB search/multi, ours left out), Add = import via syncShow; gate +25 checks, real-data scriptmre
  • cbdc4ea7tracker#12: link icons TMDB/IMDb/TVDB/TVmaze; sync fills missing ids (TVmaze lookup); movies fetched via /movie/mre
  • b105bcd8tracker#11: Hybriel master ff51cf46 (checks no longer vanish), mobile-first styles, carets, follow button, sign-in modal, inverted check, orange castmre
  • 31b758aatracker#10: installable app (manifest, service worker, offline shell), own icon + faviconmre
  • 2fa9d997tracker#9: TMDB sync (followed shows: seasons, episodes, posters), tools/sync-tmdb.hl + daily run 04:00 UTC, fake TMDB in gatemre
  • 49e1f61edeploy.sh: back up live storage/.sessions/.env before every deploy (newest 5 kept)mre
  • 54070a4etracker#8: /my/unwatched + /my/schedule (301 from old), S01E01, title (year), 1 episode, watched-set lookup (unwatched 15s -> 1s)mre
  • 3251488atracker#7: /my/shows (followed shows, newest follow first, poster, title, last watched SxxEyy); gate can take screenshots (TRACKER_GATE_SHOTS)mre
  • 44b7d9f9tracker#6: /schedule — upcoming episodes of followed shows, soonest firstmre
  • 91c9fc8ctracker#5: /unwatched — unwatched released episodes of followed shows, newest firstmre
  • a97c0295tracker#4: show page /shows/:slug (header, seasons, episodes, watch checks) + tools/relink-episode-seasons.hlmre
  • 05f407c5tracker: no border on any button except inverted ones (Log out, ident status and identities too); header brand weight 100mre
  • 17375427tracker#2: tools/migrate.hl + tools/verify.hl — old MongoDB data into mpackdb with new idsmre
  • 31aac936tracker#3: no border on the header and on filled buttons; inverted buttons keep theirsmre
  • 2ad9d29ctracker#1: login exactly like calendar (identity selector in the header, empty homepage)mre
  • 3691e176tracker#1: empty tracker with the ident login (state of 2026-09-27)mre