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

@tomscaria/consumer-fintech-design-system

v2.5.0

Published

Consumer fintech design system — design tokens, a multi-theme runtime (earth, arcade, revenant, primitive/studio, kiosk) with product/marketing expressions, kit CSS primitives, a Tailwind preset, and thin React component wrappers.

Readme

consumer-fintech-design-system

A consumer-fintech design system by Scaria, Inc. — design tokens, a multi-theme runtime, a Tailwind preset, kit CSS primitives, and React component wrappers for the web and for React Native.

One system, encoded in two formats:

  • agent/ — machine-readable atomic files any agentic tool (Claude, Cursor, Code) can read and build from.
  • tokens/ + styles.css + preset.cjs — the runtime a product ships: CSS variables, per-theme blocks, and the Tailwind preset.

Themes

Composed on <html>/<body> via data-theme (identity: color + type) and data-expression (rhythm: spacing, hero scale, motion pace — product | marketing).

| Theme | Register | |---|---| | earth-light / earth-dark | Warm consumer-fintech — parchment, graphite, chartreuse | | arcade-light / arcade-dark | Prediction-market / liveness — indigo, near-black grounds | | revenant-light / revenant-dark | American-Dynamism ops — concrete, onyx, signal orange | | primitive / primitive-dark | The Studio — Scaria flagship marketing identity | | kiosk | High-contrast, touch-first |

<html data-theme="earth-light" data-expression="marketing">

Install

npm install @tomscaria/consumer-fintech-design-system

Published to the public npm registry — no token, no .npmrc, no auth of any kind. A plain npm install (or pnpm add / yarn add / bun add) works in any project. (GitHub Packages also carries older mirrored versions; ignore it — npmjs.org is the canonical registry.)

Two setups, one package — read this before the quickstart

This package targets the web and React Native, and the two targets are set up differently on purpose. Getting that straight first will save you an afternoon, because the difference is not cosmetic — it is where the theme lives.

On the web the theme is CSS. You import stylesheets and put data-theme on <html>. There is no provider component, because there does not need to be: a CSS custom property already cascades, already re-scopes on any subtree, and already costs nothing to change. Adding a React context on top of a mechanism the platform gives you for free would be a second source of truth for the same value.

On React Native the theme is a React context. React Native has no cascade and no custom properties, so something has to carry resolved values down the tree. That is ThemeProvider, and it is what makes Box, Stack, Text and Pressable able to take token names as props.

What both targets share is the component layer. Button, Panel, StatStrip, MarketCard, Sparkline and the rest are exported under the same names from the same specifier on both platforms; Metro resolves the native renderers through the react-native export condition and every other bundler resolves the DOM renderers. Each pair is a redesign rather than a port — sharing a name is only honest if the phone version was designed for a phone — but the import and the prop API are one.

So the honest summary is: one component vocabulary, two theming models. The quickstarts below are separate because the setup is separate, and each one only uses symbols that actually resolve on its own platform. If you would rather read the symbol lists than the prose, docs/PLATFORM-INVENTORY.md is generated from the shipped type declarations on every build.

Quickstart — web

Three stylesheet imports and one attribute. Nothing else is required, and no part of this needs React until you want the components.

// 1. The runtime. `/styles` is fonts + every theme + the brand layer.
import '@tomscaria/consumer-fintech-design-system/styles';
// 2. The component classes. REQUIRED — see "Which CSS an application imports".
import '@tomscaria/consumer-fintech-design-system/css/kit';
// 3. The layout kernel: .surface / .stack / .row / .grid.
import '@tomscaria/consumer-fintech-design-system/css/layouts';

import {
  SysHeader, LiveDot, Panel, PanelHeader, PanelBody,
  StatStrip, StatCell, Button, Badge,
} from '@tomscaria/consumer-fintech-design-system';

export function App() {
  return (
    <div className="stack" data-gap="lg" data-pad="lg">
      <SysHeader wordmark="ACME MARKETS" status={<LiveDot />} />
      <Panel>
        <PanelHeader title="Positions" trailing={<Badge tone="live">LIVE</Badge>} />
        <PanelBody>
          <StatStrip columns={3}>
            <StatCell label="Net" value="$12,071.60" delta="+2.4%" deltaTone="pos" />
            <StatCell label="Open" value="14" />
            <StatCell label="Today" value="$418.02" accent />
          </StatStrip>
          <Button variant="accent">Review</Button>
        </PanelBody>
      </Panel>
    </div>
  );
}

Set the theme on the document element, not in JavaScript:

<html data-theme="earth-light" data-expression="product">

What the web does not have. There is no ThemeProvider, Box, Stack, Text or Pressable on this entry, and importing them will not typecheck. Their jobs are done by data-theme, by the layout kernel classes (.stack, .row, .grid), by your own type styles or the Tailwind preset, and by <button>. Six symbols exist only on web — cssVar, cssTint, ScoredBasketTable, ScoredBasketHeader, SCORED_BASKET_GRID, SCORED_BASKET_MIN_WIDTH — because they are CSS-cascade tools and a table, and neither idea survives the trip to a phone.

To change theme at runtime, set the attribute:

document.documentElement.dataset.theme = 'earth-dark';

Quickstart — React Native

One provider at the root, then the terse prop vocabulary underneath it. Metro resolves the native build from the same specifier the web uses.

import { ThemeProvider, VStack, Text, Box, Pressable, PortalHost } from '@tomscaria/consumer-fintech-design-system';

export function App() {
  return (
    <ThemeProvider theme={{ light: 'earth-light', dark: 'earth-dark' }} followSystem>
      <VStack p="lg" gap="md" bg="bg">
        <Text role="header-lg">Positions</Text>
        <Box bg="bg-2" p="md" radius="r-lg" gap="sm">
          <Text role="body-sm" color="fg-muted">Net</Text>
          <Text role="mono-md" tabular>$12,071.60</Text>
        </Box>
        <Pressable onPress={() => {}} haptic="light">
          <Text role="header-caps">Review</Text>
        </Pressable>
      </VStack>
      {/* Sheet, Dialog and ToastStack render through here. Mount it once, last. */}
      <PortalHost />
    </ThemeProvider>
  );
}

p, gap, bg, radius and color take token keys, not raw numbers: p="lg" resolves through the theme's spacing scale under the current expression, and bg="bg-2" resolves through its colour map. A bare number is still accepted and means points, so an escape hatch exists — but a screen written in token keys re-themes and a screen written in numbers does not.

What React Native does not have. No CSS, so no data-theme, no className, and no stylesheet imports; the /css/* subpaths are web-only and importing one under Metro is a build error. Button accepts as="a" and href so a shared screen still typechecks, and ignores them.

Before your first screenshot, read Fonts. React Native has no font fallback chain and no error for an unregistered family, so a skipped font step renders every screen in the system face and looks almost right.

Subpath exports

Every subpath the package publishes. scripts/verify-doc-contracts.mjs fails the build if this table falls behind package.json.

| Import | What | |---|---| | . | React component wrappers — DOM renderers, or the native kernel + native renderers under Metro | | ./styles | Aggregate CSS: @font-face + every theme block + motion, dataviz, patterns, brand layer | | ./css | The token layer alone: @font-face + the nine theme blocks, nothing else | | ./css/kit | Required with the web components. The .panel / .btn / .stat-strip / .badge / .sys-header / .log-row classes they emit | | ./css/components | Component-state rules the kit needs — disabled, loading, focus. Ships typed as styles.css.d.ts | | ./css/layouts | The layout kernel: .surface / .stack / .row / .split / .grid / .inline / .center | | ./css/dataviz | Chart and data-mark classes plus the categorical palette tokens | | ./css/focus | The keyboard indicator: .focus-ring / [data-focus-ring], a two-colour ring that clears 3:1 on all nine themes. Already inside ./styles; this subpath is for taking it alone | | ./css/primitives | Deck/slide primitives (frame, vrail, stoplight, matrix, venn). For decks and preview pages, not for an application | | ./tailwind (alias ./preset) | Tailwind preset (.cjsrequire and import both work) | | ./themes/earth-light, ./themes/earth-dark | One theme's colors_and_type.css, for a page that ships exactly one theme | | ./themes/arcade-light, ./themes/arcade-dark | As above | | ./themes/revenant-light, ./themes/revenant-dark | As above | | ./themes/primitive, ./themes/primitive-dark | As above | | ./themes/kiosk | As above | | ./tokens.json | Machine-readable token summary | | ./tokens/theme | Every theme as resolved JS data — real hex, real numbers, zero var() | | ./tokens/ansi | The 256-colour ANSI table, for a Node process writing to a TTY. Not for the DOM: there is no index→CSS direction, and the xterm cube is lossy against the hex the theme ships | | ./tokens/dtcg/* | Raw DTCG token files | | ./behavior | Platform-free state machines — one build serves web and native. See docs/BEHAVIOR.md | | ./format | Money (currency + scale), the price/probability duality, sentiment and countdown. Pure, one build serves both, Intl reached only through an injected provider | | ./flows/cashier | The deposit flow: 11 screens plus the machine that sequences them. Deposit only — see /flows/cashier | | ./native/kernel | The React Native kernel. Metro gets the renderers; node and web bundlers get the platform-free token/breakpoint/touch-target math | | ./backtest | Performance analytics + SVG charts on web; analytics only on React Native | | ./fonts/* | Font files. .woff2 for the web, .otf for iOS and Android | | ./agent/* | Raw atomic kit files — themes, component specs, visual patterns | | ./dataviz | Data-viz class layer (same file as ./css/dataviz, kept for existing imports) | | ./patterns | Operational UI patterns: stat panel, status grid, reveal, orbit | | ./orbfield, ./orbfield.js | The opaque-coins hover-reveal pattern: stylesheet and its mount script | | ./leadrow | The hover-to-somewhere list row | | ./brand-layer | The Scaria constants across every theme: crosshair cursor, paper grain, section dial, kicker, ticker | | ./botw | Lore-only marketing flair overlay. Opt in with data-flair="botw"; deliberately not in ./styles | | ./glyphs/ledger | The ledger glyph sheet (SVG sprite) | | ./motion, ./motion.json | Motion utility classes, and the same durations/easings as data | | ./magic_trick | The system's own explainer | | ./brands/lore/magic_trick, ./brands/revenant/magic_trick | Per-brand explainers | | ./package.json | The manifest |

Which CSS an application imports

Five of the subpaths above are stylesheets, they do different jobs, and two of them have historically been mistaken for each other. The short version:

| You are building | Import | |---|---| | An application, using the React components | ./styles and ./css/kit and ./css/layouts | | An application, using tokens only (Tailwind, your own CSS) | ./styles — or ./css if you do not want motion/patterns/brand-layer | | A chart-heavy surface | add ./css/dataviz | | A slide deck or a preview page | ./css/primitives |

Two points worth stating outright, because both have cost consumers a day:

./css/kit is required, not optional. Button, SysHeader, PanelHeader, PanelBody, StatStrip, StatCell, Badge and LogRow carry no inline var(--token) of their own — they emit a className and the kit stylesheet does all the visual work. Without it they render as unstyled bare elements, which looks like a theming bug and is not one.

./styles does not include the kit or the layout kernel. It is fonts, the nine theme blocks, motion, dataviz, patterns and the brand layer. Importing ./styles alone gives you correct tokens and unstyled components.

Scoped theming

[data-theme] is a plain attribute selector at specificity (0,1,0), and every theme block is written that way. It therefore re-scopes on any element, not just <html> — which is the cheapest thing in this package and the least obvious.

<body data-theme="earth-light">
  <main>…</main>

  <!-- A dark panel inside a light page. No provider, no portal, no CSS of your own. -->
  <aside data-theme="revenant-dark" data-expression="marketing">
    <div class="panel">…</div>
  </aside>
</body>

Everything inside the aside resolves its tokens from revenant-dark. This is how you build an embed, a white-label section, a theme gallery, or a side-by-side comparison. The one caveat is import order: because every theme block and the primitives layer are all at (0,1,0), the last matching rule takes precedence, so import ./css/primitives before ./styles if you use both.

The layout kernel

./css/layouts is the web answer to native's Box and Stack: seven classes driven by data-* attributes, with no per-component CSS to write.

| Class | Shape | |---|---| | .surface | A padded, themed container | | .stack | Vertical flow | | .row | Horizontal flow | | .split | Two children pushed apart | | .grid | Column grid | | .inline | Inline run that wraps | | .center | Centres its child on both axes |

| Attribute | Values | |---|---| | data-gap | none xs sm md lg xl | | data-pad | none xs sm md lg xl | | data-align | start center end stretch baseline | | data-justify | start center end between around | | data-cols | An integer column count, on .grid |

<section class="stack" data-gap="lg" data-pad="lg">
  <div class="row" data-justify="between" data-align="center">…</div>
  <div class="grid" data-cols="3" data-gap="md">…</div>
</section>

Because these are single class selectors at (0,1,0), one of your own class selectors overrides them without !important — so a media query that changes a column count is @media (max-width: 640px) { .my-grid { grid-template-columns: 1fr; } } on your own class, alongside the DS class.

The behavior layer

./behavior is the part of a screen that is not a renderer: reducers, predicates and formatters with no React, no DOM and no React Native in them. One build serves both platforms — there is no .native twin, because there is nothing platform-specific to twin. It is the least advertised and most reusable thing in the package.

Nine machines: orderTicket, quantity, stepFlow, selection, disclosure, sheetDetent, notificationQueue, itemQueue, asyncAction. Each ships as an init* seed, a *Reducer, and a set of pure selectors.

import { useReducer } from 'react';
import {
  initOrderTicket, orderTicketReducer, priceOrder, canSubmitOrder, orderTicketErrors,
} from '@tomscaria/consumer-fintech-design-system/behavior';

export function OrderTicket() {
  const [state, dispatch] = useReducer(orderTicketReducer, undefined, () =>
    initOrderTicket({ side: 'yes', mode: 'buy', orderType: 'market' }),
  );
  const quote = priceOrder(state);

  return (
    <form onSubmit={(e) => e.preventDefault()}>
      <output>{quote.costCents}</output>
      <button type="submit" disabled={!canSubmitOrder(state)}>
        Submit
      </button>
      {orderTicketErrors(state).map((e) => <p key={e}>{e}</p>)}
    </form>
  );
}

That example is a web one on purpose. The layer was written for a React Native app and its prose lived inside a document titled "RN Architecture", which is why a web builder shipped an order ticket without ever finding it. The reference is now docs/BEHAVIOR.md, which is platform-neutral, as the layer is.

/flows/cashier — deposit only

./flows/cashier is a deposit flow: a funding-method chooser, an amount screen, card entry, 3-D Secure challenge, and the four outcome screens, sequenced by cashierFlow. It ships 11 screen components and roughly 45 pure functions with their own test suite, and it works on both platforms.

It is not a withdrawal flow and it will not become one by relabelling. DepositLimits is the only limit type; FundingMethod enumerates four ways to pay; the fee runs on top (totalDebitCents 10175 for a creditCents of 10000), where a payout is the one case in which the opposite convention is the only correct one; and a 3-D Secure challenge has nothing to challenge on the way out. Every string is overridable through CashierCopy, so a flow dressed in withdrawal language will look right in review and reconcile wrong in production. Use it for money in.

import {
  cashierFlow, initCashier, initCashierCtx, cashierTotalCents, cashierFeeCents,
} from '@tomscaria/consumer-fintech-design-system/flows/cashier';

const ctx = initCashierCtx({ amountCents: 10_000 });
const fee = cashierFeeCents(ctx);       // 175
const total = cashierTotalCents(ctx);   // 10175 — amount + fee, debited from the payer

// `cashierFlow` is a StepFlow instance, not a function. The state is external.
const state = initCashier(ctx);
const here = cashierFlow.step(state);            // the current StepDef
const blocked = cashierFlow.blockingMessage(state); // why `next` is unavailable, or null
const onwards = cashierFlow.reducer(state, { type: 'next' });

CashierEffects.charge is what advances the flow out of processing. Omitting it leaves the flow on the processing screen indefinitely, which on a phone is a spinner with no exit.

Money formatters — which one to use

Ten near-identical functions ship with signatures that typecheck in each other's call sites, and they disagree in six places. The table below is generated from the shipped functions on every build, so it cannot go stale and a new formatter cannot be added without appearing here.

Read the columns as: $12,071.60-scale input, a negative, zero, and a price-scale input. The entry column is where the symbol is importable from.

| Function | Defined in | Importable from | 1207160 | -3840 | 0 | 6300 | Groups 1000s | Negative | Clamps 0–100 | |---|---|---|---|---|---|---|---|---|---| | formatAmountCents | components/trade-behind-card/trade-behind-card.logic.ts | root | $12071.60 | -$38.40 | $0 | $63 | no | hyphen | no | | formatAmountDollars | components/trade-behind-card/trade-behind-card.logic.ts | root | $1207160 | -$3840 | $0 | $6300 | no | hyphen | no | | formatAumCents | components/vault-card/vault-card.logic.ts | root | $12.1K | −$38.40 | $0 | $63 | no | U+2212 | no | | formatCents | behavior/quantityInput.ts | /behavior | 12071.60 | -38.40 | 0.00 | 63.00 | no | hyphen | no | | formatCentsGrouped | behavior/quantityInput.ts | /behavior | 12,071.60 | -38.40 | 0.00 | 63.00 | yes | hyphen | no | | formatCentsPlain | components/backtest-replay/backtest-replay.logic.ts | root | $12,071.60 | $38.40 | $0 | $63 | yes | sign dropped | no | | formatChipCents | components/outcome-chip/outcome-chip.logic.ts | root | 100¢ | | | 100¢ | no | clamped to zero | yes | | formatDepthPrice | components/depth-book/depth-book.logic.ts | root | 1207160.0¢ | -3840.0¢ | 0.0¢ | 6300.0¢ | no | hyphen | no | | formatEdgeCents | format/price.ts | not exported | +1207160.0c | −3840.0c | 0.0c | +6300.0c | no | U+2212 | no | | formatMoneyCents | components/live-stat/live-stat.logic.ts | root | $12,071.60 | -$38.40 | $0 | $63 | yes | hyphen | no | | formatNetPct | components/trade-behind-card/trade-behind-card.logic.ts | root | +1207160.0% | -3840.0% | 0.0% | +6300.0% | no | hyphen | no | | formatOddsCents | components/odds-bar/odds-bar.logic.ts | root | 100¢ | | | 100¢ | no | clamped to zero | yes | | formatPriceCents | components/market-card/market-card.logic.ts | root | 100¢ | | | 100¢ | no | clamped to zero | yes | | formatPriceCents | components/tick-widget-mini/tick-widget-mini.logic.ts | root, as tickFormatPriceCents | 100¢ | | | 100¢ | no | clamped to zero | yes | | formatPriceCents | format/price.ts | root | 99¢ | | | 99¢ | no | clamped to zero | yes | | formatPriceDecimal | format/price.ts | not exported | 0.99 | 0.01 | 0.01 | 0.99 | no | clamped to zero | yes | | formatReturnMultiple | format/price.ts | not exported | 1.0x | 100x | 100x | 1.0x | no | clamped to zero | yes | | formatSignedCents | components/backtest-replay/backtest-replay.logic.ts | root | +$12,071.60 | −$38.40 | +$0 | +$63 | yes | U+2212 | no | | formatSignedPercent | format/signal.ts | not exported | +1207160% | −3840% | 0% | +6300% | no | U+2212 | no | | formatSliderCents | components/slider/slider.logic.ts | root | 12,071.60 | -38.40 | 0.00 | 63.00 | yes | hyphen | no |

20 formatter(s), discovered from the AST and measured at build time by scripts/verify-doc-contracts.mjs.

The two you probably want:

  • formatMoneyCents for an amount of money on either platform. It groups thousands, drops a trailing .00, and signs a negative with a plain hyphen.
  • formatSignedCents for a P&L figure in a mono column, where the sign is the point. Note that it emits U+2212 MINUS SIGN rather than a hyphen so that gains and losses align on the character the reader is scanning; do not mix it with the others in one column.

The traps, stated once: formatAmountCents is the only one named for amounts and the only one that does not group thousands. formatCentsPlain discards the sign rather than restyling it, which is correct for a figure whose direction is carried elsewhere and wrong everywhere else. formatSignedCents(0) is +$0. The price formatters clamp to 0–100 because a binary market cannot settle outside it, except formatDepthPrice, which does not clamp — so a money amount handed to a price formatter is silently absorbed by three of them and rendered absurdly by the fourth.

React Native

React Native is a first-class target, not a port target. The specifier is the same on both platforms; what resolves behind it is not.

Metro configuration

One flag, and only if you are on Metro < 0.82 (React Native < 0.79 / Expo SDK < 53), where Metro ignores exports by default:

// metro.config.js
const { getDefaultConfig, mergeConfig } = require('@react-native/metro-config');

module.exports = mergeConfig(getDefaultConfig(__dirname), {
  resolver: {
    unstable_enablePackageExports: true,
    unstable_conditionNames: ['react-native', 'require', 'import'],
  },
});

The full annotated version ships in the package at node_modules/@tomscaria/consumer-fintech-design-system/metro.config.js.

The package works with the flag off too. A top-level "react-native" field covers the bare specifier and generated forwarding stubs (behavior/, native/kernel/, backtest/, flows/cashier/, tokens/theme.js, tokens/ansi.js) cover the subpaths, with .native.js twins where a native flavour exists. Setting the flag is still recommended — conditional exports are how the ecosystem is moving, and it is the only path on which they work at all.

The root recipe

Sheet, Dialog and ToastStack render their content through a portal, and a portal with no host renders nothing, silently. Mount one PortalHost as the last child of your app root:

import { SafeAreaProvider, useSafeAreaInsets } from 'react-native-safe-area-context';
import * as Haptics from 'expo-haptics';
import {
  ThemeProvider, SafeAreaBridge, PortalHost, installExpoHaptics,
} from '@tomscaria/consumer-fintech-design-system';

installExpoHaptics(Haptics); // once, at module scope

export function Root() {
  return (
    <SafeAreaProvider>
      <SafeAreaBridge useInsets={useSafeAreaInsets}>
        <ThemeProvider theme={{ light: 'earth-light', dark: 'earth-dark' }} followSystem>
          {children}
          <PortalHost />
        </ThemeProvider>
      </SafeAreaBridge>
    </SafeAreaProvider>
  );
}

SafeAreaBridge takes useSafeAreaInsets as a prop rather than importing it. That is the seam that keeps react-native-safe-area-context linked by your app instead of by this library: a native module linked twice is a native module linked wrong, and a library that reaches for one takes the decision away from the app that owns the build. The same pattern is why haptics arrive through installExpoHaptics(Haptics) rather than an import.

With neither module installed everything still runs: haptics fall back to Android's Vibration, and safe-area insets read zero — flush content, never unreachable content. Both are deliberate. A missing haptic is imperceptible, and a guessed inset is wrong on every device but one.

TabBar is a bottom bar

TabBar is bottom-anchored on both platforms and caps at five destinations, per the iOS HIG and Material 3. That is a considered limit rather than an oversight: past five, targets fall below the touch floor and labels truncate. It is not a web top nav and does not have a placement prop. Use safeEdge to absorb the bottom inset in a PWA or under a collapsed Safari toolbar.

Fonts — read this before your first screenshot

Fonts are the one thing this package cannot finish for you. Loading a font is an app-level act on both platforms, so a library bundle physically cannot do it. If you skip this section nothing throws: React Native has no font fallback chain and no error for an unregistered family, so every screen renders in the system face and looks almost right until somebody who knows the brand sees it.

Step 1 — load the faces this package ships. Use the .otf files, never the .woff2: WOFF2 is a web wrapper and neither iOS nor Android can load it.

import { useFonts } from 'expo-font';
import { registerFonts, fontAssets } from '@tomscaria/consumer-fintech-design-system';

const [fontsLoaded] = useFonts({
  'Aeonik-Regular': require('@tomscaria/consumer-fintech-design-system/fonts/Aeonik-Regular.otf'),
  'Aeonik-Medium':  require('@tomscaria/consumer-fintech-design-system/fonts/Aeonik-Medium.otf'),
  'Aeonik-Bold':    require('@tomscaria/consumer-fintech-design-system/fonts/Aeonik-Bold.otf'),
  'AeonikMono-Regular': require('@tomscaria/consumer-fintech-design-system/fonts/AeonikMono-Regular.otf'),
  'AeonikMono-Medium':  require('@tomscaria/consumer-fintech-design-system/fonts/AeonikMono-Medium.otf'),
  'LockSerif-Regular':  require('@tomscaria/consumer-fintech-design-system/fonts/LockSerif-Regular.otf'),
});

The keys are the faces' PostScript names, which is exactly what theme.text(role).fontFamily returns — because React Native does not pick a weight out of a family. fontFamily: 'Aeonik' with fontWeight: '700' renders Regular on iOS and faux-bold on Android; fontFamily: 'Aeonik-Bold' renders Bold. fontAssets(resolve) builds that same map from FONT_FACES so it cannot drift.

Prefer bundling at build time? npx react-native-asset works too — point react-native.config.js at the files and nothing further is required at runtime:

// react-native.config.js
module.exports = {
  project: { ios: {}, android: {} },
  assets: ['./node_modules/@tomscaria/consumer-fintech-design-system/fonts'],
};

Step 2 — three families are NOT in this package, and two themes need them.

| Family | Used by | Install | Registers as | |---|---|---|---| | Jersey 10 | arcade-* font-display | @expo-google-fonts/jersey-10 | Jersey10_400Regular | | Anton | arcade-* font-cond | @expo-google-fonts/anton | Anton_400Regular | | Syne | kiosk font-sans, font-display | @expo-google-fonts/syne | Syne_400Regular … |

All three are Google Fonts under the OFL, so a .ttf for them is freely redistributable — they are absent because they were never vendored here, not because they cannot be. Aeonik and Aeonik Mono are different: those are commercially licensed retail faces, and whether you may redistribute them inside your own app is your licence question, not this package's.

The trap, and the reason registerFonts() exists: the name your app registers is not the CSS family name. @expo-google-fonts/jersey-10 registers the face as Jersey10_400Regular, so asking for "Jersey 10" finds nothing even after you have loaded the font perfectly. Only your app knows the real key, so your app hands it over:

import { useFonts } from 'expo-font';
import { Jersey10_400Regular } from '@expo-google-fonts/jersey-10';
import { Anton_400Regular } from '@expo-google-fonts/anton';
import { registerFonts } from '@tomscaria/consumer-fintech-design-system';

const [fontsLoaded] = useFonts({ Jersey10_400Regular, Anton_400Regular });

registerFonts({
  'Jersey 10': 'Jersey10_400Regular',
  Anton: 'Anton_400Regular',
});

What happens if you do nothing. theme.face(role) and theme.text(role).fontFamily return undefined — the platform's own font, said out loud — rather than a family name that would render the identical system face while claiming to be the brand face. Under __DEV__ you also get one console.warn per family naming the package, the export and the exact registerFonts() call.

Two functions report on this, and they are not interchangeable. unresolvedFontFamilies() is an accumulator of misses observed so far, so it returns [] before any text has rendered — it is for a debug screen or a live log. fontSetupSteps() returns the complete static instruction list, every external family, unconditionally; that is the one to assert on if you want a smoke test that fails on a missing registration. Nothing crashes and nothing is silent.

FONT_FACES, EXTERNAL_FAMILIES, nativeFontFace(family, weight) and resolveFontFamily(family, weight) from /native/kernel are the machine-readable form of all of the above.

The backtest chart pack

@tomscaria/consumer-fintech-design-system/backtest exports the same eight chart names on both targets — EquityCurve, Underwater, MonthlyHeatmap, RollingSharpe, MonthlyDistribution, AnnualReturns, MetricsTable, WorstDrawdowns — and Metro resolves the React Native renderers automatically. Both renderers stand on one platform-free geometry layer, so the charts cannot disagree about where a mark goes.

The native charts are not shrunk desktop charts. They are laid out at the measured point width rather than scaled inside a viewBox, they replace hover tooltips with tap-to-inspect into a persistent readout, and two of the eight change SHAPE on a phone rather than shrink: AnnualReturns rotates to one full-width row per year, and MonthlyHeatmap trades its in-cell digits for tap-to-read swatches plus a year-total column. Rendering a chart on native needs react-native-svg.

Per-chart reasoning and the gates are in docs/RN-CHARTS.md.

Prop and value vocabulary

Twelve words for "the list", seven for "what is current", four size unions resolving to four different heights, and three props that can carry an accessible name. docs/VOCABULARY.md publishes all of it: the collection/selection table, the resolved-height table per platform, the cross-platform divergence table (three shipped doc comments cite "divergence 1" and "divergence 3" by number, and this is the document they mean), and the rule that --brand is not a data-mark colour — it computes identical to --fg in kiosk, earth-light and revenant-light, so a price line drawn in it is body text.

Gates

Every gate below runs on npm run build or in CI, and each one exists because something shipped past the others. scripts/verify-doc-contracts.mjs fails if this table falls behind the scripts that exist.

| Gate | What it refuses to let through | |---|---| | verify-tokens.mjs | A hand-edited generated token file, a var() left in the React Native data target, a brand-IP leak into the tarball, a near-duplicate brand colour | | verify-contrast.mjs | A foreground/background pairing the kit actually applies that falls below WCAG 2.1 AA, computed as arithmetic over the token data in every theme. No browser, no rendering | | verify-css.mjs | A stylesheet this package ships that a real CSS parser refuses. patterns.css shipped a var() inside an :nth-child() argument for months — dead code that looked like a feature | | verify-native-rules.mjs | A native renderer breaking the layering rules; a numeric column without an explicit width and alignment; a spec with no implementation | | verify-native-resolution.mjs | A bundle where the react-native condition resolves to the DOM renderers, or the default condition resolves to the native ones | | verify-package.mjs | A subpath in exports that does not resolve from a packed tarball | | verify-ip-boundary.mjs | Confidential brand tokens reachable from a public entry point | | verify-numeric-guards.mjs | A canSubmit-class predicate that returns true when an economic figure the machine derived is not a finite integer. Ships with the native-ergonomics pass | | verify-vocabulary.mjs | bet / wager / stake / any inflection of win in shipped copy or an identifier. This is a finance surface, not a gambling one | | verify-doc-blocks.mjs | A documentation code block that does not compile against the platform it claims — the gate that would have caught the 2.3.0 quickstart | | verify-doc-contracts.mjs | A subpath, a gate, a money formatter or an exported component that the documentation does not mention; a generated table that has drifted |

Documentation

| Document | For | |---|---| | docs/PLATFORM-INVENTORY.md | Which symbols exist on which entry. Generated from the shipped .d.ts on every build | | docs/BEHAVIOR.md | The nine platform-free machines, with web and native usage | | docs/VOCABULARY.md | Prop names, value unions, resolved sizes, cross-platform divergences | | docs/RN-ARCHITECTURE.md | The native layering rules and the reasoning behind each divergence | | docs/RN-CHARTS.md | Per-chart native reasoning and gates | | docs/MOBILE-PATTERNS.md | Screen-level patterns for a phone app | | agent/components/*/**.spec.md | One agent-readable spec per component, with the exported symbol named in front matter |

Migration

Renaming from @tomscaria/scaria-design-system, and the lore-* / rolr-* themes were renamed to earth-* / arcade-* in 2.0.0. The old data-theme="lore-*" values still resolve via deprecated backward-compat aliases, so existing markup keeps working. See MIGRATION.md.

License

MIT — © 2026 Scaria, Inc. Free to use in commercial and proprietary products; attribution required.