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

@net-advantage/jo-ui-theme

v2.1.0

Published

Runtime colour tokens, radius tokens, the Inter and Caveat font tokens, and self-hosted Inter and Caveat faces.

Readme

@net-advantage/jo-ui-theme

Runtime colour tokens, radius tokens, the Inter and Caveat font tokens, and self-hosted Inter and Caveat faces.

The theme follows a strict MVVM split. The model names the theme and the token catalogue. The view-model projects the theme class. The view renders that class around the host children. The stylesheet is the runtime source for every --jo-color-* and --jo-radius-* value.

MVVM

flowchart LR
  Props["ThemeProps"] --> ViewModel["useThemeViewModel"]
  Model["jo-ui-theme.model.ts<br/>theme name and token catalogue"] --> ViewModel
  ViewModel --> View["Theme.tsx"]
  Styles["jo-ui-theme.css<br/>tokens and font faces"] --> View
flowchart LR
  PrefModel["theme-preference.model.ts<br/>preference, resolve, apply, boot script"] --> PrefViewModel["theme-preference.view-model.ts<br/>Observable preference and theme"]
  PrefViewModel --> PrefHook["use-theme-preference.ts<br/>useThemePreference"]
  ToggleProps["ThemeToggleProps"] --> ToggleViewModel["theme-toggle.view-model.ts<br/>Observable toggle view-model"]
  ToggleModel["theme-toggle.model.ts<br/>labels, classes, keys"] --> ToggleViewModel
  ToggleViewModel --> ToggleHook["theme-toggle.use-view-model.ts<br/>useThemeToggleViewModel"]
  ToggleHook --> ToggleView["ThemeToggle.tsx"]
  PrefHook --> ToggleView
  Styles2["jo-ui-theme.css<br/>toggle tokens"] --> ToggleView

| Layer | File | Responsibility | | --- | --- | --- | | Contract | jo-ui-theme.types.ts | Theme name, props, and view-model | | Model | jo-ui-theme.model.ts | Normalize light or dark, and publish the token and font catalogues | | View-model | jo-ui-theme.view-model.ts | Project the theme class | | View | Theme.tsx | Render the theme root and import the runtime stylesheet | | Contract | theme-toggle.types.ts | ThemeToggle props, option, and view-model | | Model | theme-preference.model.ts | Normalize and resolve the preference, apply it to a root, and the boot script | | Model | theme-toggle.model.ts | Toggle labels, class names, and radio-group keys | | View-model | theme-preference.view-model.ts | Observable preference and theme, storage, and the system scheme | | View-model | theme-toggle.view-model.ts | Observable toggle view-model, clicks, and keys | | Hook | use-theme-preference.ts | Connect the preference after mount and subscribe | | Hook | theme-toggle.use-view-model.ts | Pass the latest props and subscribe | | View | ThemeToggle.tsx | Render the radio group or the compact button, controlled or stored | | Styles | jo-ui-theme.css | Define light and dark tokens and the self-hosted font faces |

Theme preference and ThemeToggle

  • useThemePreference() returns preference (light, dark, or system), the resolved theme, and setPreference. The default is system, which follows prefers-color-scheme and updates when it changes. The first render is system and light, the same as the server; the stored choice is read after mount.
  • The choice is stored in localStorage under jo-theme. Choosing system removes the key.
  • The resolved theme is applied to <html>: the theme-light or theme-dark class, data-theme, and color-scheme. Pass theme to <Theme> so the slice root matches.
  • themeBootScript is the same logic as an inline script. Put it in a <script> in the document head, before the stylesheet, so the stored theme is on <html> before first paint. If storage throws, it applies the system theme.
  • ThemeToggle is a radio group of Light, Dark, and System. Arrow keys, Home, and End move and select. Without preference it calls useThemePreference itself. With preference and onPreferenceChange it is controlled.
  • <ThemeToggle compact /> is one native icon button showing the current mode's icon. Each click moves light → dark → system → light; the view-model owns the cycle. Its aria-label names the current and next mode, for example Theme: Dark. Switch to System. It keeps the focus ring, disabled, and a 44px hit area around the 36px face.
  • The selected option is --jo-ui-theme-toggle-selected-bg (--jo-color-selected-strong, #14224a / #9db6ef) with --jo-ui-theme-toggle-selected-fg (--jo-color-selected-strong-text, #ffffff / #121419). Label and icon contrast is 15.47:1 in light and 9.10:1 in dark. Against the toggle surface the selected pill is 15.47:1 in light and 8.01:1 in dark; the old dark #1b2436 on #13203a was 1.04:1.
  • Compact matches the header theme button in Anlien's mocks (journey-of-sarah.html #themeBtn). Tokens, set on .jo-ui-theme-toggle--compact:

| Token | Value | From the mock | | --- | --- | --- | | --jo-ui-theme-toggle-compact-size | 36px | width/height: 36px | | --jo-ui-theme-toggle-compact-touch-size | --jo-ui-theme-toggle-option-height (44px) | not in the mock; touch target | | --jo-ui-theme-toggle-compact-radius | 9px | border-radius: 9px | | --jo-ui-theme-toggle-compact-border-width | 1px | 1px solid | | --jo-ui-theme-toggle-compact-border-color | --jo-color-year-bar-border (#e5e7eb / #3a4150) | #e5e7eb, dark #3a4150 | | --jo-ui-theme-toggle-compact-bg | --jo-color-year-bar-surface (#ffffff / #1c212b) | #fff, dark #1c212b | | --jo-ui-theme-toggle-compact-hover-bg | --jo-color-hover (#f0f2f6 / #242938) | none on the button; shell control hover | | --jo-ui-theme-toggle-compact-icon-size | 15px | svg 15×15, stroke 2 | | --jo-ui-theme-toggle-compact-icon-color | --jo-color-icon-strong (#1c2333 / #e9e6de) | sun #1c2333, dark #e9e6de | | --jo-ui-theme-toggle-compact-moon-color | --jo-color-theme-toggle-moon: --jo-color-literal-6495ed in dark (5.43:1), --jo-color-icon-strong in light (15.70:1), since #6495ed is 2.97:1 on white | moon stroke #6495ed | | --jo-ui-theme-toggle-compact-focus-width / -offset | 2px / 2px, --jo-color-focus | 2px #6495ed ring on the other journey pages |

const { preference, theme, setPreference } = useThemePreference();

<Theme theme={theme}>
  <ThemeToggle preference={preference} onPreferenceChange={setPreference} />
  <ThemeToggle compact preference={preference} onPreferenceChange={setPreference} />
</Theme>

Models

flowchart TD
  Props["ThemeProps"] --> Name["ThemeName"]
  Name --> ClassName["theme class"]
  Catalogue["JO_COLOR_TOKENS, JO_SHADOW_TOKENS, and JO_RADIUS_TOKENS"] --> Styles["runtime stylesheet"]
  Fallback["TEXT_MUTED_FALLBACK"] --> Styles
  Fonts["Inter and Caveat"] --> Styles
  • A missing theme is light.
  • theme="dark" adds theme-dark. Otherwise the root is theme-light.
  • The official fallback for --jo-color-text-muted is #667085. The light theme defines that value. The dark theme defines #9ca8bf.
  • --jo-color-text is the Sarah mock's reading colour. Light is #1c2333 (body). Dark is #e9e6de (html.dark body). Both meet WCAG AA for normal text on --jo-color-surface and --jo-color-surface-alt.
  • --jo-color-text-secondary is the supporting gray. Light is #4a5565, the computed colour of the mock note's text-gray-600 (rgb(74, 85, 101)). .invite-role and .ja-sel still write #4b5563, and that hex is not the token. Dark is #c3c9d4, from html.dark .text-gray-600. It meets WCAG AA for normal text on --jo-color-surface and --jo-color-surface-alt in both themes. Ratios and the light-theme miss on --jo-color-surface-dark are in docs/text-secondary.md.
  • --jo-color-surface-dark is a dark surface in both themes (#13203a light, #0b1220 dark), alongside the theme's own --jo-color-surface and --jo-color-surface-alt.
  • --jo-color-scrim and --jo-color-on-scrim paint overlays and the Button overlay variant.
  • --jo-color-text-muted and --jo-color-surface are the muted text and surface colours. Light muted is #667085. Light surface is #ffffff.
  • Badge success is --jo-color-badge-success-bg and --jo-color-badge-success-text: light #e6f6ee / #0d8a54, dark #1e3b25 / #9ce2a5. Badge danger background is --jo-color-badge-danger-bg (light #c55262, dark #ff8c98, the same values as --jo-color-danger). Badge danger text is --jo-color-badge-danger-text (#ffffff in both themes).
  • --jo-color-on-primary is text on a primary action. Light is #ffffff. Dark is #0d1838.
  • --jo-color-shell-primary is the shell primary action fill. Light is #172750. Dark is #9bbaff. Dark shell chrome is charcoal: --jo-color-shell-bg and --jo-color-shell-surface are #191c24, and --jo-color-shell-content-bg is #121419. --jo-color-shell-navy is #14224a in both themes. The other --jo-color-shell-* tokens are the shell chrome colours from the shell stylesheet, including #f5f7ff, #16483f, and #a7f3d0, the wordmark, the demo cards, and the mobile dark palette. --jo-color-shell-on-navy stays #ffffff in both themes, so avatar and missing-page text do not follow --jo-color-on-primary (dark #0d1838). Footer shadow is split into --jo-shadow-shell-footer-a and --jo-shadow-shell-footer-b so each layer can keep a comma-free fallback.
  • --jo-color-button-primary-modal-hover is #0f1a38 in both themes. --jo-color-button-ghost-text is #1c2333 in light and #e7ecf8 in dark. --jo-color-icon-error is #b42318 in both themes.
  • Button and badge colours that were still hard-coded now have tokens with the same values. Primary hover is #162448 light and #b3c6ff dark. Secondary hover background is #eef3fa / #223552, and the border is #c8d2e3 / #3a507a. Modal fill is #14224a. Text on a solid button is #ffffff (--jo-color-button-on-solid). Modal muted text is oklch(0.551 0.027 264.364) in light and the dark text-muted override is #b9c4da. Destructive fill is #d94a5a, hover #c43f4e. Ghost hover is #f1f5f9 (the old slate-100) in light and #23314d in dark. Chip success border is #b7ebc9 / #2f6e32. Chip danger fill is #fff1f3 / #3b1d27 and its border is #f3c3c9 / #6e3140. Badge success and danger read the badge tokens, including the dark success pair #1e3b25 / #9ce2a5.
  • Colour-picker gradient stops are --jo-color-picker-stop-white (#ffffff), --jo-color-picker-stop-white-fade (rgb(255 255 255 / 0)), --jo-color-picker-stop-black (#000000), and --jo-color-picker-stop-black-fade (rgb(0 0 0 / 0)). Both themes use the same stops. The hue stops (--jo-color-picker-hue-red through --jo-color-picker-hue-magenta) are the same in both themes. Picker chrome is --jo-color-picker-border #d9dee8 / #3b4c70, --jo-color-picker-border-strong #c9d2e2 / #51638a, --jo-color-picker-text #1f2a44 / #ecf2ff, --jo-color-picker-muted #7c8699 / #a9b8d4, --jo-color-picker-focus #2f9eca / #57b4da (this is not --jo-color-focus), and --jo-color-picker-field-bg #ffffff / #132547. The thumb ring is --jo-shadow-picker-thumb.
  • Role tokens from the canonical shell palette, defined here and not yet wired through the controls: --jo-color-control-border #e5e7eb / #3a4150, --jo-color-hover (and --jo-color-ghost-hover, --jo-color-secondary-hover) #f0f2f6 / #242938, --jo-color-navy #14224a in both themes, --jo-color-link #14224a / #8eb4f2, selected background and foreground --jo-color-selected-bg #eef1fa / #1b2436 and --jo-color-selected-fg #14224a / #9db6ef, --jo-color-selected-strong #14224a / #9db6ef with --jo-color-selected-strong-text #ffffff / #121419, icon --jo-color-icon-fg #8891a3 in both themes, --jo-color-icon-strong #1c2333 / #e9e6de, --jo-color-icon-active #14224a / #9db6ef, --jo-color-unread #14224a / #9db6ef, --jo-color-count-badge #b42318 in both themes, --jo-color-today-ring #8e9bb3 / #6a7b99, success --jo-color-success #166534 / #86efac and --jo-color-success-bg #eef6f1 / #1a2c26, and the switch specimen --jo-color-switch-off #d1d5db / #6a7b99, --jo-color-switch-on #14224a / #9db6ef, --jo-color-switch-knob #ffffff / #b7bec9. The role table is in docs/shell-colours.md. Existing shell, year-bar, and button tokens keep their current values.
  • Swatch chrome is --jo-color-swatch-border #d9dee8 / #3f5278, --jo-color-swatch-bg #ffffff / #0f1c38, --jo-color-swatch-selected #1a2550 / #dce6ff, --jo-color-swatch-disabled #c3cad8 / #5a6d91, --jo-color-swatch-add #667085 in both themes, and --jo-color-swatch-apply-bg #f8fafe / #172744. The add control does not use --jo-color-text-muted, because that token is #9ca8bf in dark. Icon glyph is --jo-color-icon-glyph #667085 / #cfd9ec. Icon selected is --jo-color-icon-selected #c5d8ff / #5d7fca. Icon border, well, and disabled reuse the swatch tokens that already have those exact pairs.
  • Overlay shadows, the same in both themes: --jo-shadow-dialog is 0 24px 60px rgb(0 0 0 / 0.35), --jo-shadow-confirm-dialog is 0 16px 40px rgb(15 23 42 / 0.28), --jo-shadow-menu is 0 12px 32px rgb(15 23 42 / 0.16), --jo-shadow-side-panel is 0 12px 40px rgb(15 23 42 / 0.28), and --jo-shadow-chooser is 0 8px 24px rgb(15 23 42 / 0.15).
  • Dialog, confirm dialog, dropdown menu, and side panel read the overlay shadow tokens, with the previous shadow as the fallback. The other tokens above are the replacements for colours that are still hard-coded.
  • Year-bar tokens paint the timeline bar and its controls. Light values come from the Sarah mock: track #ffffff, surface #ffffff, border #e5e7eb, text #374151, muted #9ca3af, active #14224a, active text #ffffff. Dark values: track #191c24, surface #1c212b, border #3a4150, text #e9e6de, muted #8b93a3, hover #242938, active #9db6ef, active text #121419.
  • Empty years use --jo-color-year-bar-muted. Years with moments use --jo-color-year-bar-text. Light muted #9ca3af on the white track is about 2.54:1, below WCAG AA. Anlien's palette treats muted as decoration, so the token keeps the mock's gray.
  • Unselected span labels use --jo-color-year-bar-span-text: #6b7280 in light and #e9e6de in dark, matching the mock's mode buttons. They do not use the muted token.
  • The mock does not define a light hover for the year bar. Light --jo-color-year-bar-hover is #f3f4f6, the mock's existing hover gray.
  • Navy and white for a selected year are the existing year-bar active tokens: light active #14224a with active text #ffffff. Dark active is #9db6ef with active text #121419.
  • Year-bar metrics are the same in both themes: inline padding 12px, group gap 12px, year gap 4px, pill padding 3px 6px, pill min width 3rem, pill radius 20px, pill type 11px at line-height 1.5. Block padding is --jo-year-bar-padding-block (6px, a 38px bar) below 1500px, and --jo-year-bar-padding-block-wide (8px, a 42px bar) from 1500px. The bar radius is --jo-radius-control (12px). A pinned bar reads --jo-year-bar-sticky-top (0px) and --jo-year-bar-z-index (2).
  • Compact year-bar metrics, also the same in both themes, follow the Sarah mock's phone and tablet year strip: --jo-year-bar-compact-max 768px, --jo-year-bar-hit-min 44px, --jo-year-bar-pill-min-width-compact 58px, --jo-year-bar-pill-font-size-compact 12.5px, --jo-year-bar-pill-hit-border 6px, --jo-year-bar-pill-radius-compact 999px, --jo-year-bar-year-gap-compact 0px, --jo-year-bar-year-padding-inline-compact 8px, and --jo-year-bar-year-fade 18px.
  • --jo-font-family is Inter, then ui-sans-serif, system-ui, -apple-system, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif. Inter is the Sarah mock's body face. The longer stack is the fallback when Inter is not installed. The mock's own fallback is only system-ui, sans-serif. :root, .theme-light, .theme-dark, and .jo-ui-theme set font-family to the token. --jo-ui-control-font-family points at the same token so controls follow it.
  • --jo-font-family-handwritten is Caveat, cursive. Caveat is the mock's logo accent and note-card face. cursive is the fallback when Caveat is not installed. The logo accent uses the token.
  • How Inter is loaded, which weights ship, and why this package does not add @fontsource-variable/inter are in docs/font-family.md.
  • --jo-radius-sm is 6px. --jo-radius-control is 12px. --jo-radius-card is 16px.
  • Moment-card metrics shipped in 1.4.5 and are the same in both themes. They match the Journey Details track: --jo-moment-card-max-width is 196px and --jo-moment-card-min-width is 150px. Cards share the row up to the maximum and shrink to the minimum before the track scrolls. From 1500px the minimum becomes the maximum, so each card holds 196px. A consumer repeats both values as the fallback in var(--jo-moment-card-*, <value>), so a 1.4.4 stylesheet still lays the cards out.
  • Date-picker metrics shipped in 1.4.4 and are the same in both themes. There is no separate date-picker palette: the control reads the shared surface, text, accent, focus, and danger tokens, and dialog surface tokens when it is inside a dialog. --jo-date-picker-hit-min is 44px. --jo-date-picker-control-height is 2.5rem, matching a text input, and grows to the hit minimum from --jo-date-picker-compact-max (420px) down so a 375px screen keeps 44px targets. Popover padding and gutter are 0.5rem. --jo-date-picker-z-index is 30. The date picker repeats each of these values as the fallback in var(--jo-date-picker-*, <value>).

One-line import

Load the stylesheet once at the application root:

import '@net-advantage/jo-ui-theme/style.css';

That import defines the tokens for :root (light) and .theme-dark, sets --jo-font-family and --jo-font-family-handwritten, and registers Inter 400, 500, 600, 700, and 800 plus Caveat 500, 600, and 700 with font-display: swap. Those are the same weights the Sarah mock requests. Do not load these faces from Google Fonts, and do not add a second Inter from @fontsource-variable/inter. Details are in docs/font-family.md.

Inter and Caveat are licensed under the SIL Open Font License. The license texts ship in licenses/OFL-Inter.txt and licenses/OFL-Caveat.txt.

View-model observable

Observable<T> is the jo-ui view-model state primitive. It is framework-free: value plus subscribe(listener), which returns an unsubscribe function. createObservable(initial) adds set. useObservable(observable) reads it with useSyncExternalStore.

Packages should import the subpaths so they do not load this stylesheet:

import { createObservable } from '@net-advantage/jo-ui-theme/observable';
import { useObservable } from '@net-advantage/jo-ui-theme/use-observable';

The package root exports the same two functions for callers who already load the theme.

Publish footprint

This package publishes dist, README.md, and licenses.