Foundations
FoundationsTier 1 · Tokens
Constants
Icon sizes, image ratios, currency and the other numbers the components agree on.
bunx --bun shadcn@latest add @woohl/constantsReference
Installs this item, its registry dependencies and the token layer.
bunx --bun shadcn@latest add @woohl/constantsregistry/tier-1-tokens/lib/constants.ts
/** Brand constants that are rules, not preferences. Changing one is a change to
* the published system, not to this file. */
/* ------------------------------------------------------------------- logo */
/**
* Minimum rendered heights, in px. Below the lockup minimum the wordmark stops
* resolving — a 16px bird reads, a 16px lockup is mud. `WoohlLogo` enforces
* this by falling back to the bird rather than by warning.
*/
export const LOGO_MIN_SIZE = {
/** Horizontal lockup, on screen. */
horizontal: 24,
/** Stacked lockup, on screen. */
stacked: 48,
/** Bird alone — favicon, app icon, avatar, watermark. */
bird: 16,
} as const;
/**
* X on all four sides, where X is the bird's height. May drop to half on a
* crowded surface, no further. If you can't afford half, use the bird alone.
*/
export const LOGO_CLEAR_SPACE_RATIO = 1;
export const LOGO_CLEAR_SPACE_RATIO_TIGHT = 0.5;
/** The mark needs 3:1 contrast against whatever sits behind it. */
export const LOGO_MIN_CONTRAST = 3;
/* ------------------------------------------------------------------ icons */
/**
* Three sizes. Never 19px, never 22px — an off-grid icon renders a fuzzy
* stroke, and it is visible.
*/
export const ICON_SIZES = {
/** Inside a chip, a table cell, a small button. */
compact: 16,
/** The default. */
default: 20,
/** Section headers, empty states, feature rows. */
prominent: 24,
} as const;
export type IconSize = keyof typeof ICON_SIZES;
/**
* Lucide's default stroke is a flat 2. It stays 2 at every normal size and
* eases to 1.6 only above 40px, where a flat 2 starts to look heavy.
*/
export const ICON_STROKE = 2;
export const ICON_STROKE_DISPLAY = 1.6;
export const ICON_STROKE_DISPLAY_THRESHOLD = 40;
/* ---------------------------------------------------------------- imagery */
/**
* Three ratios, plus full-bleed. Anything else is a decision somebody will have
* to un-make later.
*/
export const IMAGE_RATIOS = {
/** The default. Feed posts, product cards, editorial. */
portrait: "4 / 5",
/** Grids, thumbnails, avatars. */
square: "1 / 1",
/** Video, banners, deck slides. */
landscape: "16 / 9",
/** Story and Reel covers. */
story: "9 / 16",
/** Hero slides and campaign openers. Requires a scrim if type sits on it. */
bleed: "auto",
} as const;
export type ImageRatio = keyof typeof IMAGE_RATIOS;
/** The four subjects almost every Woohl photograph belongs to. */
export const PHOTO_SUBJECTS = [
"the maker at work",
"the object in a real home",
"the material macro",
"the place, wide",
] as const;
/* ---------------------------------------------------------- accessibility */
/** WCAG 2.2 AA — the standard Woohl holds across product, marketing and email. */
export const CONTRAST = {
/** Body copy, labels, anything under 24px. */
text: 4.5,
/** 24px+, 19px bold, icons, borders, focus rings. */
large: 3,
/** UI component boundaries. */
ui: 3,
/** AAA, where the system can reach it. */
enhanced: 7,
} as const;
/** Every interactive control, including invisible padding. */
export const TOUCH_TARGET_MIN = 44;
/* ----------------------------------------------------------------- colour */
/**
* Orange coverage on any one surface. The exception is a full-bleed CTA panel,
* where orange IS the surface and everything on it is white.
*/
export const ACCENT_MAX_COVERAGE = 0.1;
/** The target mix for a composition: paper, ink, accent. */
export const SURFACE_RATIO = { paper: 0.7, ink: 0.2, accent: 0.1 } as const;
/* ----------------------------------------------------------------- motion */
/** Stagger step in ms, and the point past which staggering stops helping. */
export const STAGGER_STEP_MS = 60;
export const STAGGER_MAX_ITEMS = 6;
/* ------------------------------------------------------------------ money */
/**
* Woohl prices in rupees, formatted the Indian way — ₹1,20,000, not ₹120,000.
* `Intl` gets the grouping right with the `en-IN` locale; hand-rolled
* formatting does not.
*/
export const CURRENCY = { code: "INR", symbol: "₹", locale: "en-IN" } as const;
export function formatPrice(
amount: number,
options: { showDecimals?: boolean; locale?: string } = {},
): string {
const { showDecimals = false, locale = CURRENCY.locale } = options;
return new Intl.NumberFormat(locale, {
style: "currency",
currency: CURRENCY.code,
minimumFractionDigits: showDecimals ? 2 : 0,
maximumFractionDigits: showDecimals ? 2 : 0,
}).format(amount);
}
This item exports no props — it is a stylesheet, a helper or a set of constants.
Registry items
None — this item stands on its own.
npm packages
None beyond React itself.