4
0

build-themes.py 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308
  1. #!/usr/bin/env python3
  2. """Generate the modern pisg themes (layout/<name>.css) from one shared stylesheet.
  3. pisg inlines exactly one CSS file into the page, so every theme must be self-contained. The
  4. shared rules live here once and each theme is just a palette.
  5. Design: ink on cool paper, and colour reserved for data. The interface is monochrome; the four
  6. time-of-day colours (night violet, morning yellow, afternoon aqua, evening orange) are the only
  7. hues on the page besides one series colour. They were checked with the dataviz palette validator
  8. in light, dark and on pure black (colour-blind separation, contrast, lightness band), so they
  9. can be trusted. Nicks and figures are set in monospace, like an IRC client. Sections are
  10. separated by a hairline and space, not boxed.
  11. Usage: python3 layout/build-themes.py (rewrites the generated .css files)
  12. Add a theme: add an entry to THEMES below and re-run, then use <set ColorScheme="yourtheme">.
  13. The old themes (default, darkred, ocean, ...) are untouched.
  14. """
  15. from pathlib import Path
  16. out_dir = Path(__file__).resolve().parent
  17. # Palette keys
  18. # bg page | surface panels (map canvas, menu) | fg text | muted secondary text
  19. # line hairlines | line2 stronger rules | hover row hover | mark single-series colour
  20. # t0..t3 hours 0-5 / 6-11 / 12-17 / 18-23 | male / female nick colours | shadow menu shadow
  21. LIGHT = dict(bg="#f6f7f9", surface="#ffffff", fg="#171b21", muted="#59626d", line="#e0e3e8",
  22. line2="#c6ccd4", hover="#eceff3", mark="#2a78d6",
  23. t0="#4a3aa7", t1="#eda100", t2="#1baf7a", t3="#eb6834",
  24. male="#2a5fb0", female="#b03a72",
  25. shadow="0 18px 32px -14px rgba(20,24,30,.28)")
  26. DARK = dict(bg="#0f1318", surface="#171c23", fg="#e8ebef", muted="#98a2ae", line="#252c35",
  27. line2="#38414c", hover="#1b2129", mark="#3987e5",
  28. t0="#9085e9", t1="#c98500", t2="#199e70", t3="#d95926",
  29. male="#7aa7ea", female="#e58ab5",
  30. shadow="0 18px 32px -14px rgba(0,0,0,.6)")
  31. SANS = 'system-ui, -apple-system, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif'
  32. MONO = 'ui-monospace, "SF Mono", "Cascadia Mono", "JetBrains Mono", Menlo, Consolas, "Liberation Mono", monospace'
  33. THEMES = {
  34. # Follows the visitor's OS setting: light by day, dark by night.
  35. "modern": dict(desc="Ink on paper, colour only for data; follows the visitor's light/dark setting",
  36. scheme="light dark", light=LIGHT, dark=DARK, sans=SANS, mono=MONO),
  37. "midnight": dict(desc="Blue-black, always dark", scheme="dark", light=DARK, dark=None,
  38. sans=SANS, mono=MONO),
  39. "amoled": dict(desc="Pure black, always dark", scheme="dark", dark=None,
  40. light=dict(DARK, bg="#000000", surface="#0a0b0d", line="#1c2027", line2="#2c323b",
  41. hover="#101216", shadow="0 18px 32px -14px rgba(0,0,0,.9)"),
  42. sans=SANS, mono=MONO),
  43. "terminal": dict(desc="Everything in monospace, green on black, like a console",
  44. scheme="dark", dark=None,
  45. light=dict(DARK, bg="#060a07", surface="#0b120d", fg="#b9f3c4", muted="#6ea77a",
  46. line="#17301c", line2="#24492b", hover="#0f1a12", mark="#3ddc84",
  47. male="#7fd6ff", female="#ff9ad0"),
  48. sans=MONO, mono=MONO),
  49. }
  50. KEYS = ["bg", "surface", "fg", "muted", "line", "line2", "hover", "mark",
  51. "t0", "t1", "t2", "t3", "male", "female", "shadow"]
  52. def vars_block(p, indent=" "):
  53. return "\n".join(f"{indent}--{k}: {p[k]};" for k in KEYS)
  54. BASE = r"""
  55. /* pisg output is table markup, so these rules restyle the existing classes (headtext, hicell,
  56. tdtop, rankc ...). The generator wraps each section in <div class="card" id="...">. */
  57. html { -webkit-text-size-adjust: 100%; scroll-behavior: smooth; }
  58. body {
  59. margin: 0;
  60. padding: 32px 24px 96px;
  61. background: var(--bg);
  62. color: var(--fg);
  63. font-family: __SANS__;
  64. font-size: 15px;
  65. line-height: 1.55;
  66. }
  67. body > div { max-width: 1040px; margin: 0 auto; text-align: left; } /* pisg centres it with align=center */
  68. /* Fixed 925/929px table widths are attributes, so plain CSS overrides them. */
  69. table { border-collapse: separate; border-spacing: 0; max-width: 100%; }
  70. table[width] { width: 100%; }
  71. table[width="520"] { width: auto; margin: 10px 0 0; }
  72. td { color: var(--fg); font-family: inherit; font-size: 14px; text-align: left; padding: 9px 10px; vertical-align: middle; overflow-wrap: anywhere; } /* long URLs and nicks wrap instead of forcing a scrollbar */
  73. a, a:link, a:visited { color: inherit; font-weight: 600; text-decoration: underline; text-decoration-color: var(--line2); text-underline-offset: 3px; }
  74. a:hover { text-decoration-color: currentColor; }
  75. a:focus-visible, summary:focus-visible, button:focus-visible { outline: 2px solid var(--fg); outline-offset: 2px; }
  76. a.background, a.background:link, a.background:visited { color: var(--muted); font-weight: 500; }
  77. /* ---- Masthead: reads like an IRC header, "#channel @ network" ---- */
  78. .title, #pagetitle1 {
  79. margin: 4px 0 8px;
  80. font-family: __MONO__;
  81. font-size: clamp(24px, 4.6vw, 40px);
  82. line-height: 1.15;
  83. font-weight: 700;
  84. letter-spacing: -0.035em;
  85. color: var(--fg);
  86. overflow-wrap: anywhere;
  87. }
  88. .subtitle { margin: 0 0 18px; color: var(--muted); font-size: 14px; }
  89. .subtitle b { color: var(--fg); font-weight: 600; }
  90. /* ---- Sections: a rule and space, no boxes ---- */
  91. .card { margin: 56px 0 0; padding: 20px 0 0; border-top: 1px solid var(--line2); overflow-x: auto; text-align: left; scroll-margin-top: 64px; }
  92. .card > br { display: none; }
  93. .pisg-nav + .card { margin-top: 28px; border-top: 0; padding-top: 0; }
  94. #overview .headlinebg { position: absolute; width: 1px; height: 1px; overflow: hidden; clip: rect(0 0 0 0); } /* the sentence below is its headline; the menu still links here */
  95. .card > table:first-of-type { margin-bottom: 8px; }
  96. .headlinebg { background: none; padding: 0; }
  97. .headtext { margin: 0; padding: 0; border: 0; background: none; color: var(--fg);
  98. font-size: 20px; line-height: 1.25; font-weight: 650; letter-spacing: -0.01em; text-align: left; }
  99. .intro { margin: 2px 0 16px; max-width: 68ch; color: var(--muted); }
  100. /* ---- Wide screens: a menu down the left, the page fills the rest ---- */
  101. body { container-type: inline-size; }
  102. .nav-side { display: none; }
  103. @media (min-width: 1000px) {
  104. body { padding-left: 272px; padding-right: 28px; }
  105. body > div { max-width: 1720px; }
  106. .pisg-nav { box-sizing: border-box; position: fixed; top: 0; left: 0; bottom: 0; width: 232px; margin: 0; padding: 26px 12px 24px 24px;
  107. border: 0; border-right: 1px solid var(--line2); background: var(--bg); overflow-y: auto; z-index: 30; }
  108. .nav-menu { display: none; }
  109. .nav-side { display: block; }
  110. .nav-brand, .nav-brand:link, .nav-brand:visited { display: block; margin: 0 0 18px; font-family: __MONO__; font-size: 18px; font-weight: 700;
  111. letter-spacing: -0.02em; text-decoration: none; overflow-wrap: anywhere; }
  112. .nav-side-list { margin: 0; padding: 0; list-style: none; }
  113. .nav-side-list a, .nav-side-list a:link, .nav-side-list a:visited { display: block; margin-left: -10px; padding: 6px 10px; border-left: 2px solid transparent;
  114. color: var(--muted); font-size: 14px; font-weight: 500; text-decoration: none; }
  115. .nav-side-list a:hover { color: var(--fg); background: var(--hover); }
  116. .nav-side-list a.active { color: var(--fg); font-weight: 650; border-left-color: var(--mark); }
  117. .card { scroll-margin-top: 24px; }
  118. .card td:not(.headtext):not(.headlinebg) { padding-left: 8px; padding-right: 8px; } /* the 10-column nick table just fits beside the menu */
  119. .pisg-nav + .card { margin-top: 20px; }
  120. }
  121. /* Two short sections share a row when the page is wide enough (measured on the page, so it also holds inside a frame). */
  122. @container (min-width: 1080px) {
  123. body > div { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); column-gap: 56px; grid-auto-flow: dense; }
  124. body > div > * { grid-column: 1 / -1; }
  125. body > div > .card.half { grid-column: auto; }
  126. }
  127. /* Way back to the landing page (option HomeLink) */
  128. .homebar { margin: 0 0 14px; text-align: left; }
  129. .homebtn, .homebtn:link, .homebtn:visited { display: inline-flex; align-items: center; gap: 6px; padding: 5px 12px; border: 1px solid var(--line2); border-radius: 6px;
  130. background: var(--surface); color: var(--fg); font-size: 13px; font-weight: 600; line-height: 1.3; text-decoration: none; }
  131. .homebtn:hover { background: var(--hover); border-color: var(--fg); }
  132. /* Column titles */
  133. .tdtop { background: none; color: var(--muted); font-size: 11px; font-weight: 600; letter-spacing: .08em;
  134. text-transform: uppercase; border-bottom: 1px solid var(--line2); padding: 8px 10px; }
  135. .tdtop { white-space: nowrap; } /* column titles stay on one line; the table scrolls instead */
  136. .tdtop b { font-weight: 600; }
  137. /* Rows: one hairline each */
  138. .hicell { background: none; border-bottom: 1px solid var(--line); border-radius: 0; padding: 11px 10px; }
  139. .hicell10 { background: none; font-size: 12px; border-bottom: 1px solid var(--line); }
  140. tr:last-child > .hicell { border-bottom: 0; }
  141. /* pisg writes an inline colour gradient on the top-nicks rows (HiCell/HiCell2); the theme owns colour. */
  142. td[style*="background-color"] { background: transparent !important; border-bottom: 1px solid var(--line); }
  143. tr:hover > td[style*="background-color"], tr:hover > .rankc, tr:hover > .hirankc { background: var(--hover) !important; }
  144. .rankc, .hirankc, .rankc10 { background: transparent; color: var(--muted); font-family: __MONO__; font-size: 12px;
  145. font-weight: 500; text-align: center; border-bottom: 1px solid var(--line); }
  146. .hirankc { color: var(--fg); font-weight: 700; }
  147. .rankc, .hirankc, .rankc10 { white-space: nowrap; }
  148. .rankc10 { text-align: left; }
  149. /* nicks are set in monospace, as in an IRC client */
  150. .rankc + td, .hirankc + td { font-family: __MONO__; font-weight: 600; }
  151. .small, .asmall { color: var(--muted); font-family: inherit; font-size: 12px; }
  152. .asmall { text-align: center; }
  153. /* ---- Bar charts (marks: thin, 4px rounded data end, 2px gap between segments) ---- */
  154. table:not([width]) { width: 100%; table-layout: fixed; margin: 6px 0; }
  155. td.asmall { padding: 0 2px; line-height: 1.3; vertical-align: bottom; } /* bars stand on one baseline */
  156. table:not([width]) tr:first-child > td.asmall { border-bottom: 1px solid var(--line2); }
  157. td.asmall br { display: none; }
  158. td.rankc10center, td.hirankc10center { background: none; border: 0; color: var(--muted); font-family: __MONO__;
  159. font-size: 11px; text-align: center; padding: 6px 0 0; min-width: 0; letter-spacing: -0.02em; } /* no side padding: 24 narrow columns must not poke out of the table */
  160. td.hirankc10center { color: var(--fg); font-weight: 700; }
  161. /* a number over every bar is noise: the peak keeps its label, the rest are in the tooltip */
  162. td.asmall .v { display: none; }
  163. td.asmall .v.pk { display: block; color: var(--fg); font-family: __MONO__; font-weight: 600; }
  164. .rankc10center.skip { visibility: hidden; }
  165. img[id$="-v"] { display: block; width: min(64%, 22px); margin: 0 auto; border-radius: 0; }
  166. img[id$="-v"]:first-of-type { border-radius: 4px 4px 0 0; }
  167. img[id$="-v"]:not(:first-of-type) { margin-top: 2px; }
  168. img[id$="-v"]:only-of-type { border-radius: 4px 4px 0 0; }
  169. img[id$="-h"] { height: 8px; vertical-align: middle; margin: 0 1px 0 0; border-radius: 0; }
  170. td[nowrap] img[id$="-h"] { background: var(--mark); margin-right: 0; } /* lines: one value, one colour; the "when" column keeps the four */
  171. img[id$="-h"]:first-of-type { border-radius: 4px 0 0 4px; }
  172. img[id$="-h"]:last-of-type { margin-right: 0; border-radius: 0 4px 4px 0; }
  173. img[id$="-h"]:only-of-type { border-radius: 4px; }
  174. td.asmall img[id$="-h"] { margin-right: 6px; }
  175. table[width="520"] img[id$="-h"] { width: 14px; height: 8px; border-radius: 2px; } /* legend: a colour key, not a bar */
  176. table[width="520"] td.asmall { text-align: left; padding: 0 14px 0 0; white-space: nowrap; }
  177. /* the colour key wraps instead of forcing a sideways scrollbar on a narrow screen */
  178. table[width="520"] { display: flex; flex-wrap: wrap; gap: 4px 0; width: auto; max-width: 100%; }
  179. table[width="520"] tbody, table[width="520"] tr { display: contents; }
  180. img[id="blue-h"], img[id="blue-v"] { background: var(--t0); }
  181. img[id="green-h"], img[id="green-v"] { background: var(--t1); }
  182. img[id="yellow-h"], img[id="yellow-v"] { background: var(--t2); }
  183. img[id="red-h"], img[id="red-v"] { background: var(--t3); }
  184. td[rowspan] img { width: 36px; height: 36px; object-fit: cover; border-radius: 50%; display: block; margin: 0 auto; }
  185. td[rowspan] img.defpic { width: 26px; height: 26px; opacity: .3; } /* the shared default picture: a quiet placeholder */
  186. /* ranked tables: one left edge, and no stray rule under the last row */
  187. tr:first-child > td:not([class]):first-child { border-bottom: 1px solid var(--line2); }
  188. tr:last-child > .rankc, tr:last-child > .hirankc, tr:last-child > td[style*="background-color"] { border-bottom: 0; }
  189. .rankc, .hirankc { width: 44px; }
  190. td[style*="background-color"] { white-space: nowrap; } /* "3 days ago" stays on one line ... */
  191. td[style*="background-color"].quote { white-space: normal; } /* ... only the quote may wrap */
  192. .male, .male a { color: var(--male); }
  193. .female, .female a { color: var(--female); }
  194. .bot, .bot a { color: var(--muted); font-style: italic; }
  195. /* single-series bars */
  196. .bar { display: inline-block; vertical-align: middle; width: 140px; max-width: 100%; height: 8px; border-radius: 4px; background: var(--line); overflow: hidden; }
  197. .bar span { display: block; height: 100%; border-radius: 4px; background: var(--mark); opacity: 1; }
  198. .sigword { font-family: __MONO__; font-weight: 600; }
  199. /* "These didn't make it to the top:" is a bare <b><i> under the table; give it the same voice as a column title */
  200. .card > b { display: block; margin: 26px 0 4px; color: var(--muted); font-size: 11px; font-weight: 600; letter-spacing: .08em; text-transform: uppercase; }
  201. .card > b > i { font-style: normal; }
  202. #totallines { margin-top: 56px; padding-top: 14px; border-top: 1px solid var(--line2); color: var(--muted); font-size: 12px; }
  203. #footer { margin-top: 2px; color: var(--muted); }
  204. #footer .small { font-size: 12px; }
  205. ::selection { background: var(--fg); color: var(--bg); }
  206. /* ---- Section menu: one slim bar, one panel ---- */
  207. .pisg-nav { background: var(--bg); border-bottom-color: var(--line2); margin-bottom: 0; }
  208. .nav-current { color: var(--fg); font-weight: 650; }
  209. .nav-toggle { color: var(--muted); opacity: 1; }
  210. .nav-list { background: var(--surface); border-color: var(--line2); box-shadow: var(--shadow); }
  211. .nav-list a, .nav-list a:link, .nav-list a:visited { color: var(--fg); font-weight: 500; text-decoration: none; }
  212. .nav-list a:hover { background: var(--hover); }
  213. .nav-list a.active { font-weight: 700; }
  214. /* ---- Overview: a sentence and a list of facts, not a wall of tiles ---- */
  215. .hero { margin: 2px 0 22px; font-size: clamp(20px, 2.6vw, 26px); line-height: 1.3; font-weight: 650; letter-spacing: -0.02em; text-wrap: balance; }
  216. .hero b { font-family: __MONO__; font-weight: 700; letter-spacing: -0.05em; }
  217. .facts { display: grid; grid-template-columns: repeat(auto-fit, minmax(300px, 1fr)); gap: 0 56px; margin: 0; }
  218. .fact { display: flex; align-items: baseline; justify-content: space-between; gap: 16px; padding: 11px 0; border-bottom: 1px solid var(--line); }
  219. .fact dt { color: var(--muted); }
  220. .fact dd { margin: 0; text-align: right; font-family: __MONO__; font-weight: 600; font-variant-numeric: tabular-nums; }
  221. .fact dd small { display: block; color: var(--muted); font-family: __SANS__; font-size: 12px; font-weight: 400; }
  222. /* ---- Roles, personalities: columns split by a rule, no boxes ---- */
  223. .cardgrid { display: grid; grid-template-columns: repeat(auto-fit, minmax(168px, 1fr)); gap: 28px 32px; margin: 8px 0 0; }
  224. .minicard { display: flex; flex-direction: column; gap: 3px; padding: 12px 0 0; border: 0; border-top: 1px solid var(--line2); border-radius: 0; background: none; text-align: left; }
  225. .mc-title { color: var(--muted); font-size: 11px; font-weight: 600; letter-spacing: .08em; text-transform: uppercase; }
  226. .mc-name { font-family: __MONO__; font-size: 19px; font-weight: 700; line-height: 1.3; overflow-wrap: anywhere; }
  227. .mc-desc { color: var(--muted); font-size: 13px; }
  228. .mc-value { margin-top: 6px; font-size: 13px; font-weight: 600; }
  229. .mc-list { margin: 8px 0 0; padding: 0; list-style: none; }
  230. .mc-list li { display: flex; justify-content: space-between; gap: 8px; padding: 5px 0; border-top: 1px solid var(--line); font-family: __MONO__; font-size: 13px; }
  231. .mc-list span { color: var(--muted); }
  232. .minicard.t0 { border-top: 3px solid var(--t0); } .minicard.t1 { border-top: 3px solid var(--t1); }
  233. .minicard.t2 { border-top: 3px solid var(--t2); } .minicard.t3 { border-top: 3px solid var(--t3); }
  234. /* ---- Relation map: the one lifted surface ---- */
  235. .relmap-canvas svg { box-sizing: border-box; background: var(--surface); border: 1px solid var(--line2); border-radius: 8px; } /* width:100% must include the border */
  236. .relmap-info { background: none; border: 0; border-top: 1px solid var(--line2); border-radius: 0; padding: 12px 0 0; }
  237. .relmap-legend { color: var(--muted); }
  238. .ri-title { font-family: __MONO__; color: var(--fg); }
  239. .ri-sub, .ri-num { color: var(--muted); opacity: 1; }
  240. .ri-list li { border-top-color: var(--line); }
  241. .ri-link { color: var(--fg); font-family: __MONO__; text-decoration-color: var(--line2); }
  242. .lg0 i, .rm-node.t0 circle { background: var(--t0); fill: var(--t0); } .lg1 i, .rm-node.t1 circle { background: var(--t1); fill: var(--t1); }
  243. .lg2 i, .rm-node.t2 circle { background: var(--t2); fill: var(--t2); } .lg3 i, .rm-node.t3 circle { background: var(--t3); fill: var(--t3); }
  244. .rm-line { stroke: var(--muted); }
  245. .rm-node circle { stroke: var(--surface); }
  246. .rm-node text { fill: var(--fg); stroke: var(--surface); font-family: __MONO__; font-size: 12px; }
  247. .rm-edge.sel .rm-line { stroke: var(--fg); }
  248. .rm-node:focus-visible circle, .rm-node.sel circle { stroke: var(--fg); }
  249. @media (max-width: 720px) {
  250. body { padding: 20px 16px 72px; font-size: 14px; }
  251. .hr.rankc10center:nth-child(even) { visibility: hidden; } /* every second hour label on a phone */
  252. td { padding: 7px 6px; font-size: 13px; }
  253. .bar { width: 64px; }
  254. .card { margin-top: 44px; }
  255. .facts { grid-template-columns: 1fr; }
  256. td[rowspan] img { width: 30px; height: 30px; }
  257. }
  258. """
  259. def theme_css(name, t):
  260. head = f"/* pisg theme: {name} - {t['desc']}.\n Generated by layout/build-themes.py, edit the palette there. */\n"
  261. root = f":root {{\n color-scheme: {t['scheme']};\n{vars_block(t['light'])}\n}}\n"
  262. if t["dark"]:
  263. root += ("@media (prefers-color-scheme: dark) {\n :root {\n"
  264. + vars_block(t["dark"], " ") + "\n }\n}\n")
  265. return head + root + BASE.replace("__SANS__", t["sans"]).replace("__MONO__", t["mono"])
  266. for name, t in THEMES.items():
  267. path = out_dir / f"{name}.css"
  268. path.write_text(theme_css(name, t), encoding="utf-8")
  269. print(f"wrote {path.name} ({path.stat().st_size} bytes) - {t['desc']}")