@nullwert/design-system (0.2.0)

Published 2026-11-03 21:24:49 +01:00 by nullwert

Installation

@nullwert:registry=https://gitea.think-jeckel.de/api/packages/nullwert/npm/
npm install @nullwert/design-system@0.2.0
"@nullwert/design-system": "0.2.0"

About this package

OS1: monochrome-dark retro design system (Iosevka) for local web apps

OS1 — design system

A monochrome-dark retro design system (Iosevka, square corners, hard shadows, pink as the signal color) for local web apps. Zero runtime dependencies: one minified CSS file with the fonts base64-inlined (~200 KB total), 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
  icons.json       icon registry — every stroke icon used across the four apps
  ds.js            DS.toast / DS.dialog / DS.confirm / DS.prompt / DS.tabs / DS.closeMenus / DS.icon
fonts/             Iosevka woff2 (lean UI subset, ligatures kept) + license
scripts/
  fetch-fonts.mjs  downloads Iosevka, subsets to the UI charset (only step that needs network)
  build.mjs        concatenates src/*.css, inlines fonts + icons, minifies via esbuild → dist/
dist/
  ds.css           the single-file bundle (~200 KB, subset fonts inlined, minified)
  ds.js            helpers + injected icon registry (minified)
preview/
  poster.html      dark-mode recreation of the poster, built from real components
  gallery.html     every component, interactive
  icons.html       icon overview — every glyph, grouped, with source + usage

Commands

npm install       # one-time (subset-font + esbuild devDependencies)
npm run fonts     # fetch Iosevka woff2 + license (network), subset locally
npm run build     # rebuild dist/ds.css + dist/ds.js (stamped with version + git hash)
npm run preview   # serve repo root at :8123 → /preview/gallery.html, /preview/icons.html

Fonts

npm run fonts downloads the full latin woff2 per weight, subsets it to the UI charset and writes the small result to fonts/ — the build itself stays offline. The subset covers basic latin, the latin-1 supplement (umlauts, ß — German and most western European text), latin extended-A (central/eastern European letters) and the symbols the UI uses (– — ' ' " " … · × ÷ › ‹ ✕ ■ □ ● ○ → ← ↑ ↓ ✓ ✗); Iosevka's code ligatures are kept. Anything outside the charset falls back to ui-monospace, Menlo, monospace per glyph — widen the ranges in scripts/fetch-fonts.mjs and re-run npm run fonts if an app ever needs more.

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

  1. Gitea → Settings → Applications → create a token with package:write (publishing) or package:read (consuming).
  2. Copy .npmrc.example to .npmrc and export GITEA_NPM_TOKEN="<token>", then npm publish — or just push to main and let the Gitea Actions workflow (.gitea/workflows/publish.yml) publish when the version changes.

Consumer repos

All four apps declare "@nullwert/design-system" and consume it directly — no copy scripts, no per-repo npmrc:

repo how the package is used
dl-manager serveAsset() resolves /ds.css /ds.js from node_modules/…/dist/, falling back to the committed copies in public/
ebook build.rs sets DS_CSS_PATH/DS_JS_PATH (node_modules, falling back to committed src/assets/ds.*); include_str!(env!(…)) embeds them
launchctl same as ebook, fallback dir static/
dashboard static page, <link>/<script> straight into node_modules/…/dist/

Registry auth lives once in ~/.npmrc (not per repo):

@nullwert:registry=https://gitea.think-jeckel.de/api/packages/nullwert/npm/
//gitea.think-jeckel.de/api/packages/nullwert/npm/:_authToken=${GITEA_NPM_TOKEN}

${GITEA_NPM_TOKEN} is a package:read token exported in the shell. Without it, installs fail; apps still build and run against their committed fallback 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
git push                 # CI publishes; or `npm publish` locally

# in each app repo
npm update @nullwert/design-system
cargo build              # Rust apps only — build.rs re-resolves and re-embeds

dl-manager picks the new files up on its next restart (it reads them per request). The first line of every served ds.css identifies its exact build: /*! @nullwert/design-system v0.1.0+89934b3 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
--info / --violet #7a9cc4 / #9a86c8 extended status (info text, special labels)
--font Iosevka, ui-monospace, Menlo, monospace everything
--shadow / --shadow-sm 4px 4px 0 / 2px 2px 0 black hard offset shadows

Components

  • app shell: .app-header sticky page header (› optional .tabs nav, .spacer), .content page column (.content--narrow = 680px reading width), layout conventions under App shell & layout
  • window: .win (+ .win--flat) › .win-bar / .win-body / .win-foot, .display inverted heading chunk, .widget-body
  • badges: .badge, --solid, --accent, --inverse, --ok/--warn/--err
  • buttons: .btn, --primary, --danger, --icon, --sm; .filelabel upload
  • inputs: .input, .textarea, .select, .field label, .field-row, .checkline
  • data: .tbl (+ .tbl--stack responsive, td[data-label]), .rowlist › .row, .card, .history-row-style rows are app-level
  • nav: .tabs › .tab(.active), .tabbar bottom nav, .crumbs
  • overlays: dialog.dlg › .dlg-bar / .dlg-body / .dlg-foot, .dlg-x close button, .toasts › .toast(--info/--warn/--err/--out)
  • status: .dot(--ok/--warn/--err/--busy), .pill, .statusline, .warnbox
  • content: .md rendered markdown (headings, lists, quotes, alerts, tasklists, tables, code), .diff-file › .diff-bar / .diff-line(.add/.del/.ctx) with .word-add/.word-del
  • menus: .dropdown › .dropdown-menu(--right) › .dropdown-title / .dropdown-item(.active) / .dropdown-sep; [data-tip] css-only tooltip (data-tip-below to flip it down)
  • boards: .heatmap › .cell(.l1…l4) + .heatmap-legend; .board › .board-col › .board-col-bar(.count) / .board-col-body › .board-card(.done)
  • misc: .progress (<b> fill / <i> square marker), .logview, .empty, .dither(--fine/--dense/--halftone), .burst, .cursor, .muted/.faint/.accent/.nowrap/.mono-file, .icon-svg inline stroke icons

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)}
DS.icon(name, cls?)                                 -> inline SVG string (from src/icons.json)
DS.hydrateIcons(root?)                              // replace <i data-icon="name"> placeholders
DS.icons                                            // the raw registry object

Dialogs created by ds.js get a .dlg-x ✕ close button in the titlebar (closing a confirm/prompt counts as cancel). ds.js also wires light-dismiss for every dialog.dlg in the page: clicking the backdrop closes it (guarded so drags that started inside don't).

Icons

All UI icons of the four apps live in src/icons.json — one entry per glyph with category, source repo, and a usage note. The registry is injected into dist/ds.js at build time; no icon paths are hardcoded in app markup anymore.

DS.icon("download")                 // '<svg class="icon-svg" …><path …/></svg>'
DS.icon("edit", "chev")             // extra class forwarded
DS.hydrateIcons()                   // replaces <i data-icon="name"> placeholders (auto-runs on load)
DS.icons                            // raw registry for lookups

Static markup uses placeholders; classes on the <i> are forwarded to the svg:

<i data-icon="settings"></i>
<i class="type-ic" data-icon="torrent"></i>
  • overview: npm run preview → /preview/icons.html — every glyph grouped (actions / transfer / nav / content / infra / feeds / favicons) with name, source repo, and usage. To change an icon, edit its d in src/icons.json, rebuild, refresh.
  • canonical merges: variants that differed per app were unified — download (was 4 variants), upload (2), edit (2, already identical), clock (Lucide r10 variant kept).
  • favicons are part of the registry too (favicon-* entries) — pink-stroke glyphs on a solid #0c0a0c tile, in the same style as the dashboard tile icons. The build writes each one to dist/icons/favicon-<name>.svg (plus a rasterized dist/icons/apple-touch-icon-<app>.png); apps reference those files from the installed package instead of keeping local copies.
  • most glyphs are Lucide paths (24px viewBox, stroke-width 2, round caps) — keep new icons in that style.

App shell & layout

Conventions shared by the four apps — page CSS may add app-specific rules on top, but start from these:

  • header: <header class="app-header"> — sticky, --panel background, padding: var(--s3) var(--s4) (12px 16px; 12px all around on mobile), gap: var(--s2), flex-wrap for narrow screens. Children in order: .display app name (lowercase) › optional .muted subtitle › optional .tabs nav row (regular .tabs/.tab, vertically centered) › .app-header .spacer › actions (.btn, .input, .filelabel, .pill).

  • main: <main class="content"> — full width for dashboards and grids, plus content--narrow for reading-focused columns (<main class="content content--narrow">, capped at --content-narrow, 680px, centered). The class provides the gutters: padding: var(--s4) (--s3 on mobile) and padding-bottom: var(--s6). Don't hand-roll max-width/padding on main in app CSS.

  • spacing: use the --s1…--s6 tokens instead of bare px values.

  • mobile (≤640px): 12px gutters, and .input, .select, .textarea { font-size: 16px } to prevent iOS zoom-on-focus.

  • icons: use the shared registry — <i data-icon="name"> (hydrated by ds.js on load) or DS.icon(name) for JS-built markup; 15px, 24px viewBox, stroke-width: 2, round caps, currentColor — no emoji in UI chrome:

    <i data-icon="edit"></i>   <!-- renders <svg class="icon-svg">…</svg> -->
    
  • favicon: solid #0c0a0c tile with the #f2a0c6 glyph (stroke-width 2, round caps) — the same glyph the app gets on the dashboard. A solid tile is deliberate: transparency would take on the browser tab strip's color (white in light-mode browsers). Generated from the registry into dist/icons/ by npm run build, referenced straight from the package:

    <link rel="icon" type="image/svg+xml"
          href="./node_modules/@nullwert/design-system/dist/icons/favicon-dl-manager.svg" />
    <!-- apple-touch-icon: solid #0c0a0c PNG, generated alongside -->
    <link rel="apple-touch-icon" sizes="180x180"
          href="./node_modules/@nullwert/design-system/dist/icons/apple-touch-icon-dashboard.png" />
    
  • app name: lowercase everywhere — <title>, header .display, breadcrumbs.

Integration pattern (used by the four 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 in public/, serveAsset() routes /ds.css /ds.js, <link> + <script src>.
  • ebook (axum): files in src/assets/, include_str! handlers for /ds.css /ds.js in web.rs.
  • launchctl (axum): files in static/, same include_str! pattern in main.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 fallback copies

The committed ds.css / ds.js in each app are offline fallbacks for fresh clones without npm install (build.rs and serveAsset prefer the package). Refresh them 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).

Dependencies

Development Dependencies

ID Version
esbuild ^0.28.2
subset-font ^2.9.0
Details
npm
2026-11-03 21:24:49 +01:00
2
208 KiB
Assets (1)
Versions (16) View all
0.9.0 2026-11-08
0.7.0 2026-11-07
0.6.0 2026-11-06
0.5.0 2026-11-06
0.4.0 2026-11-06