woosign-system
v0.5.0
Published
WooBottle 'Paper & Ink' design system — cross-platform React Native + Web components (cream canvas, ink surfaces, ember CTAs, ceremonial gold).
Maintainers
Readme
woosign-system
WooBottle "Paper & Ink" design system — one API, two platforms.
Warm cream canvas · deep-ink surfaces · ember CTAs · ceremonial gold. Cross-platform React Native + Web, hand-tuned to the WooBottle spec.
The brand, in one paragraph
WooBottle is a warm, calm, product-trust interface system. It starts from a soft cream canvas instead of stark white, then layers in a tiered ink-and-ember hierarchy so emphasis is earned by role — not shouted through saturation. Buttons are full-pill. Cards are 12px islands. Shadows are a whisper. It's coffeehouse-adjacent: grounded, breathable, confident.
Quick look
| Role | Token | Hex |
|---|---|---|
| Page canvas | colors.canvas | #F4EFE6 |
| Section surface | colors.section | #EAE4D8 |
| Card island | colors.card | #FFFFFF |
| Inverse surface | colors.inverse | #171513 |
| Brand ink | colors.brand | #2A2622 |
| Primary CTA | colors.actionPrimary | #D35B1F |
| Ceremonial gold | colors.gold | #C98A3C |
| Error | colors.actionDanger | #B02818 |
Install
pnpm add woosign-system
# or
npm i woosign-systemHost app also needs peer deps: react, and optionally react-native if
you're shipping to iOS/Android.
Use it
import { ThemeProvider, Button, Card, Badge } from 'woosign-system';
export function App() {
return (
<ThemeProvider>
<Card>
<Badge variant="gold">Members</Badge>
<Button onPress={() => order()}>Order now</Button>
</Card>
</ThemeProvider>
);
}The same code renders on web and native — platform extensions
(.web.tsx / .native.tsx) switch implementations automatically.
Components
Core
| | Variants |
|---|---|
| Button | default, secondary, outline, ghost, dark, inverse, destructive, link |
| Card | default (white island), outline, ghost, warm, ceramic, inverse |
| Badge | default, secondary, brand, gold, success, reward, outline, destructive |
| Input | default, error · sm / default / lg |
| Switch | default · sm / default / lg |
| Text | h1–h4, p, lead, large, small, muted |
| Box | Flex-first layout primitive with padding/margin/gap/radius tokens |
Brand primitives
| | Purpose |
|---|---|
| Chip | Square-cornered tag — default, solid, outline |
| Pill | Selectable filter — active / inactive, Pressable |
| Tabs | Underline tab rail, light + inverse surfaces |
| Fab | 56px floating action button — ember / ink / gold, layered shadow |
| FeatureBand | Deep-ink, ember, or reward feature band — the brand's hero surface |
| Progress | Gold/ember/ink fill on light or inverse rail |
| StatusDot | Tinted circle wrapper for icons — success / danger / brand / neutral |
| Toast | Floating notification with leading StatusDot |
| Eyebrow | Tracked, uppercased label — default / brand / gold / inverse |
| Divider | Hairline separator, horizontal or vertical, light or inverse |
Overlays
| | Purpose |
|---|---|
| Dialog | Controlled modal — portal scrim (web) / RN Modal (native), Esc & Android back, Header/Title/Description/Body/Footer |
| DialogProvider / useDialog | Imperative layer over Dialog — await useDialog().confirm({...}) → Promise<boolean>, .alert({...}) → Promise<void>, queued one-at-a-time |
| BottomSheet | Controlled bottom sheet — drag-to-dismiss grabber handle, content-based height with maxHeightRatio cap, same subcomponent API |
All components expose the same ButtonProps/CardProps/etc. on both
platforms — TypeScript is the contract.
Design tokens
One source of truth for both platforms (src/core/theme/tokens.ts):
import { colors, typography, borderRadius, shadows, wbSpace } from 'woosign-system';
colors.actionPrimary // '#D35B1F'
borderRadius.pill // 999 — buttons are ALWAYS pill
typography.fontSize.headingMd // { size: 24, lineHeight: 36 }
wbSpace[4] // 24 — WooBottle's named spacing scale
shadows.card // layered low-alpha card elevationShadcn-compat aliases (primary, secondary, muted, ring, …) are
preserved so existing integrations keep working.
Dark mode
Wrap your app (or a subtree) in <ThemeProvider> and the converted components
follow the active scheme:
import { ThemeProvider, useResolvedColors } from 'woosign-system';
<ThemeProvider defaultColorScheme="dark">
<App />
</ThemeProvider>Without a ThemeProvider, components render the light palette exactly as before —
fully backward compatible. Component styles read theme colors via the
useResolvedColors() hook (theme colors under a provider, static light fallback
otherwise). All components consume theme colors, so wrapping any subtree in
<ThemeProvider> switches it to dark. Storybook has a Light/Dark toolbar toggle.
Fonts
Web — drop an @font-face rule pointing at woosign-system/src/assets/fonts:
@font-face {
font-family: 'Woobottle';
src: url('woosign-system/src/assets/fonts/Woobottle-Regular.woff2') format('woff2');
font-display: swap;
}React Native — one-time setup after install:
npx react-native-assetFonts get linked into Xcode + UIAppFonts and copied into
android/app/src/main/assets/fonts/.
Then cross-platform access via the helper:
import { resolveFontFamily } from 'woosign-system';
<Text style={{ fontFamily: resolveFontFamily('display') }}>
A warmer kind of morning.
</Text>Playground
Web Storybook
pnpm storybook # → http://localhost:6006
pnpm build-storybook # static export → storybook-static/Native Storybook (iOS / Android)
pnpm storybook:native:generate
pnpm storybook:native:ios # or :androidDev commands
pnpm build # build with react-native-builder-bob (cjs + esm + dts)
pnpm typecheck # tsc --noEmit
pnpm lint # eslint src/**
pnpm test # jest smoke tests (tokens + resolveFontFamily)CI & release
- CI —
.github/workflows/ci.ymlruns typecheck, lint (core + components), smoke tests, build, and verifies the published tarball has no stories / examples / duplicated font assets. Triggered on every push and PR. - Release —
.github/workflows/release.ymlpublishes to npm with provenance when av*.*.*tag is pushed. Requires theNPM_TOKENsecret and annpmGitHub environment (for approval gating if you want it).
Cutting a release:
npm version patch # bumps package.json + tags
git push --follow-tagsProject layout
src/
├── assets/fonts/ Woobottle display + signature faces
├── core/
│ ├── theme/ tokens · types · ThemeContext
│ ├── variants/ createVariants (shared web/native)
│ ├── utils/ platform · colors · resolveFontFamily
│ └── hooks/ useTheme
├── components/
│ ├── Button/ Component.tsx + .web.tsx + .native.tsx
│ ├── Card/ Badge/ Input/ Switch/ Text/ Box/
│ └── Chip/ Pill/ Tabs/ Fab/ FeatureBand/
│ Progress/ StatusDot/ Toast/ Eyebrow/ Divider/
└── examples/ Marketing page + mobile app composed from the libraryEach component ships:
Component.tsx— platform-agnostic facadeComponent.web.tsx— pure React + inline stylesComponent.native.tsx— React Native primitivesComponent.styles.ts— shared variantstypes.ts— the contract
Principles we don't bend
- Buttons are full-pill. No square, no "slightly rounded" buttons.
- Cream over white. The page is
#F4EFE6, never#FFFFFF. - Green is role-specific. Ink for bands, brand for headings, ember for CTAs. Don't swap.
- Gold is ceremonial. Rewards, loyalty, achievements. Never a generic accent.
- Hover is for promise, not drama. No lift, no scale on hover. Press is
scale(0.95). - Shadows whisper. Layered, low-alpha — never one heavy drop.
