A personal performance fork of the Blueprint theme for Roam Research, built on
roam-extension-template's
zero-runtime-dependency, esbuild-based Depot extension scaffold.
Extension URL: https://svyk.github.io/svy-theme/ ·
Published assets: extension.css ·
extension.js
This is not affiliated with, endorsed by, or a replacement for the upstream Depot
listing. It exists to run a locally maintained, auditable copy of the theme with a real
extension.css file (instead of upstream's JS-string-inlined CSS) and a hand-ported
dark-mode toggle, and to make future performance refactors of the stylesheet possible
against a pinned, provenance-tracked source. It is never submitted to Roam Depot.
Formerly published as roam-blueprint — renamed to Svy Theme on 2026-08-07. The old
install URL (https://svyk.github.io/roam-blueprint) is dead; use the URL below.
- Original theme:
rcvd/blueprintby Alexander Rink (alex@goedel.io). Official Depot listing: search "blueprint" in Roam's Depot, or see the manifest atRoam-Research/roam-depot→extensions/rcvd/blueprint.json. - Pinned upstream commit:
cc1c71784a26bc86da99a1572733c624e9196299. - Vendored CSS and full licensing/provenance notes:
vendor/upstream/PROVENANCE.md. - The dark-mode toggle in
src/dm-toggle.jsis a plain-JS port of upstream'ssrc/components/dm-toggle.tsat the same pinned commit — see that file's header comment for behavioral notes.
If you want the real thing with the author's ongoing updates and Depot revenue-share
support, install rcvd/blueprint from Roam Depot directly instead of this fork.
extension.cssis a real stylesheet Roam loads directly, not a<style>tag injected by a JS string (upstream'sadd_css.shmechanism). This fork removes that mechanism entirely.- Plain JS + esbuild build (this template's toolchain) instead of upstream's
TypeScript + webpack +
roamjs-components. Zero runtime dependencies. - The dark-mode toggle is rewired onto this template's
src/lifecycle.jsdisposal contract: every DOM node, event listener, and the settings panel are registered for cleanup, so disabling the extension fully restores native Roam UI with no page reload.
Two identifiers are pinned even though the extension itself is now called Svy Theme:
.blueprint-dm-toggle— the CSS class on the dark-mode toggle button, unchanged from upstream. The Better Tasks extension probes for this exact class atsrc/index.js:19874(document.querySelector(".blueprint-dm-toggle")) to detect that this theme is active and pick matching panel colors. Renaming the class would silently break Better Tasks' theme awareness — the naming asymmetry (extension named Svy Theme, class still namedblueprint-dm-toggle) is intentional and permanent until Better Tasks' probe changes.bp-appearance— the extension setting id storing the auto/dark/light choice. Roam syncs extension settings through the graph, so this id is user data already written into every graph this theme is installed on. Renaming it would orphan existing synced values instead of reading them.
Settings → Roam Depot → Svy Theme. Every row id is prefixed bp-.
| Row | id | Control | Default |
|---|---|---|---|
| Appearance | bp-appearance |
select auto / dark / light |
auto |
| Svy Beam | bp-pack-beam |
switch | on |
| Caret color (light) | bp-beam-caret-light |
input (hex) | #00695e |
| Caret color (dark) | bp-beam-caret-dark |
input (hex) | #48d0c0 |
| Caret shape | bp-beam-caret-shape |
select beam / block / outline / underline / bar / native |
beam |
| Caret width scale (%) | bp-beam-caret-width |
input, clamped 50–200 | 100 |
| Caret height (%) | bp-beam-caret-height |
input, clamped 30–120 | 82 |
| Caret corner radius (px) | bp-beam-caret-radius |
input, clamped 0–12 | 3 |
| Caret opacity (%) | bp-beam-caret-opacity |
input, clamped 45–100 | 100 |
| Caret glow | bp-beam-caret-glow |
select soft / none / halo |
soft |
| Caret behavior | bp-beam-caret-behavior |
select responsive / steady / glide / breathe / comet |
responsive |
| Caret blink | bp-beam-caret-blink |
switch | off |
| Focus wash | bp-beam-wash |
switch | off |
| Wash intensity | bp-beam-wash-intensity |
select subtle / medium / off |
off |
| Cursor style | bp-beam-cursor |
select svy / native |
svy |
| Preview | bp-beam-preview |
reactComponent |
— |
Roam's settings panel supports only generic input, select, switch, button, and
reactComponent rows — there is no native color picker or slider. Colors are typed as
hex; invalid values fall back without reaching CSS. Numeric inputs accept decimals,
retain one decimal place, and clamp to the documented safe range, so a synced typo cannot
make the caret vanish or fill the screen. The preview row renders through Roam's own
window.React, adding no dependency; it is stateless and repaints from the same custom
properties the stylesheet reads. It is omitted if window.React is unavailable.
src/theme-vars.js is the theme's only CSS-variable writing path. It reads the settings,
computes the --svy-beam-* property set, and publishes it from one injected
<style id="svy-theme-vars"> element — not inline style on documentElement, which
would put every value at the inline specificity level where nothing in a stylesheet,
including the user's own roam/css, could override it. The element is registered on the
lifecycle, so unload removes it in one node.remove().
src/css/40-beam.css reads every value through var(--svy-beam-…, <safe value>). With
JavaScript unavailable, the researched caret colors and a native bar remain; the extended
shape/size/behavior system simply steps aside. A test asserts both directions of the
variable contract: nothing the stylesheet reads is unpublished, and nothing published
is unread.
Switching Svy Beam off puts svy-off-beam on <html>, and every rule in
40-beam.css is scoped under :root:not(.svy-off-beam) — one extra class test per rule,
no reload, native caret and cursors restored immediately.
The dark-fixes (10-fixes-dark.css) and plugin-compatibility (20-plugins.css) layers
have no master switch yet, deliberately. Gating them the same way is not a mechanical
wrap: many of their rules are themselves rooted at :root.bp3-dark/:root:not(.bp3-light),
so neither a nesting wrapper (:is(:root:not(.svy-off-…)) :root.bp3-dark … can never
match) nor a @container style() wrapper (custom-property queries evaluate against the
parent element, so :root-level token declarations inside one stop applying) preserves
their behavior. Shipping a switch that silently does nothing is worse than no switch, so
those packs stay always-on until their rules are re-rooted during the U7 tokenization
pass.
npm run dev # initial build, then rebuild src/ changes
npm run build # generate root and deploy artifacts
npm test # node:test suite
npm run scan:secrets # fail on common committed credentials
npm run verify:generated # compare source with root/Pages artifacts
npm run check # build, syntax check, and testsThere are no runtime dependencies. The sole build-time dependency is exactly pinned
esbuild, with its complete dependency graph locked in package-lock.json. It bundles
src/extension.js and legitimate relative modules into one browser ESM file while
preserving the default Roam lifecycle export. Browser targeting makes unresolved
packages and Node built-ins hard build failures, and an explicit build guard rejects
HTTP(S) imports so the published extension is self-contained. The build emits no source
map.
Run npm ci --ignore-scripts --no-audit --no-fund once after cloning and whenever the
lock changes. The Depot entry point, build.sh, performs that clean locked install
itself before building, including when invoked from another working directory.
The secret scanner covers common Roam, OpenAI, Anthropic, GitHub, Google, AWS, Slack, and
private-key formats. Fix a finding rather than suppressing it. For a genuinely synthetic
false positive, put secret-scan: allow RULE-ID -- REASON on the same or immediately
preceding line; the reason must contain at least eight characters.
src/extension.js exports { onload, onunload } as the default export, as required by
Roam. onload also returns a cleanup callback. Both paths share one idempotent lifecycle
instance, so reloads cannot leave a second active runtime.
Roam automatically removes extension-scoped command palette commands, slash commands, the
settings panel, and extension.css. src/lifecycle.js also owns resources Roam does not
remove: DOM nodes, listeners, observers, timeouts, intervals, and pull watches. Every
long-lived resource the dark-mode toggle creates is registered here, so onunload
disposes them in reverse order and native Roam UI is fully restored without a page
reload.
The settings example follows the current Extension API contract: get returns null
for an unset key, set persists a JSON value, and settings.panel.create automatically
stores switch/input/select controls by row id.
Reference: Roam Depot/Extension API.
Hosted URL (recommended): in the target Roam graph, open Settings → Roam Depot, enable Developer mode, then use Developer Extensions → Load extension → choose the URL option and point it at:
https://svyk.github.io/svy-theme
GitHub Pages serves extension.js and extension.css straight from the deploy/
artifacts on main, rebuilt on every push.
Local folder (for source edits):
- Run
npm ci --ignore-scripts --no-audit --no-fund, thennpm run build. - In Developer Extensions, choose Local folder.
- Select this repository root — the folder containing
README.md,extension.js, andextension.css. - Local-folder extensions do not auto-start after a new app session because the browser
requires a fresh filesystem permission. Load the folder again from Developer
Extensions, or use
Ctrl-D, thenCtrl-R.
Developer extensions are installed per client, not synced through the graph. Repeat
the installation (hosted URL or local folder) on every desktop/browser profile that
should run it. Reload all developer extensions with Ctrl-D, then Ctrl-R, or use
Settings → Roam Depot → Developer Extensions → Reload.
This repository's source and the built extension.js/extension.css on GitHub Pages
are public, so the hosted install URL above works and the code is auditable. That is
the only reason it is public — it is never submitted to Roam-Research/roam-depot,
carries no Depot listing, and receives no Depot revenue share. It is not affiliated
with, endorsed by, or a substitute for the upstream rcvd/blueprint Depot listing (see
"Credit and provenance" above). Submitting to Depot would require fresh, explicit
sign-off — it is not a side effect of any build, install, or live-test step performed
against this repo.
- Update
package.jsonandCHANGELOG.md. - Run
npm run check. - Inspect the generated diff and scan it for secrets.
- Commit source and generated artifacts together.
- Reload the developer extension in a disposable test graph before relying on it.
MIT for the template scaffolding and lifecycle/build code in this repository.
The vendored theme CSS and ported toggle behavior originate from rcvd/blueprint — see
vendor/upstream/PROVENANCE.md for that code's own licensing situation (upstream
declares MIT in package.json but ships no LICENSE file).