@nullwert/design-system (0.1.0)
Installation
@nullwert:registry=https://gitea.think-jeckel.de/api/packages/nullwert/npm/npm install @nullwert/design-system@0.1.0"@nullwert/design-system": "0.1.0"About this package
OS1 — design system
A monochrome-dark retro design system (Iosevka, square corners, hard shadows, pink as the signal color) for local web apps. Zero dependencies: one CSS file with the fonts base64-inlined, plus a small vanilla JS helper.
Derived from the TypeSafeAI "TS.AI.OS1" poster — this is its dark mode.
Files
src/
tokens.css design tokens (colors, type, spacing, borders, shadows)
base.css @font-face (Iosevka 400/700), reset, focus/selection, .cursor
components.css all component classes
textures.css dither / halftone decorations
ds.js DS.toast / DS.dialog / DS.confirm / DS.prompt / DS.tabs
fonts/ Iosevka woff2 (latin) + license — fetched by scripts/fetch-fonts.mjs
scripts/
fetch-fonts.mjs downloads the fonts (only step that needs network)
build.mjs concatenates src/*.css, inlines fonts as base64 → dist/
dist/
ds.css the single-file bundle (~2.6 MB, fonts included)
ds.js copy of src/ds.js
preview/
poster.html dark-mode recreation of the poster, built from real components
gallery.html every component, interactive
Commands
npm run fonts # fetch Iosevka woff2 + license (network)
npm run build # rebuild dist/ds.css + dist/ds.js (stamped with version + git hash)
npm run preview # serve repo root at :8123 → open /preview/poster.html
Distribution via Gitea npm registry
The bundle ships as the npm package @nullwert/design-system. The Gitea npm
registry requires the scope to match the owner, so publish it from the nullwert
user/org (or change the scope everywhere if you use a different owner).
One-time setup
- Gitea → Settings → Applications → create a token with
package:write(publishing) orpackage:read(consuming). - Copy
.npmrc.exampleto.npmrcandexport GITEA_NPM_TOKEN="<token>", thennpm publish— or just push tomainand let the Gitea Actions workflow (.gitea/workflows/publish.yml) publish when the version changes.
Consumer repos
dl-manager, ebook and launchctl all pin "@nullwert/design-system": "0.1.0"
and carry a scripts/sync-ds.mjs + npm run sync-ds that copies
node_modules/@nullwert/design-system/dist/* into place:
| repo | target | notes |
|---|---|---|
| dl-manager | public/ |
wired into npm run build (graceful fallback if not installed) |
| ebook | src/assets/ |
run cargo build after — assets are include_str!-embedded |
| launchctl | static/ |
same as ebook |
Each repo has its own .npmrc.example (registry + ${GITEA_NPM_TOKEN} indirection,
no secrets committed). Until the package is installed, sync-ds exits 0 and keeps
the committed copies.
Changing the design spec
# in design-system/
npm version patch # 0.1.0 → 0.1.1 (edit src/*.css first)
npm run build # stamp picks up version + git hash
npm publish
# in each app repo
npm update @nullwert/design-system && npm run sync-ds
cargo build # Rust apps only — assets embed at compile time
The first line of every deployed ds.css identifies its exact build:
/*! @nullwert/design-system v0.1.0+dev built 2026-10-31 */
Tokens
| token | value | role |
|---|---|---|
--bg / --panel / --panel2 / --inset |
#0c0a0c / #141117 / #1b171e / #08070a |
surfaces |
--titlebar / --titlebar-ink |
#e8e4ea / #0c0a0c |
inverted title strips |
--line / --line-strong |
#2b262e / #554b58 |
borders, rules |
--text / --muted / --faint |
#ece8ee / #938c98 / #635c68 |
text |
--accent / --accent-ink |
#f2a0c6 / #1c0a14 |
pink signal — selection/active/focus only |
--ok / --warn / --err |
#74c58c / #d9b36a / #e07a72 |
status |
--font |
Iosevka, ui-monospace, Menlo, monospace | everything |
--shadow / --shadow-sm |
4px 4px 0 / 2px 2px 0 black |
hard offset shadows |
Components
- window:
.win(+.win--flat) ›.win-bar/.win-body/.win-foot,.displayinverted heading chunk,.widget-body - badges:
.badge,--solid,--accent,--inverse,--ok/--warn/--err - buttons:
.btn,--primary,--danger,--icon,--sm;.filelabelupload - inputs:
.input,.textarea,.select,.fieldlabel,.field-row,.checkline - data:
.tbl(+.tbl--stackresponsive,td[data-label]),.rowlist›.row,.card,.history-row-style rows are app-level - nav:
.tabs›.tab(.active),.tabbarbottom nav,.crumbs - overlays:
dialog.dlg›.dlg-bar/.dlg-body/.dlg-foot,.toasts›.toast(--info/--warn/--err/--out) - status:
.dot(--ok/--warn/--err/--busy),.pill,.statusline,.warnbox - misc:
.progress(<b>fill /<i>square marker),.logview,.empty,.dither(--fine/--dense/--halftone),.burst,.cursor,.muted/.faint/.accent/.nowrap/.mono-file
ds.js
DS.toast(msg, kind?, ms?) // kind: ok | info | warn | err; ms=0 stays
DS.confirm(msg, {title, okLabel, cancelLabel, danger}) -> Promise<boolean>
DS.prompt(msg, value?, {title, ...}) -> Promise<string|null>
DS.dialog({title, body, footer}) -> {el, open, close}
DS.tabs(container, onChange?) -> {set(index)}
Integration pattern (used by the three apps)
Apps serve dist/ds.css + dist/ds.js next to their HTML and keep a small
page-level <style> for layout only:
- dl-manager (Node
http): files inpublic/,serveAsset()routes/ds.css/ds.js,<link>+<script src>. - ebook (axum): files in
src/assets/,include_str!handlers for/ds.css/ds.jsinweb.rs. - launchctl (axum): files in
static/, sameinclude_str!pattern inmain.rs.
Class-name collisions to watch for
App markup must not reuse ds component classes for other meanings. dl-manager's
content panes were renamed section.tab → section.pane (ds .tab is the tab
button), and its generic .row flex helper coexists with ds .rowlist .row via a
late override in the page style.
Updating the committed copies (package not yet published)
Before the Gitea registry is set up, refresh the apps' committed copies directly:
node scripts/build.mjs
cp dist/ds.css dist/ds.js ../dl-manager/public/
cp dist/ds.css dist/ds.js ../../rust/ebook/src/assets/
cp dist/ds.css dist/ds.js ../../rust/launchctl/static/
Then rebuild the Rust apps (assets embed at compile time).