Skip to content
fossil-skin Artifact [6ef87b8e64]
Login

Artifact [6ef87b8e64]

Artifact 6ef87b8e646c8a5727427fdeee2d32c63db8a736a39164ffabcdfb73ce179253:


Skin Architecture

How the skin works, in its dark and light variants, and how to change it.


Files

File Purpose
skin/css.txt All CSS: design tokens, layout, and the overrides for colors Fossil hard-codes. It is the dark variant; the light one is generated from it
skin/header.txt TH1 template: HTML injected before page content on every Fossil page
skin/footer.txt TH1 template: HTML injected after page content
skin/apply.py Writes the (dark) skin into every repo on this server
skin/build.py Writes the four importable files in dist/: dark and light, each with the system font or Instrument Sans
www/index.html Custom landing page served by Caddy at /
www/a11y.js Accessibility and behavior fixes, served by Caddy at /a11y.js; build.py inlines it into the importable files

Two variants from one stylesheet

css.txt holds every color as a token in its :root block, including about a dozen that used to be literal values further down (input backgrounds, selection rows, table stripes) and the RGB triplets behind every translucent tint (--green-rgb, --veil-rgb and so on, used as rgb(var(--green-rgb) / 0.10)).

build.py makes the light variant by:

  1. replacing the :root block with its LIGHT_TOKENS;
  2. dropping the block between /* @dark-only */ and /* @end-dark-only */ (the re-tinting of Fossil's ticket status colors, which a light page does not need);
  3. switching the color of the <select> arrow, which lives in a data URI;
  4. setting white-foreground: 0 in the details instead of 1.

Step 4 is why the light variant is its own skin and not a CSS toggle. Fossil reads white-foreground on the server and chooses user, branch and pikchr colors from it: dark, low-saturation tints for white text when it is on, pastels for dark text when it is off. A class toggle on the dark skin would receive the dark tints and could only wash them toward gray.

Dark tokens

The dark variant is a dark rendering of Fossil's default skin: same layout (title bar, menu band, horizontal submenu row), same sizes (16px body, 2.2em project title), with colors for WCAG AA contrast on a near-black page. www/index.html repeats them.

Token Value Usage
--bg #1c1e22 Page background
--surface #272a30 Menu band, cards, code blocks
--surface2 #31353d Hover states, selected rows
--border #41454e All borders
--text #e8e9eb Body text (13:1 on --bg)
--muted #b3b8c0 Secondary text (7.3:1 on --bg)
--link #7fb6ef Links (7.2:1 on --bg); the default skin's #3779BF, lightened
--title #8cbdf0 Project title
--active #f08a3c Active menu underline; the default skin's #D26911, lightened
--green / --red / --yellow #6fd48f / #f08a7e / #f2c94c Diff and status colors
--tint-mute / --chat-mute 0.4 / 0.3 How far Fossil's branch and chat colors are blended toward the surface
--font Instrument Sans, then system-ui Body text
--mono ui-monospace stack Code

Light tokens

Token Value Usage
--bg #ffffff Page background
--surface / --surface2 #f4f5f7 / #e8ebef Menu band, cards; hover
--border #d3d7dd All borders
--text #1c1e22 Body text (16:1)
--muted #565e68 Secondary text (6.6:1)
--link #2b67a8 Links (5.8:1); near the default skin's #3779BF
--title / --active #3a7bc0 / #d26911 Project title; the default skin's menu accent
--tint-mute / --chat-mute 0 / 0 Fossil's pastels are shown as they are

Deploying skin changes

On this server, after editing any skin file:

python3 /var/fossil/skin/apply.py

Fossil serves the updated skin immediately; no restart needed. For other repositories, run python3 skin/build.py and import the file from dist/ (see the README).


Colors Fossil hard-codes

Timeline graph

Fossil's built-in CSS draws graph lines and arrows black, which vanish on a dark page. The skin gives .tl-line a default of --muted without !important, so the per-branch color Fossil sets inline (with timeline-color-graph-lines) still wins. Nodes, arrows and warp lines are overridden as before.

Branch rows and chat messages

Fossil sets an inline background-color on timeline cells (branch colors) and on chat messages (the sender's color, m.uclr). With white-foreground on, those colors are already dark tints meant for white text, so the skin does not replace them. It only lays a translucent layer of the surface color over them with background-image, which CSS paints above background-color:

table.timelineTable td[style*="background"] {
  background-image: linear-gradient(rgb(var(--tint-rgb) / var(--tint-mute)), ...) !important;
}
body.chat .message-widget-content[style*="background"] { /* same, with --chat-mute */ }

--tint-mute (0.4) and --chat-mute (0.3) set how far they are blended toward the surface. In the light variant both are 0. Messages without a color get --surface.

Which color each user gets is decided by Fossil, from a hash of the login, and can be changed per repository with the user-color-map setting; see the README.

Chat layout

Message bubbles get messaging-app corners. When Fossil right-aligns your own messages (body.chat.my-messages-right, its default in tall windows), they point the other way. The skin does not force right alignment, so the "Left-align my posts" chat setting keeps working.

The message options popup (click a message's header) is one card in --surface2, with quiet timestamps, buttons at least 44px tall on touch screens, and "Delete globally" in --red because it removes the message for everyone.

isDark flag

Fossil's C code hard-codes isDark: false for custom skins, which affects the timeline graph renderer. a11y.js sets window.fossil.config.skin.isDark = true and the body class fossil-dark-style, but only when the stylesheet declares color-scheme: dark, so the light variant, which embeds the same script, is left alone. It runs after Fossil's inline scripts because of defer, and again on window.load because Fossil's load handlers may reset it.


Hamburger menu (☰)

Fossil's hbmenu.js toggles nav#hbdrop. Three parts work together:

Timeline weekdays

a11y.js puts the full weekday on a small line above each div.timelineDate. It reads the date Fossil prints, turns it into a day number with Date.UTC, and takes the weekday with Ben Joffe's branchless method (benjoffe.com/fast-day-of-week), the same one zit uses:

((Math.imul(day, 613566756) + 0x95000000 + (day >> 1) + (day >> 4)) >>> 0) >>> 29

It was checked against a plain modular calculation for every day from 1970 ± 100 million days (no mismatch), and 10,000 dates take under 4ms. The span carries its own layout in its style attribute, so a stylesheet the browser has cached from before cannot misplace it.

The logo is shown in a 48px box (36px on phones) with object-fit: contain, so a repository whose logo is not square shows it whole. A square logo looks the same as with cover.

Phones

At max-width: 600px wide tables (table.sortable, table.report) scroll inside themselves, label-and-value tables (check-in info) wrap their values, including the hash in span.nobr, and the project title is 1.3em. Markdown tables scroll inside themselves too. Fossil's label-and-field tables on ticket pages (body.tkt) and admin forms (body.setup) stack into label above value, File Ages shows the check-in under each file, forms and their fields are held to the screen width, and controls and submenu links are at least 44px tall. Forum replies are indented 10px a level (Fossil's inline style asks for 4ex), up to ten levels.

The page heading and Fossil's forum script

header.txt prints the page title as h1.page-title, shown only on wiki, document, directory and artifact pages. It is not shown there either when the page opens with a heading of its own (a wiki page's first heading, "History of ...", "Files in ..."), which would say it twice; the rule uses body:has(...) to look for a heading at the top of div.content. Elsewhere it is hidden with clip-path and a 1px width, never with display: none: Fossil's forum script (fossil.page.forumpost.js, refillTapZone) sizes its expand control from the height of the first h1 or h2 on the page and loops forever if that height is 0, which froze every long forum post.

Markup the skin works around


TH1 Template Notes

The header.txt and footer.txt files use Fossil's TH1 scripting language (a Tcl subset).

Available variables in header/footer:

The capexpr function checks user capabilities; the mainmenu loop uses it to hide links the current user can't reach.


CSP and JavaScript

Fossil's Content Security Policy:

script-src 'self' 'nonce-<per-request-nonce>'

Inline scripts without the matching nonce are blocked. On this server a11y.js is served as a static asset by Caddy at /a11y.js, which satisfies 'self'. It's loaded in header.txt with:

<script defer src="/a11y.js"></script>

The importable files from build.py inline it instead, as <script nonce="$nonce">, run on DOMContentLoaded for the same ordering.

The defer attribute ensures:

  1. HTML is fully parsed before the script runs
  2. Fossil's inline config scripts (which set window.fossil) have already executed
  3. The script runs before DOMContentLoaded

window.load re-runs the timeline and isDark fixes because Fossil's own load handlers insert SVG graph elements and may reset config values.