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

@duabalabs/rn-ui

v0.1.1

Published

Shared React Native UI foundation for Duabalabs apps: token contract, themed components and auth flows.

Readme

@duabalabs/rn-ui

Shared React Native UI foundation for Duabalabs apps.

Scope, decided deliberately: DuabaConnect and apps built after it. gob3-app and truthora/app are out of scope — they are on different styling stacks (StyleSheet.create and Ignite ThemedStyle) and migrating them costs more than it returns. That is a decision, not a backlog item; please don't relitigate it without a new reason.

Status: v0.1 — theme, components, auth and shared pages. Proven by DuabaConnect, which consumes all of it.

Published on npm under @duabalabs/, like the other nine packages. In use by DuabaConnect.


What's here

| | | |---|---| | theme/ | | | tokens.ts | The token contract — 34 semantic colour roles | | contrast.ts | WCAG maths and auditTheme | | defaults.ts | Neutral, brand-free base palette | | define-theme.ts | Brand spec → complete audited theme | | store.ts | Theme preference state (the only resolver of "system") | | provider.tsx | ThemeProvider — injects values, owns the scheme | | components/ | Text · Heading · Button · Input · PasswordInput · FormField · Pressable | | auth/ | AuthProvider + AuthAdapter, and four screens: sign-in, sign-up, forgot-password, reset-password | | shell/ | decideDestination / useSessionDestination, useWhenAllReady, navigationTheme | | settings/ | SettingsSection · Row · Switch · Segmented · Busy · AppearanceSection | | screens/ | OnboardingScreen · EmptyState · NotificationCard | | tva.ts | tailwind-merge fix for the role-named type scale | | tailwind-preset.js | withRnUi() — class names, type scale, content glob |

What is deliberately not here

  • Navigation. Screens take callbacks, never a router. Apps standardise on one router for their own reasons; a UI package that imports one pins every consumer to its version.
  • Sessions. The auth adapter has no tokens, no storage, no current user. The app owns the session because the app is what needs it afterwards. That split is why adopting these screens does not mean adopting an auth store.
  • Profile editing and account deletion. Shaped entirely by the backend. The settings scaffolding makes them short to write per app, which is the useful half.
  • Domain components. Channel avatars, network badges, calendar views. The bar for adding a category beyond the six above should be high.

Install

npm install @duabalabs/rn-ui
// tailwind.config.js
const { withRnUi } = require('@duabalabs/rn-ui/tailwind-preset');

module.exports = withRnUi({
  presets: [require('nativewind/preset')],
  content: ['./app/**/*.{tsx,ts}', './components/**/*.{tsx,ts}'],
});

Wrap the config — don't just add the preset. Tailwind's resolveConfig merges theme and plugins from a preset but drops its content (verified against tailwind 3.4). So a preset cannot make Tailwind scan this package's own files, and every class inside it renders unstyled with no error. withRnUi() appends the glob to the app's config, where it survives.

// babel.config.js — required, see src/tva.ts
alias: { '@gluestack-ui/nativewind-utils/tva': '@duabalabs/rn-ui/tva' }

That is all of it. A registry install is a plain directory under node_modules, so the package resolves react, react-native and nativewind to the app's own copies — no Metro configuration, no tsconfig paths.

The source lives at apps/duabaforge/packages/rn-ui, beside the product that owns it, the same shape as apps/sellub/packages/*.

Testing an unpublished change

Link it, and expect three things the registry path does not need:

// package.json
"@duabalabs/rn-ui": "file:../../duabaforge/packages/rn-ui"
// metro.config.js — a `file:` dep is a symlink Metro will not otherwise follow,
// and without nodeModulesPaths the package can resolve a second React.
config.watchFolders = [path.resolve(__dirname, '../../duabaforge/packages/rn-ui')];
config.resolver.nodeModulesPaths = [path.resolve(__dirname, 'node_modules')];
config.resolver.unstable_enableSymlinks = true;
// tsconfig.json — a symlinked package resolves a SECOND copy of react-native's
// types, and NativeWind's className support is a `declare module`, which
// attaches to one module identity. Without these, every className inside the
// package fails to typecheck while the app's compile fine. Do NOT make it a
// wildcard: mapping `react` the same way bypasses @types/react and breaks all JSX.
"react-native": ["./node_modules/react-native"],
"nativewind": ["./node_modules/nativewind"]

A link also inflates the bundle, because the linked package's own node_modules contributes duplicate modules — worth ~2 MB in DuabaConnect. Unlink before measuring anything.

This package ships TypeScript source, not a build. NativeWind resolves className at build time in the consuming app, so a compiled dist/ would ship components whose className props do nothing. That is the exact failure that killed the sixteen @duabalabs/sellub-vendure-* packages — "source-only mirrors that never worked when consumed from npm". Don't add a build step.


Defining a theme

import { defineTheme, ThemeProvider, configureThemeStore } from '@duabalabs/rn-ui';
import * as SecureStore from 'expo-secure-store';

export const theme = defineTheme({
  accent: '#612BD3',
  accentSecondary: '#C22FC6',
  dark: { bg: '#0E0E0E', surface: '#1A1919' },
  extend: { slotDayBg: { light: '#F4F4F6', dark: '#212020' } },
});

configureThemeStore({
  storage: SecureStore,          // any { getItem, setItem }
  storageKey: 'myapp.theme',
  systemFallback: 'dark',        // dark-first apps
});

Supply an accent; the rest falls out of the defaults. Anything you set explicitly is never second-guessed by a derivation.

extend tokens become CSS variables and Tailwind classes exactly like contract tokens — they just aren't referenced by shared components or checked by the audit. Anything app-specific (calendar slots, tab chrome, brand marks) goes there rather than into the contract.


The audit

import { auditTheme, formatFindings, hasErrors } from '@duabalabs/rn-ui';

const { light, dark } = auditTheme(theme);
if (hasErrors(light)) throw new Error(formatFindings(light, 'light'));

Put this in a test. It is the reason the contract exists: DuabaConnect shipped nine failing pairs that all looked fine by eye, including body text at 3.9:1 and an accent used as link text at 2.57:1.

Two rules worth knowing:

  • Foregrounds are measured against the hardest surface they can land on, not a nominal background. Passing on white and failing on the grey card is a failure. Writing the neutral defaults, this caught textTertiary at 4.38:1 on surface2 — values carried over from a palette that had already been hand-audited once.
  • error blocks, warn informs. Text, icons and button labels are errors. Hairlines, field outlines and disabled labels are warnings, because a 3.0:1 divider is a visible bar and no design system complies. A noisy audit is an ignored audit.

Why accent and textAccent are separate

A brand colour chosen to carry white text as a button fill is almost never readable as text on a dark background. #612BD3 is 7.51:1 as a fill and 2.57:1 as dark-mode link text. One value cannot serve both roles.

defineTheme derives textAccent by walking lightness until it clears 4.5:1 against that mode's actual surfaces, scaling saturation as it goes so the hue survives and neutrals stay neutral. From #612BD3 it produces #9D77ED — within a few percent of the value that was reached by hand.


Development

npm install
npm run typecheck
npm test

The peer deps are also devDeps, pinned to exactly the versions DuabaConnect runs. That is not redundancy: a file: dependency is a symlink, and TypeScript resolves a symlinked package's imports from its REAL location — so without its own node_modules the package sees no react at all, every import degrades to any, and the consuming app inherits hundreds of implicit-any errors. Metro is told to prefer the app's copies at runtime via resolver.nodeModulesPaths.

Tests are plain assertions under sucrase-node — no runner, no React Native environment. That is why they import only the pure modules (tokens, contrast, defaults, define-theme) and never provider or vars.


Publishing

Public npm, @duabalabs/ scope, one GitHub repo per package — the convention in PACKAGES.md. prepublishOnly runs typecheck, tests and the packaging check, so a broken package cannot go out.

npm run verify-package

This packs a real tarball, extracts it somewhere unrelated to this working tree, and checks the things that only break once the package is consumed from a registry instead of a symlink: that every exports entry actually shipped, that no dist/ snuck in, that the Tailwind content glob still resolves after being moved, that nothing under src/ reaches for a Node builtin Metro cannot bundle, and that no peer dependency got published inside the tarball.

It exists because deferring the first publish defers discovering all of that. Sixteen @duabalabs/sellub-vendure-* packages were deprecated and deleted for being "source-only mirrors that never worked when consumed from npm" — every one of them looked fine locally right up to the end. Run this on every change and the wait costs nothing.

It does not replace a real install, and 0.1.0 proved it: the first registry install threw ERR_PACKAGE_PATH_NOT_EXPORTED because the exports map made package.json itself unreachable, which plenty of tooling reads. Fixed in 0.1.1, and now one of the checks. For anything structural, still do one end-to-end pass:

npm pack --pack-destination /tmp
cd ../../../duabaconnect/duabaconnect-mobile
npm install /tmp/duabalabs-rn-ui-<version>.tgz
npx tsc --noEmit && npx expo export --platform ios
npm install @duabalabs/rn-ui@latest   # restore

Conventions this package must keep

  1. Tokens are named for roles, never colours. accent, not purple. The moment a token is named after its value it stops being a contract.
  2. Nothing under src/ may import a Node builtin. Metro bundles this code; tailwind-preset.js runs in Node and is the only file allowed path. The type scale lives in src/theme/type-scale.js precisely so both worlds can reach it.
  3. Screens never navigate. They take callbacks and return decisions. The one exception is documented where it occurs: reset-password navigates because resetting creates no session, so no guard takes over.
  4. "system" is resolved in exactly one place — store.ts. DuabaConnect shipped a build that rendered perfectly and was completely invisible because two resolvers disagreed about Android's "unspecified" colour scheme. The provider consumes resolved, never preference.
  5. The preset's TOKENS list and TOKEN_NAMES must match. They are separate because the Tailwind CLI can't read TypeScript; test/theme.test.ts fails if they drift.
  6. Consumers wrap with withRnUi(). A bare preset cannot contribute content, and the symptom of getting this wrong is silent: unstyled components, no error anywhere.