npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@old-school-chronicle/vellum

v0.6.3

Published

OSC "Vellum" brand tokens, fonts, vm- component CSS and icon set — framework-agnostic, shared by the Foundry module and the web app

Readme

@old-school-chronicle/vellum

The OSC Vellum brand token layer — one source of truth for colour, type, spacing, radii, and the "vellum"/brass-gold surface treatments shared across the OSC ecosystem (the Foundry character-sheet module and the web app).

Extracted from the Vellum system in the osc-character-sheet repo (OSC-77).

What's in it

  • tokens.css — the primary artifact. Plain CSS custom properties on :root (dark theme, default) and [data-theme="cream"]. Framework-agnostic, self-contained, zero dependencies. This is the source of truth for styling.
  • src/tokens.scss + src/_scales.scss — the SCSS source. _scales.scss holds the type/radius/spacing scale maps; tokens.scss loops them to emit the --fs-*/--r-*/--spacer-* custom properties. tokens.css is generated from these (pnpm build), so the scales can't drift.
  • tokens.js + tokens.d.ts — a convenience JS mirror (scale maps + raw palette) for JS/TS consumers (inline styles, Tailwind theme extension, a token reference UI). Not required for styling.
  • fonts.css and components.css — opt-in; see Fonts and Components.
  • icons, icons/react — the OSC icon set (game-icons.net, CC BY 3.0): path data, iconSvg() markup, item/class defaults and credits; and an optional React OscIcon. See ICONS.md.

Tokens include: the teal/crimson palette + warm brass-gold accent (--accent-alt, --gold*), surface ramp (--bg/--surface*/--border*), text ramp, ink stamp, type families (--display/--serif/--sans/--mono), the --fs-* type scale, --spacer-* spacing, --r-* radii, line-heights, the --vellum-grain paper texture, and semantic/functional tokens (--hit/--damage/--magic/--gold/ --on-accent/--section-rule), the *-ink accents for text (--teal-ink/--danger-ink/ --accent-alt-ink, AA on every ground in both themes — --teal/--crimson stay fills), and the --tint-hover/--tint-press/--tint-warm overlays and --stamp-drop. Legacy aliases (--brass/--oxblood/…) are kept so downstream components can migrate incrementally.

Install

pnpm add @old-school-chronicle/vellum

Zero dependencies. Inside this monorepo, use "@old-school-chronicle/vellum": "workspace:^".

Consume

Import the CSS once at your app's entry (works in Vite, Astro, plain bundlers — no framework coupling):

import "@old-school-chronicle/vellum/tokens.css";

Import order matters: any stylesheet that reads these custom properties assumes they are already on :root, so this import comes first.

Or pull the SCSS source if you want the raw scale maps:

@use "@old-school-chronicle/vellum/scales" as scales; // src/_scales.scss — $type-scale, $radius, $spacers
@forward "@old-school-chronicle/vellum/tokens"; // src/tokens.scss   — emits the custom properties

Or the JS object:

import { palette, typeScale, spacers } from "@old-school-chronicle/vellum";

Switch themes by setting data-theme="cream" on any ancestor element (defaults to the dark theme on :root).

The role layer (roles.css)

Opt-in, additive, and visually inert on its own. A font stack answers what faces exist; it does not answer what jobs exist. roles.css declares that middle layer — eight type roles (--font-head/-name/-key/-figure/ -status/-bar/-ui/-prose), their size roles plus --fs-statkey / --fs-statsub, six --case-* tokens, and ten density tokens — each assigned to the value tokens.css already produces.

import "@old-school-chronicle/vellum/tokens.css";
import "@old-school-chronicle/vellum/roles.css"; // after tokens.css

Components read roles, never families. That is the whole point: a style direction (or a font swap) becomes a token diff on one selector instead of a survey of every component. The seam is words vs figures, not key vs value — a key is words ("Hit Dice"), a value may be digits ("13") or words ("Ready"), and mono only earns its place where digits align in a column. Roles compose and the more specific wins: a figure inside a bar still takes --font-figure.

Density is the second axis, independent of type: data-density="compact" | "airy" on the scope element or any subtree below it. --row-min-h sits in that group but never drops below 44px — density trades whitespace, not reachability.

Emitted on :root like the rest of the package. To confine it to a subtree, use the SCSS source instead of the CSS:

@use "@old-school-chronicle/vellum/roles.scss" with (
  $scope: ".osc-sheet"
);

No look lives here. The terminal direction is a preset in its consumer.

Tailwind v4 (OSC-139 spike)

Additive. tokens.css is untouched and stays the zero-dependency path — nothing below is required to consume Vellum.

Entry points

| Entry | What it is | | -------------------------------------------- | ----------------------------------------------------------- | | @old-school-chronicle/vellum/tokens.css | Custom properties only. No Tailwind, no build step. | | @old-school-chronicle/vellum/theme.css | The @theme block alone — no Tailwind import, no opinions. | | @old-school-chronicle/vellum/roles.css | The role layer. Opt-in, visually inert on its own. | | @old-school-chronicle/vellum/postcss-scope | PostCSS plugin that confines the output to a root selector. |

theme.css maps every colour to var(--…) from tokens.css, so utilities keep following data-theme="cream". Load tokens.css first or the colours resolve to nothing.

That block is @theme inline, and the inline is load-bearing. A plain @theme lands on :root, so --color-bg: var(--bg) is substituted there and the resulting colour inherits down as a fixed value. A descendant setting data-theme="cream" moves --bg, but nothing re-substitutes — so utilities stay on the dark palette while hand-written CSS using var(--bg) switches, and half the UI changes theme. With inline, bg-surface emits var(--surface) directly and resolves against the element. The theme scope is normally a descendant (a web app toggling a wrapper; the Foundry sheet owning a subtree rather than the document), so this is the case that matters.

There is deliberately no preset entry point. prefix(), layer() and source() are import options — a package cannot parametrise them, so shipping a preset just hard-codes the three things each consumer must own. Write the entry yourself; it is six lines.

The consumer entry — two shapes, and the choice is not cosmetic

A. An app whose document you own (a web app, Storybook). Layer the utilities. Precedence is then decided by layer order rather than specificity, so your own component CSS beats a utility by design and nothing can surprise you.

@layer theme, base, components, utilities;
@import "@old-school-chronicle/vellum/tokens.css";
@import "tailwindcss/theme.css" layer(theme);
@import "@old-school-chronicle/vellum/theme.css";
@import "tailwindcss/utilities.css" layer(utilities) source(none);
@source "./src";

B. A host DOM you don't own (a Foundry module, a browser extension, an embed). Leave the utilities unlayered and import them last.

@layer theme, base, components; /* note: no `utilities` */
@import "@old-school-chronicle/vellum/tokens.css";
@import "tailwindcss/theme.css" layer(theme) prefix(tw);
@import "@old-school-chronicle/vellum/theme.css";
/* ...your own stylesheets... */
@import "tailwindcss/utilities.css" prefix(tw) source(none); /* LAST, unlayered */
@source "./src";

Why: unlayered CSS beats every layered rule regardless of specificity. In a shared document, any installed third party can ship a global unscoped rule and it will outrank your layered utilities — and no amount of specificity rescues them, because layering is decided before specificity is even consulted.

This is not hypothetical. The Foundry sheet's coin fields collapsed to 28px and clipped their values because the ose-advancedfantasytome module ships:

input[type="number"] {
  width: 2em;
} /* unlayered, unscoped, global */

which beat tw:w-full sitting in @layer utilities. Chasing individual offenders is unbounded — there is no limit on what modules a user installs — so the fix is to leave the layer.

The tradeoff, stated plainly: unlayered, precedence between a utility and your own CSS falls back to specificity then source order. A scoped utility lands at (0,2,0), so a bespoke rule with three classes beats it. Importing utilities last handles ties; anything more specific needs to be a deliberate choice.

prefix(tw) belongs with shape B for the same reason — Foundry ships its own .hidden/.flex/.active, and unprefixed utilities collide in both directions.

fixtures/plain.css (A) and fixtures/prefixed.css (B) are exactly these, and tools/verify.mjs compiles both on every check — so the snippets can't rot.

The escape hatches

  • Layer order — the @layer line must be the first statement, above every import, or precedence falls back to first-encounter order. That's where the surprises come from when injecting into a host DOM. Declaring base is free even though nothing here fills it: an empty layer just reserves the slot, and it gives a consumer's own reset somewhere to sit that utilities still beat. Tailwind hoists its own @layer properties; above yours — expected, it reserves the lowest slot for the @property polyfill.

  • Prefix — set it on the tailwindcss imports; it reaches Vellum's @theme too (--tw-color-surface, tw:bg-surface).

  • Layer assignment — layer(theme) / layer(utilities), or omit to sit unlayered against a host stylesheet.

  • Preflight — off, always. tailwindcss/base.css is imported nowhere in this package; the Foundry sheet's all: unset reset must survive. Preflight-off is not the same as no universal selector: Tailwind still emits *, ::before, ::after, ::backdrop to polyfill @property fallbacks. It only assigns --tw-* variables and sits behind an @supports gated to old Safari/Firefox, so it is inert on Chromium — but it lives in @layer properties, which a scoping post-pass must therefore also cover.

  • Content scanning — pin source(none) and list @source explicitly. An unscanned class emits nothing, with no error, and default detection will happily invent utilities out of prose in a comment. "It built" is not evidence.

  • Root scoping — Tailwind has no primitive for this, so it's a post-pass:

    postcss([vellumScope({ root: ".osc-sheet", layers: ["theme", "utilities", "properties"] })]);

    :root/html collapse onto the root selector, everything else nests under it.

Token lock-in

theme.css resets --color-*, --font-*, --text-*, --spacing, --radius-* and --leading-* to initial before declaring Vellum's. Tailwind's stock palette and its dynamic spacing scale are gone: p-7 and text-slate-500 don't compile, and off-scale must be written p-[28px] — greppable in review. tools/verify.mjs asserts this on every check.

Fonts (fonts.css)

Opt-in: @import "@old-school-chronicle/vellum/fonts.css"; declares the @font-face rules for IM Fell English SC, IM Fell English (regular, italic), Libre Franklin (variable, both styles) and JetBrains Mono, with the woff2 files and OFL notices beside it in fonts/. tokens.css stays tokens-only. The url()s are ./-relative so a bundler resolves them; under Tailwind's PostCSS plugin, import it from its own stylesheet, since Tailwind's @import inlining does not rebase URLs.

IM Fell English SC carries ascent-override: 98.02%, descent-override: 28.83% and line-gap-override: 0%, or its caps sit 7.4% of an em high in a fixed-height box.

Components (components.css)

Opt-in, framework-agnostic component CSS. Every class is vm- prefixed (daisyUI owns the bare names) and every rule sits in @layer components, reading Vellum tokens only. Rules restate every property they rely on, because the Foundry sheet's base layer unsets form controls. For a host DOM, run it through postcss-scope with layers: ["components"]; tools/components.test.mjs proves the result stays under .osc-sheet.

Sources are src/components/_*.scss, one partial per group, wired by src/components.scss.

  • vm-paper — the --vellum-grain paper texture. Opt-in; background-attachment is the consumer's call.
  • Type roles: vm-title, vm-heading (+ vm-heading-sm), vm-action, vm-folio, vm-prose, vm-flavor, vm-help; atoms vm-display, vm-caps, vm-serif, vm-mono, vm-dim, vm-mute. Sans (vm-help) is the voice for UI prose; upright Fell (vm-flavor) is only for in-world flavor; italic only for emphasis within text.
  • Buttons (_buttons.scss): vm-btn with vm-btn-primary, -danger, -secondary, -link, -quiet; vm-bevel for raised controls. Add vm-btn-muted to vm-btn-secondary or vm-bevel for a quieter face mixed toward its surface; set --btn-ground when that surface isn't --bg.
  • Data (_data.scss): vm-table (comfortable; vm-table-dense), with vm-table-num, vm-table-select, vm-table-sort + vm-table-sort-icon (driven by the th's aria-sort), and two phone modes — vm-table-scroll (a wrapper that scrolls) or vm-table-stack (rows become label/value lists from each cell's data-label). A row is selected via vm-selected, aria-selected or a checked control in its vm-table-select cell, and disabled likewise. vm-meter (-head, -label, -value, -track, -fill, -mark, -caption; vm-meter-sm, vm-meter-warn, vm-meter-over) — put role="meter" and the aria-value* on the track. vm-tag-edit + vm-tag-remove and vm-tag-input in a vm-tag-list. vm-bulk-bar (-count, -actions, -clear), sticky to the viewport bottom; render it after the list.
  • Icons (_icons.scss): vm-icon with tints vm-icon-dim/-gold/-blood/-mute/-ink, and slots vm-icon-slot + vm-icon-well/vm-icon-gold-edge (or vm-stamp). Emitted by OscIcon from ./icons/react; see ICONS.md.

Contrast

--text-mute and every *-ink token reach 4.5:1 on --bg, --bg-2, --surface and --surface-2 (composited over --bg, since they are translucent) and on the -solid twins in both themes. There is no fainter text step: use --hair/--hair-soft for decoration. tools/contrast.test.mjs asserts it.

Excluded (deliberately)

  • The .themed / theme-scope wiring and Foundry window-frame theming.
  • The enricher / rich-text CSS (rich-text.scss).
  • Modals, menus, popovers and tooltips: consumers compose them.
  • Element resets.

Maintenance

Edit scales in src/_scales.scss and other tokens in src/tokens.scss, then regenerate the CSS:

pnpm --filter @old-school-chronicle/vellum build   # sass -> tokens.css, theme.css, roles.css, components.css
pnpm --filter @old-school-chronicle/vellum check   # drift diff on every artifact + escape-hatch smoke test
pnpm --filter @old-school-chronicle/vellum test    # postcss-scope, contrast and component-layer tests

theme.scss loops the same _scales.scss maps as tokens.scss, so the utility scale and the custom-property scale are one source.

Everything is generated; nothing here is hand-kept. tokens.css and theme.css come from the SCSS, and tokens.js is derived from tokens.css — so the chain SCSS → tokens.css → tokens.js is diffed end to end by check. tokens.js used to be hand-maintained and had silently drifted a full type step; that is now structurally impossible.

tokens.js also derives the palette, families and line-heights, which are literals in tokens.scss and never lived in _scales.scss — deriving from the built CSS rather than the Sass maps is what closes that half of the hole.