Skip to main content

Primus SaaS UI Components

Package: @primus/ui-core ยท Version: 0.1.0 ยท Size: ~35KB minified CSS ยท ~9KB minified JS

@primus/ui-core is the framework-agnostic design system base layer of the Primus SaaS Framework. It provides design tokens, semantic HTML component styles, and interactive Web Components that work identically in React, Angular, and plain HTML โ€” with zero dependencies and no build pipeline required.

Architecture
@primus/ui-core  โ†  single visual source of truth
โ†“ โ†“
primus-angular-ui primus-react-ui
(Angular SDK) (React SDK)

Both SDKs render HTML structures that @primus/ui-core styles. PrimusThemeService (Angular) and PrimusUiCoreBridge (React) keep themes in sync automatically.


Installationโ€‹

npm install @primus/ui-core

Angularโ€‹

In angular.json:

"styles": [
"node_modules/@primus/ui-core/dist/primus-ui.min.css",
"src/styles.scss"
]

PrimusThemeService from primus-angular-ui bridges the token sets automatically โ€” no additional configuration needed.

Reactโ€‹

In main.tsx:

import '@primus/ui-core/dist/primus-ui.min.css';

Add <PrimusUiCoreBridge /> once inside PrimusThemeProvider:

import { PrimusThemeProvider, PrimusUiCoreBridge } from 'primus-react-ui';

function App() {
return (
<PrimusThemeProvider defaultTheme="dark">
<PrimusUiCoreBridge />
<YourApp />
</PrimusThemeProvider>
);
}

Plain HTML (CDN)โ€‹

<link rel="stylesheet" href="https://unpkg.com/@primus/ui-core/dist/primus-ui.min.css" />
<script type="module" src="https://unpkg.com/@primus/ui-core/dist/primus-ui.min.js"></script>

Design Tokensโ€‹

All tokens are CSS custom properties on :root. Override globally with:

:root {
--primary: #your-brand-color;
--primary-foreground: #ffffff;
}

Core tokensโ€‹

TokenLightDarkPurpose
--primary#7c3aed#a78bfaBrand purple โ€” buttons, links, rings
--background#ffffff#0a0a0fPage background
--foreground#0f172a#fafafaPrimary text
--card#ffffff#14101fCard/panel backgrounds
--border#e2e8f0#2e2545Borders, dividers
--muted#f4f4f5#1a1625Muted backgrounds
--muted-foreground#64748b#a1a1aaSecondary text
--accent#f5f3ff#211940Accent hover states
--ring#7c3aed#a78bfaFocus ring
--success#16a34a#4ade80Success states
--warning#d97706#fbbf24Warning states
--danger#dc2626#f87171Error/danger states

Components Referenceโ€‹

Buttonโ€‹

Semantic <button> elements are styled automatically. No class required for primary.

<!-- Primary (default) -->
<button>Save Changes</button>

<!-- Variants -->
<button data-variant="secondary">Cancel</button>
<button data-variant="danger">Delete Account</button>
<button class="outline">Export</button>
<button class="ghost">More Options</button>

<!-- Sizes -->
<button class="small">Compact</button>
<button class="large">Call to Action</button>

<!-- Icon button -->
<button class="icon small" title="Edit">โœŽ</button>

<!-- Button group -->
<menu class="buttons">
<li><button>Month</button></li>
<li><button>Quarter</button></li>
<li><button>Year</button></li>
</menu>

Badgeโ€‹

<span class="badge">Default</span>
<span class="badge secondary">Secondary</span>
<span class="badge outline">Outline</span>
<span class="badge success">Active</span>
<span class="badge warning">Pending</span>
<span class="badge danger">Overdue</span>
<span class="badge accent">Beta</span>

Alertโ€‹

<div role="alert">Default alert message.</div>
<div role="alert" data-variant="success">Record saved successfully.</div>
<div role="alert" data-variant="danger">Validation failed. Check your input.</div>
<div role="alert" data-variant="warning">Your subscription expires in 3 days.</div>
<div role="alert" data-variant="info">2 background jobs are running.</div>

Cardโ€‹

<!-- Standard card -->
<div class="card">
<h3>Revenue</h3>
<p>Monthly metrics at a glance.</p>
</div>

<!-- Elevated (hover glow effect) -->
<div class="card elevated">
<h3>Premium Feature</h3>
</div>

<!-- Stat/metric card -->
<div class="card stat">
<span class="card-label">Monthly Revenue</span>
<span class="card-value">$142,000</span>
<span class="card-delta up">+12%</span>
</div>

<div class="card stat">
<span class="card-label">Failed Jobs</span>
<span class="card-value">3</span>
<span class="card-delta down">-2 from last week</span>
</div>

Form Elementsโ€‹

Form elements are styled automatically via semantic HTML โ€” no classes needed.

<form>
<!-- Text input -->
<label>
Email Address
<input type="email" placeholder="name@company.com" />
</label>

<!-- With error state -->
<div data-field="error">
<label>Password <input type="password" aria-invalid="true" /></label>
<span class="error">Password must be at least 8 characters.</span>
</div>

<!-- With hint -->
<div data-field>
<label>API Key <input type="text" /></label>
<span data-hint>Your key is visible only once.</span>
</div>

<!-- Select -->
<label>
Plan
<select>
<option>Starter</option>
<option>Pro</option>
<option>Enterprise</option>
</select>
</label>

<!-- Textarea -->
<label>Notes <textarea rows="4" placeholder="Add notes..."></textarea></label>

<!-- Checkbox -->
<label><input type="checkbox" /> Enable notifications</label>

<!-- Switch/Toggle -->
<label><input type="checkbox" role="switch" /> Dark mode</label>

<!-- Radio -->
<fieldset>
<legend>Billing Cycle</legend>
<label><input type="radio" name="billing" /> Monthly</label>
<label><input type="radio" name="billing" /> Annual</label>
</fieldset>

<!-- Input group -->
<fieldset class="group">
<legend>https://</legend>
<input type="text" placeholder="your-domain.com" />
</fieldset>

<button type="submit">Save Settings</button>
</form>

Tableโ€‹

<div class="table">
<table>
<thead>
<tr>
<th aria-sort="ascending">Tenant</th>
<th>Plan</th>
<th>Status</th>
<th>MRR</th>
</tr>
</thead>
<tbody>
<tr>
<td>Acme Corp</td>
<td>Enterprise</td>
<td><span class="badge success">Active</span></td>
<td>$12,000</td>
</tr>
<tr>
<td>Globex Inc</td>
<td>Pro</td>
<td><span class="badge warning">Trial</span></td>
<td>$499</td>
</tr>
</tbody>
</table>
</div>

<!-- Striped + compact -->
<table class="striped compact">...</table>

<!-- Bordered -->
<table class="bordered">...</table>

Dialog (Modal)โ€‹

<!-- Trigger -->
<button commandfor="my-dialog" command="show-modal">Open Dialog</button>

<!-- Dialog -->
<dialog id="my-dialog">
<header>
<h2>Confirm Action</h2>
<p>This operation cannot be undone.</p>
</header>
<div>Are you sure you want to delete this tenant?</div>
<footer>
<button class="outline" commandfor="my-dialog" command="close">Cancel</button>
<button data-variant="danger">Delete</button>
</footer>
</dialog>

<!-- Size variants -->
<dialog class="dialog-sm">...</dialog> <!-- 22rem -->
<dialog>...</dialog> <!-- 32rem (default) -->
<dialog class="dialog-lg">...</dialog> <!-- 48rem -->
<dialog class="dialog-xl">...</dialog> <!-- 64rem -->

Tabsโ€‹

<!-- Pill style (default) -->
<primus-tabs>
<div role="tablist">
<button role="tab">Overview</button>
<button role="tab">Transactions</button>
<button role="tab">Settings</button>
</div>
<div role="tabpanel">Overview content</div>
<div role="tabpanel">Transaction content</div>
<div role="tabpanel">Settings content</div>
</primus-tabs>

<!-- Underline style (Primus extension) -->
<primus-tabs>
<div role="tablist" class="underline">
<button role="tab">General</button>
<button role="tab">Billing</button>
<button role="tab">Security</button>
</div>
<div role="tabpanel">General settings</div>
<div role="tabpanel">Billing settings</div>
<div role="tabpanel">Security settings</div>
</primus-tabs>

Events: Listens to primus-tab-change โ†’ { index, tab }.


<primus-dropdown>
<button popovertarget="actions-menu">Actions โ–พ</button>
<menu popover id="actions-menu">
<button role="menuitem">Edit</button>
<button role="menuitem">Duplicate</button>
<div role="separator"></div>
<button role="menuitem" data-variant="danger">Delete</button>
</menu>
</primus-dropdown>

Toastโ€‹

Requires JS bundle. Calls primus.toast() globally.

// Info (default)
primus.toast('Syncing data in the background.');

// With title
primus.toast('Your plan has been updated.', 'Subscription Changed');

// Variants: success | danger | warning | info
primus.toast('All changes saved.', 'Saved', { variant: 'success' });
primus.toast('Connection failed.', 'Error', { variant: 'danger', placement: 'bottom-right' });
primus.toast('API rate limit approaching.', 'Warning', { variant: 'warning', duration: 6000 });

// Custom duration (ms) or persistent (duration: 0)
primus.toast('Processing...', 'Please wait', { duration: 0 });

// Clear all
primus.toast.clear();
primus.toast.clear('top-right'); // clear specific placement

Placements: top-left ยท top-center ยท top-right (default) ยท bottom-left ยท bottom-center ยท bottom-right


Skeletonโ€‹

<!-- Line skeleton -->
<div role="status" class="skeleton line"></div>
<div role="status" class="skeleton line" style="width: 60%"></div>
<div role="status" class="skeleton line-sm"></div>

<!-- Box/image skeleton -->
<div role="status" class="skeleton box"></div>

<!-- Avatar circle -->
<div role="status" class="skeleton avatar"></div>
<div role="status" class="skeleton circle"></div>

Spinnerโ€‹

<!-- Inline spinner (element shows spinner before its content) -->
<div aria-busy="true"></div>

<!-- Sizes -->
<div aria-busy="true" data-spinner="small"></div>
<div aria-busy="true" data-spinner="large"></div>
<div aria-busy="true" data-spinner="xlarge"></div>

<!-- Overlay (disables children while loading) -->
<div aria-busy="true" data-spinner="overlay large">
<table><!-- content is dimmed + disabled while aria-busy --></table>
</div>

Accordionโ€‹

<details>
<summary>What is multi-tenancy?</summary>
<p>Multi-tenancy allows a single instance of the application to serve multiple customers...</p>
</details>

<details>
<summary>How is data isolated?</summary>
<p>Each tenant's data is isolated via row-level security and separate connection strings...</p>
</details>

Progress & Meterโ€‹

<!-- Progress bar -->
<progress value="65" max="100"></progress>

<!-- Sizes -->
<progress value="65" max="100" class="progress-sm"></progress>
<progress value="65" max="100" class="progress-lg"></progress>

<!-- Color variants -->
<progress value="92" max="100" class="progress-success"></progress>
<progress value="45" max="100" class="progress-warning"></progress>
<progress value="12" max="100" class="progress-danger"></progress>

<!-- Meter (optimum/suboptimum/danger auto-colored) -->
<meter value="0.7">70%</meter>

Tooltipโ€‹

<!-- Via data-tooltip attribute -->
<button data-tooltip="Save your changes">Save</button>
<span data-tooltip="This field is required">?</span>

<!-- Below position (Primus extension) -->
<button data-tooltip="Opens in new tab" data-tooltip-pos="bottom">External Link โ†—</button>

JS progressive enhancement: any title attribute is automatically converted to data-tooltip when the JS bundle is loaded.


Grid Layoutโ€‹

<div class="container">
<div class="row">
<div class="col-4">Sidebar</div>
<div class="col-8">Main Content</div>
</div>

<div class="row">
<div class="col-3">Card 1</div>
<div class="col-3">Card 2</div>
<div class="col-3">Card 3</div>
<div class="col-3">Card 4</div>
</div>
</div>

Responsive: stacks to 4-column grid on mobile (<768px).


<div data-sidebar-layout>
<!-- Optional top nav -->
<nav data-topnav>
<button data-sidebar-toggle>โ˜ฐ</button>
<strong>Primus Admin</strong>
</nav>

<!-- Sidebar -->
<aside data-sidebar>
<header><strong>Primus</strong></header>
<nav>
<ul>
<li><a href="/dashboard" aria-current="page">Dashboard</a></li>
<li><a href="/tenants">Tenants</a></li>
<li><a href="/settings">Settings</a></li>
</ul>
</nav>
</aside>

<!-- Main content -->
<main>
<h1>Dashboard</h1>
</main>
</div>

Utilitiesโ€‹

<!-- Layout -->
<div class="hstack">...</div> <!-- horizontal flex row with gap -->
<div class="vstack">...</div> <!-- vertical flex column with gap -->
<div class="flex items-center justify-between">...</div>

<!-- Typography -->
<p class="text-light">Muted text</p>
<p class="text-primary">Brand colored</p>
<p class="truncate">Long text that gets cut off...</p>
<span class="font-semibold">Bold label</span>

<!-- Spacing -->
<div class="mt-4 mb-6 p-4">...</div>

<!-- Visual -->
<div class="card rounded-xl shadow-md">...</div>
<span class="badge rounded-full">Tag</span>

<!-- Visibility -->
<div class="hide-mobile">Hidden on mobile</div>
<div class="hide-desktop">Hidden on desktop</div>
<span class="sr-only">Screen reader only</span>

Light / Dark Modeโ€‹

@primus/ui-core uses color-scheme: light dark on :root, which means:

  • Auto mode: follows the OS prefers-color-scheme by default with no JS.
  • Controlled mode: PrimusThemeService (Angular) or PrimusUiCoreBridge (React) override the tokens on :root via setProperty.

To force a theme without JS:

<html style="color-scheme: dark">...</html>

Upstream Attributionโ€‹

@primus/ui-core is based on OAT UI by Kailash Nadh, licensed under the MIT License. Significant extensions have been added for the Primus SaaS Framework.