@platform/sdk-ui
@platform/sdk-ui is the shared Design System for all module UIs.
It provides Radix UI–based components pre-styled with Tailwind CSS v4,
a three-level design token system, and full white-label theming.
Modules import components from this package — they do not duplicate
styles or reinvent UI primitives.
Installation
pnpm add @platform/sdk-ui
Design Token System
The Design System uses three levels of CSS custom properties:
| Level | Name | Description | Example |
|---|---|---|---|
| 1 | Core tokens | Base palette — raw color values | --color-blue-500: #1a73e8 |
| 2 | Brand tokens | Tenant-customizable accent colors (computed from one accentColor) | --color-accent: #1a73e8 |
| 3 | Context tokens | Semantic roles — where a color is used | --color-btn-primary: var(--color-accent) |
Module components consume Context tokens only — never Core or Brand
tokens directly. This ensures white-label theming works automatically:
when a tenant changes their accentColor, all module buttons, links,
and progress bars update without any module code changes.
Typography
| Attribute | Value |
|---|---|
| Primary typeface | Google Sans (headings, hero text) |
| Body typeface | Inter (body, labels, tables) |
| Icon set | Lucide Icons (React) |
| Font loading | next/font or @fontsource — no FOUT |
Theming (Light / Dark / White-Label)
import { ThemeProvider } from '@platform/sdk-ui';
function ModuleRoot() {
return (
<ThemeProvider
accentColor={tenant.branding.accentColor} // e.g. '#1a73e8'
theme={tenant.branding.theme} // 'light' | 'dark' | 'system'
>
<MyModuleUI />
</ThemeProvider>
);
}
ThemeProvider automatically generates 40+ color shades from the
single accentColor (hover, pressed, light variant, dark variant) and
injects them as CSS custom properties. Modules never hardcode color hex
values.
WCAG Compliance
Every tenant-supplied accentColor is validated:
- Minimum WCAG AA contrast ratio of 4.5:1 against the background
- Pure black (
#000000) and pure white (#FFFFFF) are rejected - Invalid color → fallback to platform default accent + admin notification
Components
Button
import { Button } from '@platform/sdk-ui';
<Button variant="primary" size="md" loading={isSubmitting} onClick={handleSubmit}>
Save Contact
</Button>
<Button variant="destructive" size="sm">
Delete
</Button>
<Button variant="ghost" leftIcon={<PlusIcon />}>
Add Item
</Button>
| Prop | Type | Default | Description |
|---|---|---|---|
variant | primary | secondary | destructive | ghost | outline | primary | Visual style |
size | sm | md | lg | md | Button size |
loading | boolean | false | Shows spinner, disables click |
leftIcon | ReactNode | — | Icon left of label |
rightIcon | ReactNode | — | Icon right of label |
Table
High-density data table built on TanStack Table v8 with built-in sort, pagination, and faceted filtering:
import { DataTable } from '@platform/sdk-ui';
const columns = [
{ key: 'name', header: 'Name', sortable: true },
{ key: 'email', header: 'Email', sortable: true },
{ key: 'createdAt', header: 'Joined', sortable: true, format: 'datetime' },
{ key: 'status', header: 'Status', filter: 'select' },
];
<DataTable
columns={columns}
data={contacts.data}
pagination={contacts.meta}
onPageChange={setPage}
loading={isLoading}
emptyState={<EmptyContacts />}
/>
| Feature | Description |
|---|---|
| Sorting | Click column header — ascending / descending / off |
| Faceted filters | Select, multi-select, date range |
| Column visibility | User can show/hide columns |
| Row selection | Checkbox column for bulk actions |
| Export row | Pass onExport callback for CSV/JSON export |
Chart
Recharts-based chart components pre-styled with Brand tokens:
import { LineChart, BarChart, PieChart, AreaChart } from '@platform/sdk-ui';
<LineChart
data={revenueData}
xKey="month"
yKey="revenueCents"
formatY={(v) => `$${(v / 100).toFixed(2)}`}
height={300}
/>
<BarChart
data={conversionData}
xKey="source"
yKey="conversions"
stacked
/>
Charts automatically use --color-accent and its generated palette —
Brand tokens propagate into chart series colors without any config.
Form
Radix UI–based form primitives with built-in validation display:
import { Form, Input, Select, Textarea, Switch, Checkbox } from '@platform/sdk-ui';
<Form onSubmit={handleSubmit} error={formError}>
<Input
name="email"
label="Email Address"
type="email"
required
error={errors.email}
/>
<Select
name="role"
label="Role"
options={roles.map(r => ({ label: r.name, value: r.id }))}
error={errors.role}
/>
<Switch name="sendWelcomeEmail" label="Send welcome email" defaultChecked />
<Button type="submit" loading={isSubmitting}>Create User</Button>
</Form>
Form errors display inline (below field) and as a summary banner at
the top. RFC 9457 validation-error responses are automatically parsed
and mapped to the correct fields.
ImageUpload
Full-featured image upload with an interactive crop editor:
import { ImageUpload } from '@platform/sdk-ui';
<ImageUpload
bucket="avatars"
aspectRatio={1} // 1 for square avatars, 3 for banners (3:1)
maxSizeMB={10}
outputFormat="webp"
presets={['icon_32', 'avatar_64', 'card_300']}
onUploadComplete={(result) => {
setAvatarUrl(result.processed.url);
setThumbnails(result.thumbnails);
}}
onError={(err) => toast.error(err.message)}
/>
The crop editor (powered by react-easy-crop) provides:
- Instagram-style crop grid with drag and zoom
- Aspect ratio lock (1:1 square, 16:9 widescreen, 3:1 banner)
- Rotation control
- Real-time preview of the cropped area
The component handles the full pipeline: file selection → crop → upload
to kernel.files().uploadImage() → callback with all thumbnail URLs.
EmbedCodeBlock
Display a ready-to-copy embed snippet for widget integrations:
import { EmbedCodeBlock } from '@platform/sdk-ui';
<EmbedCodeBlock
code={`<script src="https://cdn.platform.io/embed/${widgetId}.js"></script>`}
language="html"
label="Paste this snippet into your website's <head>"
/>
Features: syntax highlighting, one-click copy button, language badge, and an optional "Test embed" link.
Loading States
| Situation | Component |
|---|---|
| App boot / page transition | <BrandedSpinner /> — logo centered on screen |
| Data loading in table | <TableSkeleton rows={5} /> — shimmer animation (left→right) |
| Card / widget loading | <CardSkeleton /> — container-based, matches card dimensions |
| Button submitting | loading={true} prop — inline spinner replaces label |
Skeleton screens are always preferred over spinners for content loading. Spinners are reserved for blocking operations (auth checks, navigation).
Responsive Layout
All module widgets must use Container Queries (@container), not
media queries. This allows the widget to respond to its own container
width, not the viewport — enabling correct rendering whether the widget
appears in a full-page view or a narrow dashboard column:
.widget-card {
container-type: inline-size;
}
@container (min-width: 600px) {
.widget-metrics-grid {
grid-template-columns: repeat(3, 1fr);
}
}
/* Fallback for browsers without @container support */
@supports not (container-type: inline-size) {
@media (min-width: 600px) {
.widget-metrics-grid {
grid-template-columns: repeat(3, 1fr);
}
}
}
| Viewport | Sidebar | Dashboard |
|---|---|---|
| Desktop (> 1200 px) | Fixed, expanded | Full widget grid |
| Tablet (768–1200 px) | Collapsible (icons only) | 2-column grid |
| Mobile (< 768 px) | Off-canvas drawer (hamburger) | Vertical stack, metrics on top |
Minimum supported browsers: Chrome 105+, Firefox 110+, Safari 16+, Edge 105+.