@codeatlasdev/ui
v0.1.0
Published
Glass-first Svelte 5 design system. OKLCH tokens, Liquid Glass materials, Bits UI primitives.
Maintainers
Readme
Install
npm install @codeatlasdev/ui
# or
bun add @codeatlasdev/ui@codeatlasdev/ui
Glass-first Svelte 5 design system. Svelte 5 runes, Bits UI primitives, OKLCH tokens, zero Tailwind.
Setup
// app.css (or root layout)
import '@codeatlasdev/ui/styles/global.css';
import '@codeatlasdev/ui/styles/materials.css';<!-- root +layout.svelte -->
<script>
import { colorScheme } from '@codeatlasdev/ui';
import { onMount } from 'svelte';
onMount(() => colorScheme.init());
</script>Page layouts — which one to use
| Layout | Use when |
|---|---|
| AppList | List/CRUD page — table with internal scroll, fixed pagination footer |
| AppForm | Create/edit form — fixed header, scrollable body, sticky action footer |
| AppSettings | Settings page — fixed left nav + scrollable content (GitHub/Vercel style) |
| PageShell | Dashboard, reports, free content — normal scroll |
AppList
Data-driven mode (recommended):
<!-- Data-driven mode (recommended) -->
<AppList title="Users" data={ALL} searchKeys={['name', 'email']} perPage={8}>
{#snippet filters(list)}
<FilterGroup options={roleOpts} value={list.filters['role']}
onchange={v => list.setFilter('role', v)} />
{/snippet}
{#snippet children(list)}
<SimpleTable>
<thead><tr><th>Name</th><th>Role</th><th>Status</th><th></th></tr></thead>
<tbody>
{#each list.rows as u (u.id)}
<tr>
<td class="primary"><UserCell name={u.name} email={u.email} /></td>
<td>{u.role}</td>
<td><StatusChip status={u.status === 'active' ? 'success' : 'neutral'} /></td>
<td><Row gap={4} class="ca-row-actions">
<Button variant="ghost" size="sm"><Edit2 size={13} /></Button>
<Button variant="ghost" size="sm"><Trash2 size={13} /></Button>
</Row></td>
</tr>
{/each}
</tbody>
</SimpleTable>
{/snippet}
</AppList>Manual mode (for server-side data):
<AppList title="Users" description="24 of 240">
{#snippet actions()}
<Button variant="filled" href="/users/new">+ New user</Button>
{/snippet}
{#snippet toolbar()}
<TextField placeholder="Search…" bind:value={search} class="search-field">
{#snippet leading()}<Search size={14} />{/snippet}
</TextField>
<Select bind:value={roleF} options={roleOpts} />
{/snippet}
<SimpleTable>
<thead><tr><th>Name</th><th>Role</th><th>Status</th><th></th></tr></thead>
<tbody>
{#each rows as u (u.id)}
<tr>
<td class="primary"><UserCell name={u.name} email={u.email} /></td>
<td>{u.role}</td>
<td><StatusChip status={u.status === 'active' ? 'success' : 'neutral'} /></td>
<td><Row gap={4} class="ca-row-actions">
<Button variant="ghost" size="sm"><Edit2 size={13} /></Button>
<Button variant="ghost" size="sm"><Trash2 size={13} /></Button>
</Row></td>
</tr>
{/each}
</tbody>
</SimpleTable>
{#snippet footer()}
<span class="ca-page-info">1–8 of 24</span>
<Pagination count={24} perPage={8} bind:page />
{/snippet}
</AppList>AppForm
<AppForm title="New User" description="Fill in the details." backHref="/users">
<!-- layout="grid" is default — auto 2-column for text fields -->
<FormSection title="Account details">
<TextField label="Full name" bind:value={name} />
<TextField label="Email" type="email" bind:value={email} />
<Select label="Role" bind:value={role} options={roleOpts} />
</FormSection>
<!-- layout="list" for Switch/SettingItem rows -->
<FormSection title="Notifications" layout="list">
<Switch label="Send welcome email" bind:checked={notify} />
</FormSection>
{#snippet footer()}
<Button variant="ghost" href="/users">Cancel</Button>
<Button variant="filled" onclick={save} disabled={saving || !name || !email}>
{saving ? 'Saving…' : 'Create user'}
</Button>
{/snippet}
</AppForm>AppSettings
<!-- No external state needed — AppSettings manages tab internally -->
<AppSettings title="Settings" tabs={TABS}>
{#snippet actions()}
<Button variant="filled" size="sm" onclick={save}>Save changes</Button>
{/snippet}
{#snippet children(tab)}
{#if tab === 'general'}
<FormSection title="Workspace">
<TextField label="Name" bind:value={workspaceName} />
<Select label="Timezone" bind:value={timezone} options={tzOpts} />
</FormSection>
{/if}
{#if tab === 'security'}
<FormSection title="Authentication" layout="list">
<SettingItem label="Two-factor authentication" description="Require 2FA for all members.">
<Switch bind:checked={mfa} />
</SettingItem>
<Divider />
<SettingItem label="Single sign-on" description="SAML or OIDC provider.">
<Switch bind:checked={sso} />
</SettingItem>
</FormSection>
{/if}
{/snippet}
</AppSettings>PageShell
<PageShell title="Dashboard" description="Sep 22, 2026" maxWidth="1200px" gap={24}>
{#snippet actions()}
<Button variant="ghost" size="sm">Export</Button>
{/snippet}
<Grid cols="repeat(auto-fill, minmax(200px, 1fr))" gap={16}>
<KpiCard title="Revenue" value="$112k" trend={18.4} icon={DollarSign} />
<KpiCard title="Users" value="24.8k" trend={12.1} icon={Users} />
</Grid>
<GlassCard title="Revenue vs Expenses">
<LineChart series={[...]} categories={months} area height="260px" />
</GlassCard>
</PageShell>FormSection layouts
| layout | Use for | Behaviour |
|---|---|---|
| "grid" (default) | TextField, Select, NumberField | Auto 2-col, minmax(260px, 1fr) |
| "list" | Switch, SettingItem, Divider | Single col, flush (no gap) |
| "stack" | Composite blocks (Avatar + fields) | Single col, gap: 16px |
Never wrap fields in <Stack gap={N}> inside FormSection — the layout prop handles it.
Layout primitives
<!-- Grid: cols as number (equal) or CSS string -->
<Grid cols={3} gap={16}>…</Grid>
<Grid cols="2fr 1fr" gap={20}>…</Grid>
<Grid cols="repeat(auto-fill, minmax(200px, 1fr))" gap={16}>…</Grid>
<!-- Row: horizontal flex -->
<Row gap={8} align="center" justify="space-between">…</Row>
<!-- Stack: vertical flex -->
<Stack gap={16}>…</Stack>Utility classes (from global.css)
| Class | Use |
|---|---|
| ca-search-field | Search input in toolbar — flex:1, min 180px, max 320px |
| ca-toolbar-spacer | flex:1 push-right in toolbar |
| ca-toolbar-sep | 1px vertical separator between toolbar groups |
| ca-clear-btn | "Clear filters" button in toolbar |
| ca-page-info | Pagination label "1–8 of 240" |
| ca-row-actions | Table row actions — hidden by default, visible on row hover |
| ca-col-check | Checkbox column — fixed width + compact padding |
| ca-col-actions | Actions column — 80px fixed width |
| ca-row-check | Row checkbox — brand accent color |
| ca-row--selected | Selected row — brand tinted background |
| ca-avatar-row | Avatar + metadata flex row (profile pages) |
| ca-trend-up | Positive trend — success color |
| ca-trend-down | Negative trend — error color |
| ca-rail-btn | Icon button in AppShell rail bottom slot |
| ca-rail-avatar | Avatar link in AppShell rail bottom slot |
| ca-btn-danger | Outlined button in error color |
| ca-btn-danger-ghost | Ghost/text button in error color |
| ca-bar-fill | Progress bar width via --bar-w CSS var |
| ca-bar-fill--warn | Warning variant of progress bar |
| ca-pricing-cta | Full-width CTA at bottom of pricing card |
| CodeSnippet | Syntax-highlighted code block with copy button |
Slider
<!-- Single value (most common) -->
<Slider bind:value={volume} min={0} max={100} />
<!-- Range (two thumbs) -->
<Slider bind:values={range} min={0} max={100} />Spacing tokens
All page layouts share the same spacing vars. Override in your app root to change globally.
--ca-page-px: 24px; /* horizontal padding of all page layouts */
--ca-page-pt: 20px; /* top padding */
--ca-page-gap: 20px; /* gap between sections */
--ca-card-padding: 20px; /* default GlassCard padding */
/* Scale (base 4px) */
--ca-space-1: 4px --ca-space-4: 16px --ca-space-8: 32px
--ca-space-2: 8px --ca-space-5: 20px --ca-space-10: 40px
--ca-space-3: 12px --ca-space-6: 24px --ca-space-12: 48pxAppShell
<AppShellLayout
modules={MODULES}
pathname={$page.url.pathname}
persistPanel={false}
onNavigate={path => goto(path)}
>
{#snippet railBottom()}
<!-- Avatar, notifications, search — pinned to bottom of the rail -->
<button class="rail-btn" onclick={() => (cmdOpen = true)}>
<Search size={16} />
</button>
<NotificationBell {notifications} onRead={handleRead} />
<a href="/profile" class="rail-avatar">
<Avatar alt={user.name} size="sm" />
</a>
{/snippet}
<!-- No {#snippet header()} — each page owns its own header via AppList/AppForm/etc -->
{@render children?.()}
</AppShellLayout>NavModule shape:
const MODULES: NavModule[] = [
{
id: 'main', label: 'Main', icon: LayoutDashboard, basePath: '/app',
items: [
{ id: 'dashboard', label: 'Dashboard', icon: LayoutDashboard, path: '/app/dashboard' },
{ id: 'users', label: 'Users', icon: Users, path: '/app/users' },
],
},
];Theming
<!-- data-preset on <html> -->
<html data-preset="ocean">
<!-- amber | ocean | crimson | cyan | violet | emerald | rose | slate -->
<!-- dark mode -->
<html class="dark">colorScheme.toggle() // light ↔ dark
colorScheme.setPreset('ocean') // switch preset
colorScheme.isDark // reactive booleanSurface styles
Four visual modes controlled by a single attribute. Applied automatically via colorScheme.setStyle().
colorScheme.setStyle('glass') // default — Liquid Glass, blur + mesh background
colorScheme.setStyle('frosted') // reduced blur, subtle gradient
colorScheme.setStyle('solid') // no blur, opaque surfaces, pastel-friendly
colorScheme.setStyle('flat') // no elevation, border-based separation only<!-- Or set directly on <html> -->
<html data-preset="amber" data-style="solid">Override individual surfaces (CSS):
/* All overlays solid */
:root {
--ca-overlay-bg: var(--ca-surface-overlay);
--ca-overlay-blur: none;
}
/* Just Popover */
.ca-popover-content {
--ca-overlay-bg: var(--ca-surface-overlay);
--ca-overlay-blur: none;
}Surface layer tokens:
| Token | Components | Default |
|---|---|---|
| --ca-overlay-bg/blur/border/shadow | Popover, DropdownMenu | glass-thin |
| --ca-panel-bg/blur/border/shadow | NotificationBell, CommandPalette | glass-thick |
| --ca-chrome-bg/blur/border | Rail, BottomNav, AppShell | glass-chrome |
| --ca-upload-bg/blur | FileUpload drop zone | glass-ultra-thin |
Theme Editor
Full visual customization — color, radius, spacing, typography, shadows, glass. Generates a CSS block you paste into your app.
import { editorTokensToCSS, DEFAULT_CONFIG } from '@codeatlasdev/ui';
import type { ThemeConfig } from '@codeatlasdev/ui';
const css = editorTokensToCSS({
...DEFAULT_CONFIG,
hue: 260, // Ocean blue
chroma: 0.22,
lightL: 0.50,
radiusBase: 8, // Tighter corners
surfaceStyle: 'solid', // No glass
fontScale: 0.95, // Slightly smaller
});
// Paste css into your app.css after the global.css importThemeConfig fields:
| Field | Type | Default | Description |
|---|---|---|---|
| hue | number | 55 | Brand hue 0–360 |
| chroma | number | 0.21 | Brand saturation 0–0.32 |
| lightL | number | 0.54 | Brand lightness 0.30–0.72 |
| radiusBase | number | 10 | Base radius px — all sizes scale from this |
| spacingBase | number | 4 | Spacing unit px — scale-1 through scale-16 |
| fontScale | number | 1.0 | Font size multiplier |
| fontSans | string | 'Inter, ...' | Sans-serif font stack |
| fontMono | string | '"JetBrains Mono", ...' | Monospace font stack |
| shadowIntensity | number | 1.0 | Shadow opacity multiplier 0–2 |
| blurBase | number | 14 | Base blur px for glass effects |
| surfaceStyle | SurfaceStyle | 'glass' | 'glass' \| 'frosted' \| 'solid' \| 'flat' |
| pagePx | number | 24 | Page horizontal padding px |
| pagePt | number | 20 | Page top padding px |
| cardPadding | number | 20 | GlassCard padding px |
Key rules
- No inline styles — use CSS vars and classes. Only exception: dynamic CSS custom properties (
style="--bar-w:{pct}%"). - No
Stack gap={N}insideFormSection— uselayoutprop instead. - No
maxWidth/margin: 0 autowrappers —AppList,AppForm,AppSettingsfill available space. Width is controlled by the shell. - No
FormPage/ListPage— deprecated. UseAppForm/AppList. - No global header —
AppShellLayouthas no header slot. Each page defines its own header through the layout component.
Deprecated
| Deprecated | Use instead |
|---|---|
| FormPage | AppForm |
| ListPage | AppList |
