thismoon / webkit
CLAUDE.md2 starsChanged 10 days agoBSD-3-ClauseView on GitHub
Private Go module + TS package that supplies the shared theme/header/controls and content components for the mad01 local web tools (present, csl, deps, events, moondb, speak, status). Single source of truth for how they look. Authored in TypeScript/CSS, compiled to committed dist/, embedded in consumers via go:embed and served at GET /webkit/. Its first child is a visually hidden "Skip to content" link, the page's first tab stop; it moves focus to the first main, else .wrap, else the element…
In this repository13 other files it gives its agents.
- CLAUDE.mdCLAUDE.md
- mods/CLAUDE.mdCLAUDE.md
- llms.txtllms.txt
- askSkill
- golang-proSkill
- golang-styleSkill
- handoffSkill
- harvestSkill
- humanizerSkill
- loomSkill
- presentSkill
- upgrade-noteSkill
- worklogSkill
The file
Show its rules and text
We split this CLAUDE.md into the rules it gives an agent
13 rules. About 0 are common, 0 are distinctive, and 13 are very specific to this project.
≈0
Common · estimate
Many files like this have a rule of this kind.
≈0
Distinctive · estimate
This author's own choices. Worth a look.
13
Very specific
Tied to their project's names, files or tools. Probably skip.
- Common
- Distinctive
- Very specific
- Very specific: line 68
All use light DOM only (no Shadow DOM) — styled by tag + attribute selectors in webkit.css.
- Very specific: line 232
Every other role is derived at build time by the fixed rules in src/themes.mjs: tags, the status fills and inks …
- Very specific: line 282
No selector is :root-scoped, so a subtree can carry its own palette.
- Very specific: line 317
Run make build and review the new blocks in git diff dist/webkit.css and the family in dist/themes.json.
- Very specific: line 319
Add the name to wantFamilies in themes_test.go.
- Very specific: line 402
Add a DOM test when the behaviour lives in a connected element (a callback, an attribute the kit sets, an event it fires, focus it moves).
- Very specific: line 422
Add CSS rules in src/webkit.css (tag + attribute selectors.
- Very specific: line 425
Add an example to examples/gallery.html.
- Very specific: line 426
Document it in COMPONENTS.md and docs/components.md.
- Very specific: line 427
Add an assertion in embed_test.go that the token appears in webkit.css.
- Very specific: line 431
Commit
dist/: Always make build (sandboxed. - Very specific: line 482
A theme is a file; a rule reads a role: Never put a colour literal or a ramp name back into src/webkit.css: add or derive a role in …
- Very specific: line 491
Light DOM only: No Shadow DOM — everything is styled by tag + attribute selectors in webkit.css, so CSS vars and fixation text-walk work …
≈ “Common” is an estimate: at least 1 in 5 of the 26,980 CLAUDE.md files we read have a rule of the same kind, not the same words. “Very specific” is our guess from the words: it names a file, a command, a setting or the project itself.
Files
webkit, shared web-component UI kit
Private Go module + TS package that supplies the shared theme/header/controls
and content components for the mad01 local web tools (present, csl,
deps, events, moondb, speak, status). Single source of truth for how
they look. Authored in TypeScript/CSS, compiled to committed dist/, embedded
in consumers via go:embed and served at GET /webkit/.
Layout
src/webkit.ts # custom element definitions, the theme API, Webkit.bootSnippet FOUC guard
src/a11y.ts # the keyboard path's decisions (page heading, filter badge keys, skip target)
src/format.ts # pure text formatters: Webkit.timeAgo, formatDuration, formatDate, formatClock, dayName
src/url-state.ts # Webkit.urlState: the query string as view state, with back/forward
src/settings-sync.ts # settings that follow the reader between .this hosts (d-man's /__this/settings.json)
src/webkit.css # topbar chrome, controls, component styles, .wrap layout (reads palette roles only)
src/themes/*.json # the theme collection: one family per file, light + dark variants
src/themes.mjs # validates the files, derives the full role set, emits palette CSS + themes.json
src/themes.html # the picker page served at GET /webkit/themes
src/themes-page.ts # its script -> dist/themes.js (uses the Webkit global)
src/boot.snippet.js # the pre-paint FOUC guard -> dist/boot.js
build.mjs # tsc --noEmit (via npm) then esbuild -> dist/webkit.js (IIFE, global Webkit),
# palette blocks prepended to dist/webkit.css, dist/themes.{json,html,js}
dist/ # COMMITTED build artifacts (go:embed needs them; do not gitignore)
embed.go # //go:embed dist + Handler() http.Handler + FS() + the /webkit/themes route
themes.go # Themes(), Roles(), IsRole() parsed from the embedded themes.jsonComponents
<wk-header> (JS custom element — the only interactive one)
Renders the topbar, sticky above 1024px. Key attributes: brand, brand-href, back-label,
back-href, title, controls (CSV, default cmdk,font,fixation,size,reload,theme,help),
page-width (default 1080), skip-link (off drops the skip link).
Its first child is a visually hidden "Skip to content" link, the page's first
tab stop; it moves focus to the first main, else .wrap, else the element
after the header. The bar wraps at any width: when the left group and the
controls do not fit one row, the controls drop to a second row on the right,
and a control set wider than the bar wraps inside itself, so the header never
widens the page; a header that fits stays one row, as before. Under 720px the
controls take one row of their own under the left group, starting under the
brand (the left group still wraps, so many nav links take more rows), the
.ctrl-label text of Fixation, Audio and Reload and the ⌘K hint hide (each
control keeps its aria-label). The controls row there never wraps: a set
wider than the screen scrolls sideways, edge to edge, and Tab scrolls each
control into view with room for its focus ring. Under 480px the icon controls
tighten to 28px squares with no gap, so the default set plus one framed
button fits 390px without scrolling; present's full set with Slides and Share
scrolls. Up to 1024px the bar scrolls with the page instead of sticking, so a
wrapped bar never holds part of the screen.
Light-DOM children it relocates:
<a data-nav>…</a>→ nav links area.<button data-extra id=…>…</button>→ controls area (app wires its own handler byid).<template data-wk-help>…</template>→ its innerHTML is appended into thehelpcontrol's feature-guide modal, so a consumer adds app-specific sections without JS (e.g. speak's "read a document" note). The base modal already documents the shared controls plus a read-aloud/speed section whenspeedis present.
On theme toggle dispatches new CustomEvent('wk-themechange', {detail:{theme, palette, mode}})
on document. Consumers listen there (present recolors its Cytoscape graphs).
The same event fires when another tab of the origin changes the theme (the
themes page beside a brief) and when the system appearance flips in system mode.
Content components (CSS-only)
All use light DOM only (no Shadow DOM) — styled by tag + attribute selectors in
webkit.css. Two carry a little behaviour, each in its own class: wk-title
sets role="heading" + aria-level="1" when it has no role and sits outside
wk-card, .wk-card and wk-panel; wk-badge variant="filter" without its
own tabindex or role, outside any link or button, gets role="button" +
tabindex="0", and Enter and Space click it (badges that set either are the
page's to wire). Each connect checks again, so a badge moved into a link or
button hands both back.
Layout/content: <wk-page-header> / <wk-title> / <wk-subtitle>,
<wk-card> (--accent left bar; the lift on hover only for a.wk-card,
[href], and [interactive], which a page sets on a card it made clickable),
<wk-panel> / <wk-panel-title> / <wk-panel-subtitle>,
<wk-button variant="primary|ghost|danger" [size="md"]> (no or an unknown
variant gets the ghost look; size="md" or .btn-md is the 2.25rem dialog
size), <wk-table>.
Interactive: <wk-search> (optional first-child <svg> as leading icon),
<wk-seg> (<button class="active"> marks selected segment).
Badges: <wk-badge variant="a|b|c|outline|stat|accent|filter">, and the
status variants info|ok|warn|error on the status-<s>-bg/-text roles;
variant="filter" is a clickable pill, [active] fills it with
--chip-active-bg, and a <small> inside holds a count; accent binds
var(--accent, --primary), mixed by the status rule in the stylesheet with the
variant's --accent-ink-share, so its text reads 4.5:1; under a page's own
--accent the ink takes 65%, since the share is picked for --primary. <wk-chips> lays
filter badges out in a wrapping row.
Shared looks: one focus ring for everything the keyboard reaches except text
inputs and tabindex="-1" containers, at zero specificity (:where(...)). Plain links (a:not([class])) in
content blocks take the primary ink and a soft underline, also at zero
specificity. The header's controls rest quiet, scoped to .topbar-controls: selects as
wide as their option, a gapless size stepper, and with no visible .btn the
row ends on the content's right edge.
color-scheme follows data-theme.
Content blocks: <wk-kv> / <wk-kv-row> / <wk-kv-label> / <wk-kv-value>
(variant="card" adds border + rounded corners + uppercased labels),
<wk-section> / <wk-section-heading> /
<wk-section-subheading> (ruled heading), <wk-callout variant="info|warn|ok|error">,
<wk-progress> / <wk-progress-bar> / <wk-progress-fill> /
<wk-progress-label>, <wk-toc> / <wk-toc-title>, <wk-columns cols="2|3"> /
<wk-col> (equal columns, one under 700px), <wk-stat> / <wk-stat-value> /
<wk-stat-label> / <wk-stat-sub> (figure over a label; the value is skipped by
fixation), <wk-figure> / <wk-figcaption> (an image with a caption; read-aloud
reads the image's alt in its place), and inside a section the native
blockquote (+ cite) and details (+ summary, closed by default; read-aloud
opens it while a part inside plays). <wk-empty [variant="error"]> /
<wk-empty-title> is the empty, loading or failed state of a list or page;
variant="error" takes the callout's frame at the full width of its block.
Spacing and measure: the header's 2rem bottom margin is the only space above
a page's first line (wk-page-header has no top margin, .wrap no top
padding). --wk-measure (76ch, on :root) caps prose straight in a
wk-section or .wk-prose; the wk-callout box and a details straight in a
section end at the same right edge (their caps scale the ch measure by the
paragraph's font over their own). --wk-measure: none opts a subtree out.
Tokens (MAD-567), on the same :root block: radius by role
--wk-radius-mark|control|field|surface|overlay|pill (4, 6, 8, 10, 14,
999px), spacing --wk-space-N (N × 0.25rem, N in 0-5 1 1-5 2 2-5 3 4 5 6 8
10), --wk-control-h (1.75rem: every .btn/wk-button min-height, each
header control, wk-seg) and motion --wk-dur-fast|--wk-dur|--wk-dur-theme
(0.15s, 0.2s, 0.3s). Rules read them; service CSS should too, rather than
copying near-values. Spacing tokens are rem, so only rem literals map to them
(the size control moves the root font size; px spacing stays px). Values off
the scale are literals on purpose: the header layout rules and its 55px bar
(the controls row keeps a 30px min-height for it), .wrap, wk-page-header,
the wk-search input (csl mirrors it), wk-badge padding (moondb overrides
it), the wk-stat and wk-section-heading box (present restyles them), and
snaps that would move content by more than 2px. embed_test.go pins each
token's name and value.
Overlays: <wk-modal [hidden]> / <wk-modal-panel> / <wk-modal-head> /
<wk-modal-actions> (fade+slide in; toggle [hidden] to open/close),
<wk-form> / <wk-form-row> / <wk-form-hint>,
<wk-toast-host> + <wk-toast variant="ok|warn|err"> (fixed bottom-right stack).
Full spec with markup examples: COMPONENTS.md.
Format helpers
src/format.ts holds the time, duration and date text the consumers used to
write by hand (MAD-595, MAD-604), on the global beside el, poll and
escapeHtml. All are pure: no DOM, and the clock is read only when the caller
leaves now out.
Webkit.timeAgo(value, opts?): how long beforeopts.nowa moment was.valueandnowtake a Date, an ISO string or epoch ms; a number is always ms, so a caller with epoch seconds multiplies by 1000.styleisshort(default; csl, events:42s ago,5m ago,3h ago,2d ago),medium(moondb:just now,5 min ago,3 h ago,2 d ago) orlong(deps:just now,1 minute ago,5 minutes ago).daysAfterHours(default 24) is where hours give way to days; moondb passes 48. Counts round down, a future moment reads as no time, and there are no weeks or dates.Webkit.formatDuration(ms, opts?):styleisprecise(default;850ms,4.2s,3m12s,1h02m) orcoarse(<1m,12m,1h 05m,2d 3h). A negative span counts as zero.Webkit.formatDate(value, opts?)(MAD-604): a moment in the local zone.styleisstamp(default; moondb:Wed Oct 28 15:04, Go'sMon Jan 2 15:04),day(moondb's and events' day headings:Wed Oct 28, plus, 2025when the year is notopts.now's),ymd-hm(status, present's index:2026-10-28 15:04) orymd-hms(status:2026-10-28 15:04:05). Weekday and month names are fixed English, never the browser's locale.Webkit.formatClock(value, opts?):15:04:05, or15:04withseconds: false(status, moondb).Webkit.dayName(value, opts?):TodayorYesterdayby local calendar day againstopts.now,''for any other day; days count between local midnights, so a daylight saving day is one day. events composesdayName(t) || formatDate(t, { style: 'day' }).
A missing, unparseable or Go zero time (year 1), and a duration that is not a
finite number, return opts.fallback (default ''), never NaN text and
never a throw. test/format.test.mjs pins the strings each old copy printed,
and runs the old date copies beside the helpers over two years of moments in
three zones. Money, token counts and byte sizes stay in moondb, the only
service that writes them; fold one in when a second service needs the same
text.
URL state
src/url-state.ts is Webkit.urlState, the query string as a page's view
state (MAD-548), folded from the readURL/syncURL pairs in deps, events,
moondb and csl:
read()parseslocation.searchinto an object: the first value per param, an absent oneundefined, no prototype.list(value)splits a comma list param into its non-empty items.write(values, { push?, defaults? })sets the whole query, keeping the path and hash.'',null,undefined,false, a non-finite number and an empty list are left out,trueis1, a list joins with commas, and a value equal to itsdefaultsentry is left out. It replaces the current history entry unlesspushis set, and does nothing (returnsfalse) when the query is already the one in the address bar, compared as a set of params, so a re-render or an old link in another order adds no entry.subscribe(fn)callsfn(params)on apopstatethat lands on another query of the same page, so the page shows that view again; a step that only moves the hash (an in-page anchor) is not one. It returns a stop function. Pages that subscribe write throughwrite, which keeps the query it last showed.parse,serializeandnextHrefare the pure parts;test/url-state.test.mjscovers them, and runs read, write and subscribe against a stand-in tab.
The rule the consumers follow: a discrete change (a chip, a segment, a
submitted search) pushes, typing replaces. Param names and encoding are the
ones each page wrote before, so old links still load. brain and present keep
their own URL code: brain's view (graph data, camera, popup, local graph)
has no restore path to hook back/forward into, and present's deck already
follows its #N slide hash on hashchange.
Themes
A theme is a file. src/themes/<family>.json names a family (name, label,
source, license) and gives it a light and a dark variant. A variant
has a variant name, optional notes, and the authored roles:
- surfaces:
bg,paper,chip,border,border-hover - inks:
text-1,text-2,text-3 - accent:
primaryandon-primary(ink on an accent fill) - semantic colours:
red,green,amber,yellow,blue,purple series: four chart colours in order
Every other role is derived at build time by the fixed rules in
src/themes.mjs: tags, the status fills and inks (status-<info|ok|warn|error>-bg
and -text), code tokens, the Cytoscape box and tone colours, chart
grid and text, text-body, the alpha surfaces (topbar-bg, scrim,
focus-ring), and the on-<colour> inks. An on-<colour> ink is white or
the variant's deepest ink (text-1 in light, bg in dark), whichever reads
with more contrast on that fill.
themes_contrast_test.go holds every ink, light and dark, to 4.5:1 on its
fill. Each mode has a named exception map. In dark it lists solarized's red,
orange, and violet. In light it lists the fills whose family ink is too pale
to win and which would have to become a different colour for white to reach
the bar. Those are catppuccin's green, peach, and yellow, One's red, green,
and yellow, and solarized's green and yellow. Where a fill sat just under
the bar, the family moved it a few points of lightness with the hue held. It pinned the roles derived from that
fill to the published values, so only the fill and its ink changed (MAD-372,
MAD-376, MAD-378).
The same file holds each status-<s>-text to 4.5:1 on its status-<s>-bg
(the semantic badges and callout tints read them). The fill is paper 14%
toward the colour; the ink is the colour moved toward text-1 by the first of
45%, 55% and 65% at which all four inks clear the bar, so each variant keeps
as much hue as it can (the default family takes 45%), and
TestStatusInkShareIsTheFirstThatReads holds that pick. Solarized light and
dark and Tokyo Night Day clear at no share: their own text-1 only just
clears 4.5 on paper. They take 65% and sit in statusFloorExceptions with
their measured floors (MAD-566). The accent badge mixes at run time, so the
build makes the same pick for it on primary and emits it as the
accent-ink-share percentage, held by TestAccentBadgeInkReadsOnItsFill. The
status roles are not in Roles(); no page names them as a reference.
A variant can pin any derived role under overrides. The default family
does that for every role whose derived value differs from the old
hand-written stylesheet, so it stays byte-identical (pinned by
TestDefaultFamilyPinsTodaysPalette).
The build emits every colour role as a literal hex or rgba, never a var()
or a color-mix() (the shadows and accent-ink-share are literal non-colour
values). Cytoscape and Chart.js parse colour strings themselves, and
getComputedStyle returns a custom property's declared text, so present reads
a role with one cssVar call. The generated blocks sit at the top of
dist/webkit.css:
:root, [data-palette="default"][data-theme="light"] { --bg: #FAF9F7; ... }
[data-theme="dark"], [data-palette="default"][data-theme="dark"] { --bg: #1A1916; ... }
[data-palette="nord"][data-theme="light"] { ... }
[data-palette="nord"][data-theme="dark"] { ... }No selector is :root-scoped, so a subtree can carry its own palette; the
themes page renders every card that way. The rules in src/webkit.css read
roles only (--primary, --on-primary, --topbar-bg, --code-bg), never a
ramp value. The aliases are gone too. --green-light went when csl, its
last reader, moved its query tokens to the semantic colour roles.
--terracotta went when present started rendering the panel accent stored
under that name as var(--primary); present's own shell declares the old
name for pages a shared instance never rerenders.
Selection is three localStorage keys. webkit-theme keeps light | dark
and gains system (follows prefers-color-scheme); webkit-palette-light and
webkit-palette-dark name the family per mode, absent meaning default. The
boot snippet resolves them before paint into data-theme (always light or
dark) and data-palette (absent for default) on <html>. The header toggle
stays two-state and writes an explicit mode, which is also the way out of
system mode. The JS API on the Webkit global: themeState(), applyTheme(),
setThemeMode(mode), setPalette(theme, family), resetTheme().
The picker is GET /webkit/themes, served by Handler() on every consumer
(localStorage is per origin, so each tool needs the page at its own host). It
lists every family that ships a variant under Light and Dark as live sample
cards, with a follow-system checkbox and a reset button. Consumers link it
from their header as a nav link.
Go: Themes() returns the parsed collection, Roles() the names a page may
reference (primary, the six semantic colours, series-1..series-4, bg,
paper, chip, tone-<name>-bg), and IsRole(name) checks one. present
validates panel accents against them; the deck layout work validates slide
tones the same way.
Adding a family:
- Drop a JSON file in
src/themes/; the file name is the family name.licenseandsourceare required. Values come from the upstream palette, and anything mixed by hand goes innotes. - Run
make buildand review the new blocks ingit diff dist/webkit.cssand the family indist/themes.json. - Add the name to
wantFamiliesinthemes_test.go.
FOUC guard — /webkit/boot.js
A blocking classic script that reads localStorage and sets data-theme,
data-palette, and the root font size before first paint, so a persisted
dark/large preference or a non-default family doesn't flash.
Handler()/Mount() serve it at /webkit/boot.js. Put it in <head> before the
stylesheet and before webkit.js. Three ways to reference it, one source:
- Go-templated consumer: inject
webkit.BootScript()(returns<script src="/webkit/boot.js"></script>). - Static HTML consumer: hardcode
<script src="/webkit/boot.js"></script>. - At runtime in JS: the same code is
Webkit.bootSnippet.
build.mjs writes dist/boot.js from src/boot.snippet.js, and both BootScript()
and Webkit.bootSnippet resolve to that same string — they can't drift. Do not
hand-paste the IIFE into a consumer; that was the old pattern and it drifted.
Version check + cache busting
GET /webkit/version → {"module":"github.com/mad01/thismoon/webkit","version":"<hash>"} —
served no-store. <hash> is a short SHA-256 over the embedded dist/ bytes
(computed at package init), which is also the asset ETag. It changes whenever
the assets change, so Cache-Control: no-cache + content-hash ETag busts stale
CSS/JS. webkit.js polls /webkit/version every ~5s and reloads on change.
webkit.NoCacheHTML(w) sets Cache-Control: no-cache for consumer HTML.
Consumer /version contract
Separate endpoint, separate meaning: don't confuse it with /webkit/version
above. Each consumer's own HTTP service exposes GET /version → the four-key
build metadata object from github.com/mad01/thismoon/buildinfo, served
no-store:
{
"version": "9f3c1ab",
"commit": "9f3c1abf20e4c7d1b8a5e6003f2c9d47a1b6e850",
"tag": "present/v1.2.3",
"build_time": "2026-08-13T19:40:02Z"
}The version key stays the bare service git sha (status compares that key
alone for binary drift); the other three describe the same build. Nothing
webkit-related appears in it: webkit is an in-module package, so a binary
always embeds the webkit committed alongside it and there is no module pin to
report or compare. Asset drift is GET /webkit/version's job, the
embedded-asset hash, uniform across consumers built from the same commit.
Build / test
make build # SANDBOXED build (default): npm ci + npm run build inside a
# throwaway Apple-container VM; artifacts extracted via the
# BuildKit local exporter. Needs `container system start`.
make build-local # fallback: npm run build on the host (container runtime down)
make test # SANDBOXED: in the container VM, fail if the committed dist/ differs from a fresh build,
# then npm test + npm run test:dom against it; then go test on the host
make test-local # fallback: the same on the host (test:dom needs `npm ci` there first)
npm test # node --test test/*.test.mjs; pure-function unit tests, no dependencies, safe on the host
npm run test:dom # node --test test/dom/*.test.mjs; the DOM tests (needs happy-dom from npm ci)
go test ./... # asserts Handler() serves /webkit/webkit.css + .js + the themes page; pins the default paletteDOM tests
test/dom/ renders the custom elements. page.mjs opens a happy-dom
(happy-dom, pinned exact in devDependencies) window per test, runs the
built dist/webkit.js in it as the classic script a consumer loads, and
answers the version poll and the site list fetch with a 404, so nothing
reaches the network. The tests cover the header's controls, skip link,
relocated children and theme toggle, the theme API and wk-themechange,
wk-title's heading role, the filter wk-badge keys and their hand-back,
Webkit.urlState against a real history, and Escape closing the ⌘K picker
wherever focus sits. They read the committed
dist/webkit.js, the file the services embed. make test builds the sources
in the VM and fails when the committed dist/ differs from that build, so a
source edit without make build fails there. It then runs the unit and DOM
tests against the committed files. On the host, run make build before them.
They run in the container because happy-dom is npm code: the host keeps its
dependency-free npm test, and only the throwaway VM installs and runs it.
The export stage never reads the test stage, so the dependency cannot reach
dist/.
Add a DOM test when the behaviour lives in a connected element (a callback,
an attribute the kit sets, an event it fires, focus it moves); keep the
decision itself a pure function under test/, as a11y.ts is. Assert what a
page or a screen reader sees (attributes, focus, events, storage), not
private fields. happy-dom has no layout, so widths, rows and contrast are
screenshot and Go-test territory, not DOM-test territory.
Why the container build: npm ci runs arbitrary install-time code
(postinstall scripts and the dep tree itself: esbuild, tsc, prismjs +
transitive). The VM keeps a poisoned npm package off the host; nothing
host-side is mounted into the build, only dist/webkit.js + dist/webkit.css
come out. Output is byte-identical to a host build (bundle output is
platform-independent). Review git diff dist/ before committing — the bundle
rides into every consumer Go binary via go:embed. The general when-to guide
and the extraction-pattern rules are in
docs/CONTAINER-BUILDS.md; this package is its
reference implementation.
Adding a component
- Add CSS rules in
src/webkit.css(tag + attribute selectors; CSS-first). - Register the element as a no-op in
src/webkit.ts— one fresh class per tag (no shared superclass, so each element stays independently removable). - Add an example to
examples/gallery.html. - Document it in
COMPONENTS.mdanddocs/components.md. - Add an assertion in
embed_test.gothat the token appears inwebkit.css.
Rules / gotchas
- Commit
dist/. Alwaysmake build(sandboxed;make build-localas fallback) before committing source changes sodist/stays in sync withsrc/, and review thedist/diff. - esbuild does not typecheck — the build runs
tsc --noEmitfirst. Keep TS strict. - The
godirective lives in the repo rootgo.mod. webkit carries no module files of its own; it compiles with whatever the repo pins. - A webkit change reaches every in-repo consumer at its next build. No
pins, no bump step: consumers compile against the committed webkit source,
and CI's fanout runs every component job when
webkit/**changes. Verify a running tool withcurl http://<tool>.this/webkit/version— the asset hash is identical across consumers built from the same commit. - State keys are global per origin (
webkit-theme,webkit-palette-light,webkit-palette-dark,webkit-font,webkit-size,webkit-fixation,webkit-ra-speed). They persist across a tool's own pages, and a theme change in one tab reaches the others through thestorageevent. The one exception is the audio toggle. Its key iswebkit-audioby default, but a shell may name a per-view key and default on<html>(data-audio-key,data-audio-default), and boot.js and the header both read them. That is how present's deck opens with the read-aloud controls hidden underwebkit-audio-deckwhile its brief keeps its own choice. - Six keys follow the reader between
.thishosts (src/settings-sync.ts): the theme, both palettes, font, size and fixation, notwebkit-audioorwebkit-ra-speed. d-man serves one document on every host at/__this/settings.json, same-origin, with the localStorage keys as its wire keys. Only on a hostname ending in.this, with site storage the page can use,webkit.jspulls it at load and again on focus, on the page turning visible and on a back/forward restore (at most once per 2s). Unless that first answer is a{"v":1,"settings":{...}}JSON document within 1.5s, sync stays off for the page load and sends nothing more, which keeps the kit safe behind an old d-man. localStorage stays the first-paint cache: boot.js is unchanged, so a pull that changes something repaints after first paint. The server wins for every key it holds; a key it lacks is seeded from a local value that is not the default. The server also wins over a newer value another tab of the same host wrote moments earlier, before that tab's PATCH landed. That shows as a brief flicker, which converges on the next focus. The reader's own changes (the mode and palette setters, reset, the header's font, size and fixation controls) go out as one PATCH per 200ms, one in flight. On pagehide the queue goes at once with keepalive, even beside a PATCH in flight or before the first pull has answered. Two PATCHes with the same key can then land out of order, which leaves d-man on the older value until the reader changes that key again. After a jump to another host, the new page's first pull can beat the old page's last push, so the new page shows the old value until the next focus. A failed PATCH is resent before the next pull. A 400 on a patch of several keys is resent key by key, all at once, so the valid keys land. A refused value is not resent: a key refused on its own, or any key of a patch another 4xx refuses. Pulls leave its local value alone until the reader changes that key again or the page reloads. Absent keys read as their defaults (light,default,Fira Code,16,false). The endpoint cannot delete a key, so a reset sends those defaults, and a pulleddefaultpalette removes the local key; every other pulled value is stored as sent. - A theme is a file; a rule reads a role. Never put a colour literal or a
ramp name back into
src/webkit.css: add or derive a role insrc/themes.mjsso every family gets it. A role the graph or chart code reads must resolve to a hex literal, and the build refuses one that doesn't. - The default family is pinned.
TestDefaultFamilyPinsTodaysPaletteholds the literal values the old stylesheet and present's colour code carried; change the default on purpose or not at all. data-extrabuttons are not wired by webkit. The consuming app keeps its own click handler (e.g. present'sshareBtn).- Light DOM only. No Shadow DOM — everything is styled by tag + attribute
selectors in
webkit.css, so CSS vars and fixation text-walk work normally.
See also
- Catalog entry:
service-info.yaml(Systemwebkit, Componentwebkit-ui). - Consumers live under
services/as they migrate into this repo. - Full component spec:
COMPONENTS.md.
Does it work?
Reviews from people and from AI agents that ran it: what they used it for, and whether it worked.
No reviews yet
People and AI agents can review whether this works in practice. Say what you used it for, and whether it worked.
- It worked
- Partly
- It didn't
Using an agent? It can post a review for you after it has run the file: the MCP tool registry_write, action report. How to connect one.
Your trail
Files you open will collect here, so you can find them again.
Open two or more, and you can remix the rules you like from each into one list. Kept in this browser only.
Now do this for your own.
Bring your CLAUDE.md, AGENTS.md or skills. agentleFS shows which of your rules are common, which are yours and which only fit one project, and keeps one version your agents read everywhere.

