@ossy/design-system
v3.10.0
Published
Ossy's design system React component library
Readme
@ossy/design-system
React component library for the Ossy ecosystem — themable UI primitives, layout, forms, and shell extension hooks. Built for React 19+ with server-side rendering and CSS-variable theming from @ossy/themes.
This package is not a home for product-specific or marketing widgets. Domain UI lives in feature packages (for example @ossy/profile, @ossy/resumes, @ossy/booking).
What’s in the box
| Area | Examples |
|------|----------|
| Primitives | Button, Text, Input, InputTitle, Card, Alert, Badge, Tabs, … |
| Layout | View, View.Item, Page, PageSection |
| Forms | Form, FieldFactory, Fields, applyFieldChange, FormStatus, useFormContext, field slot keys |
| Theme | Theme, useTheme (requires @ossy/themes) |
| Shell extension | ComponentSlotsProvider, Slot, useSlot |
| Icons | Icon — kebab-case names, lazy css.gg chunks |
See docs/SLOTS.md for the three different “slot” concepts and AUDIT.md for refactor history and open work.
Four-layer mental model
Use this stack when deciding which API to reach for:
- Tokens & theme — Install
@ossy/themes, wrap the app inTheme, switch palettes withuseTheme. Components readvar(--*)tokens only at the global level. - Layout primitives —
View(and optionallyPage/PageSection) for structure. Layout slots are theslotattribute on children (header,content,sidebar-primary, …). - Shell & form extension — Component slots: app
export const slotsin*.layout.jsx+<Slot />; schema UI via ADR 0006metadata.idon*.component.jsx. Form fields:@ossy/design-system/input/{type}. - Components — Buttons, inputs, feedback, display, navigation helpers.
Rule of thumb: layout regions ≠ injectable app components ≠ form field overrides.
Typography (Text)
Single component for body and headings. Set as for semantics (h1, label, p, …), variant for theme tokens under theme.text, and optional text / params for locale keys. InputTitle is a separate borderless input styled like a heading — use it as a form-field slot override for resource name editing, not as display typography.
Responsive type comes from the theme, not from Text props: @ossy/themes base.media['(max-width: 900px)'].text overrides heading/hero/logo sizes via createCss @media CSS variables. Keep body/default at 1rem so iOS does not zoom focused inputs.
Feedback (Alert)
Theme-aware callout for inline status. Tones come from CSS variables (--color-info, --color-accent for success, --color-warning, --color-danger); neutral uses surface/separator tokens so light and dark themes stay consistent.
| Prop | Purpose |
|------|---------|
| variant | neutral (default), info, success, warning, danger |
| title | Optional uppercase eyebrow above the body |
| icon | Optional leading React node (inline SVG or Icon) |
| onDismiss | When set, renders an accessible dismiss control |
| dismissLabel | aria-label for the dismiss control (default Dismiss) |
| as | Root element (default div) |
| role | Override a11y role (danger defaults to alert, others to status) |
import { Alert } from '@ossy/design-system'
<Alert variant="warning" title="Attention" onDismiss={() => setOpen(false)}>
This action cannot be undone after 24 hours.
</Alert>Status (Badge)
Compact pill for inline status. Variants share Alert’s semantic --color-* mapping (success → --color-accent, warning → --color-warning, …) via color-mix against --surface-base, so badges follow light/dark themes.
| Prop | Purpose |
|------|---------|
| variant | neutral (default), info, success, warning, danger |
| size | s or m (default) |
| label / children | Badge text |
import { Badge } from '@ossy/design-system'
<Badge variant="warning">Pending</Badge>Installation
npm install @ossy/design-system @ossy/themesPeer dependencies
react>= 19.0.0react-dom>= 19.0.0@ossy/themes— theme objects passed toThememarkdown-to-jsx,react-syntax-highlighter— only if you useMarkdownViewer
Quick start
import { Text, Theme } from '@ossy/design-system'
import { CloudLight } from '@ossy/themes'
export default function App() {
return (
<Theme theme={CloudLight}>
<YourApp />
</Theme>
)
}Multiple themes with runtime switching:
import { Text, Theme } from '@ossy/design-system'
import { CloudLight, CloudDark } from '@ossy/themes'
<Theme
theme={CloudLight}
themes={{ light: CloudLight, dark: CloudDark }}
defaultTheme="light"
>
<YourApp />
</Theme>Storybook
Component docs and MDX guides live in the ossy monorepo Storybook. Feature package UI is previewed in @ossy/app-test, not Storybook.
From the monorepo root (business/platform/ossy):
npm install # if needed
npm start # storybook dev -p 6006Open in the browser:
- Design System → Getting Started —
packages/design-system/docs/intro.mdx - Design System → Layout Guide and Slots — layout and injection docs (
layout-guide.mdx,slots.mdx) - Design System → Principles, Theme Concepts, Patterns, Variants — other
packages/design-system/docs/*.mdx - Design System → Base / … —
*.stories.jsxnext to components
Static build: npm run build:storybook.
Storybook resolves @ossy/design-system from source (src/index.js), so you do not need lerna run build before npm start.
Layout guide
When to use what
| Need | Use |
|------|-----|
| Flex/grid, gaps, surfaces, app chrome grid | View with a layout preset and data-region children |
| Standard in-app page body (scroll, header, slot spacing) | Page — optional maxWidth, built-in header via page.* slots |
| Centered content band with max-width only | PageSection (marketing heroes, standalone sections) |
| Bordered vertical/horizontal list of panels | View with stack, horizontal, bordered, and View.Item |
| Injectable header/sidebar/toolbar from packages | <Slot view="…" region="…" /> + *.component.jsx — see docs/SLOTS.md |
View layout presets
View is the primary layout primitive. Set layout to a preset (page, sidebar, off-center, column, row, …) and assign children with data-region="header", data-region="content", etc. Storybook Design System → Base → View shows each preset.
layout="header" — title row (leading + optional far), optional description and children rows below. The default grid is a single row ("leading far"); :has([data-header-slot="…"]) expands to two or three rows when description and/or children slots are present, so row gap is not applied between empty tracks. Used by ContentHeader and AppHeader via HeaderLayout.
Page and PageSection
Page is the standard in-app page body inside the shell content region. It renders a single root View (default layout="page") with registered slots for each layout region. Built-in title / description / near / far props feed the page.header slot fallback (ContentHeader); override the region via ComponentSlotsProvider or the header prop. Unslotted children land in page.content. Children with region or data-region props become fallbacks for the matching page slot.
Props:
| Prop | Purpose |
|------|---------|
| title, description, near, far, … | Passed to page.header slot (fallback: built-in ContentHeader) |
| header | Custom header node for page.header fallback; when set, built-in header props are ignored |
| maxWidth | Wraps content in PageSection for a centered max-width band (s, m, l, xl, …) |
| inset | Default s (minimal edge padding on content); none for full-bleed; or any View inset token |
| scroll | false (default): page grows with content; the shell scrolls (@ossy/app/layout/default). true: page fills a height-locked shell <main> (e.g. @ossy/app/layout/workspace) and page.content scrolls (or nested panels via contentStyle). |
| contentStyle | Extra styles on page.content (e.g. { overflow: 'hidden' } so child panels own scrolling, as on Storage) |
| layout | page (default) or column — page uses View layout="page" grid regions |
| surface, gap | Passed through to the root View (gap applies to content column) |
Page layout region slot keys:
| Key | Region |
|-----|--------|
| page.header | Top title row |
| page.content | Main body (scrolls when scroll is true) |
| page.sidebar-primary | Left rail |
| page.sidebar-secondary | Right rail |
| page.footer | Bottom bar |
import { Page, Button, ComponentSlotsProvider } from '@ossy/design-system'
<Page
title="tasks.home.title"
description="tasks.home.description"
near={<Tags tags={['Beta']} size="s" />}
far={<Button variant="cta" label="design-system.add" />}
>
{children}
</Page>
// Override page.header for a route or feature:
<ComponentSlotsProvider slots={{ 'page.header': MyPageHeader }}>
<Page title="…">…</Page>
</ComponentSlotsProvider>For a fully custom page header, pass header={<ContentHeader … />} or any node as the page.header fallback. Use ContentHeader as a body child for section intros within the page.
ContentHeader and AppHeader
Both use HeaderLayout (View layout="header") and share most props:
| Prop | Purpose |
|------|---------|
| title | i18n key (string) or custom title node (ReactNode) |
| titleParams, titleAs, titleVariant | Used when title is a string |
| description, descriptionParams, … | Lead paragraph below the title row |
| lead | Custom lead node (overrides description) |
| near | Slot beside the title (tags, status badges, etc.) |
| far | Slot on the right (CTA buttons, toolbar actions) |
ContentHeader— page title row insidePage(data-component="content-header", defaultinset="list-row").Pageuses this as the built-inpage.headerfallback.AppHeader— app shell top bar (data-component="app-header", defaultinset="m"). Register onapp:header; typically only the first header row (logo + actions).
import { ContentHeader, AppHeader, Tags, Button } from '@ossy/design-system'
<ContentHeader
title="tasks.home.title"
near={<Tags tags={['Beta']} size="s" />}
far={<Button variant="cta" label="design-system.add" />}
/>
<AppHeader
title={<Text variant="heading-tertiary">Workspace</Text>}
far={<Button variant="link" prefix="profile" label="Profile" />}
/>PageSection is a max-width wrapper around arbitrary content — use for marketing heroes (HeroCover), sales blocks, or when you need a centered band without the full page chrome. For normal app pages, prefer Page with maxWidth instead of hand-rolling View + padding.
Shell canvas (full viewport behind <main>) uses View surface="base", not Page.
Stack → View (migration)
Stack and Stack.Item were removed. Use View with stack mode:
Before
import { Text, Stack } from '@ossy/design-system'
<Stack horizontal bordered>
<Stack.Item fill>…</Stack.Item>
<Stack.Item>…</Stack.Item>
</Stack>After
import { Text, View } from '@ossy/design-system'
<View stack horizontal bordered>
<View.Item fill>…</View.Item>
<View.Item>…</View.Item>
</View>stack— enables stack item styling and bordershorizontal— row direction (default column)bordered— separators between itemsView.Item— same as formerStack.Item(fillprop supported)
Icons (migration)
Legacy Material Icon and Icon2 are consolidated into a single Icon export.
| Before | After |
|--------|--------|
| Icon2 with name="arrow-right" | Icon with name="arrow-right" |
| Legacy Icon with PascalCase / Material names | Icon with kebab-case css.gg name (see Storybook Design System → Base → Icon) |
| icon= prop on some internals | Prefer name= (icon still accepted as alias) |
Icons load lazily per name. Unknown names log a warning and render empty.
import { Text, Icon } from '@ossy/design-system'
<Icon name="check" size="m" variant="primary" />Domain widgets (moved out)
These are no longer exported from @ossy/design-system:
| Former export | New home |
|---------------|----------|
| Profile | @ossy/design-system |
| ResumeExperience, ResumeView | @ossy/resumes |
| sections/* (Hero, marketing blocks, …) | Consumer feature packages or local app code — sections/ removed from design-system |
Install the feature package that owns the domain, or copy patterns using View + primitives.
Platform integration
- Pages —
*.page.jsxprimitives; use design-system for UI inside pages. PRIMITIVES.md - App chrome — app
export const slotsin*.layout.jsxmapsapp:*to component ids; platform setsapp:contentto the current page. docs/SLOTS.md, SHELL-SPEC.md - Resource / schema UI —
*.component.jsxwithmetadata.id=@ossy/…/view/…,@ossy/…/form/…, or@ossy/design-system/input/…
Deprecations and removed exports
Removed (breaking)
Stack,Stack.Item— useView+stack/View.Item- Legacy Material
Icon— useIconwith kebab-casename Icon2— renamed toIcon(same API)ResumeExperience,ResumeView— use@ossy/resumessections/*exportsTemplateForm— use<Form><FieldFactory /></Form>Title— useTextwithas+ heading variants (heading-primary,heading-secondary, …)EditFields,ResourceTemplateFields— useFieldsapplyResourceTemplateFieldChange— useapplyFieldChangeComponentsProvider,useComponentSlot— useComponentSlotsProvider,useSlotresourceSlot,RESOURCE_SLOT_PREFIX,normalizeResourceTypeKey— useviewComponentId/formComponentIdfrom@ossy/schema
Further reading
- docs/SLOTS.md — layout vs component vs form field slots
- AUDIT.md — streamline audit and task status
- packages/platform/PRIMITIVES.md — platform file primitives
License
MIT
