Skip to main content

@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:

LevelNameDescriptionExample
1Core tokensBase palette — raw color values--color-blue-500: #1a73e8
2Brand tokensTenant-customizable accent colors (computed from one accentColor)--color-accent: #1a73e8
3Context tokensSemantic 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

AttributeValue
Primary typefaceGoogle Sans (headings, hero text)
Body typefaceInter (body, labels, tables)
Icon setLucide Icons (React)
Font loadingnext/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>
PropTypeDefaultDescription
variantprimary | secondary | destructive | ghost | outlineprimaryVisual style
sizesm | md | lgmdButton size
loadingbooleanfalseShows spinner, disables click
leftIconReactNodeIcon left of label
rightIconReactNodeIcon 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 />}
/>
FeatureDescription
SortingClick column header — ascending / descending / off
Faceted filtersSelect, multi-select, date range
Column visibilityUser can show/hide columns
Row selectionCheckbox column for bulk actions
Export rowPass 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"
placeholder="[email protected]"
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

SituationComponent
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 submittingloading={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);
}
}
}
ViewportSidebarDashboard
Desktop (> 1200 px)Fixed, expandedFull 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+.