Domma Themes
Beautiful, consistent theming with light & dark modes across 11 colour families. Zero configuration required.
Choose a Theme
Click any theme to preview it instantly. Changes apply to the entire page.
<div class="theme-grid"><div class="theme-pair">
<div class="theme-card" data-theme="ocean-light">
<div class="theme-card-preview" style="background: linear-gradient(135deg, #e0f7fa, #b2ebf2);">
<span style="color: #006064;">Aa</span>
<div class="theme-card-accent" style="background: #0077b6;"></div>
</div>
<div class="theme-card-body">
<div class="theme-card-name">Ocean</div>
<div class="theme-card-mode">βοΈ Light</div>
</div>
</div>
<div class="theme-card" data-theme="ocean-dark">
<div class="theme-card-preview" style="background: linear-gradient(135deg, #023e8a, #0077b6);">
<span style="color: #caf0f8;">Aa</span>
<div class="theme-card-accent" style="background: #00b4d8;"></div>
</div>
<div class="theme-card-body">
<div class="theme-card-name">Ocean</div>
<div class="theme-card-mode">π Dark</div>
</div>
</div>
</div>
<div class="theme-pair">
<div class="theme-card" data-theme="forest-light">
<div class="theme-card-preview" style="background: linear-gradient(135deg, #e8f5e9, #c8e6c9);">
<span style="color: #1b5e20;">Aa</span>
<div class="theme-card-accent" style="background: #2d6a4f;"></div>
</div>
<div class="theme-card-body">
<div class="theme-card-name">Forest</div>
<div class="theme-card-mode">βοΈ Light</div>
</div>
</div>
<div class="theme-card" data-theme="forest-dark">
<div class="theme-card-preview" style="background: linear-gradient(135deg, #1b4332, #2d6a4f);">
<span style="color: #95d5b2;">Aa</span>
<div class="theme-card-accent" style="background: #52b788;"></div>
</div>
<div class="theme-card-body">
<div class="theme-card-name">Forest</div>
<div class="theme-card-mode">π Dark</div>
</div>
</div>
</div>
<div class="theme-pair">
<div class="theme-card" data-theme="sunset-light">
<div class="theme-card-preview" style="background: linear-gradient(135deg, #fff3e0, #ffe0b2);">
<span style="color: #bf360c;">Aa</span>
<div class="theme-card-accent" style="background: #c67b5c;"></div>
</div>
<div class="theme-card-body">
<div class="theme-card-name">Sunset</div>
<div class="theme-card-mode">βοΈ Light</div>
</div>
</div>
<div class="theme-card" data-theme="sunset-dark">
<div class="theme-card-preview" style="background: linear-gradient(135deg, #2d1b12, #4a2c1a);">
<span style="color: #e8c9a8;">Aa</span>
<div class="theme-card-accent" style="background: #c67b5c;"></div>
</div>
<div class="theme-card-body">
<div class="theme-card-name">Sunset</div>
<div class="theme-card-mode">π Dark</div>
</div>
</div>
</div>
<div class="theme-pair">
<div class="theme-card" data-theme="royal-light">
<div class="theme-card-preview" style="background: linear-gradient(135deg, #ede7f6, #d1c4e9);">
<span style="color: #4a148c;">Aa</span>
<div class="theme-card-accent" style="background: #7c3aed;"></div>
</div>
<div class="theme-card-body">
<div class="theme-card-name">Royal</div>
<div class="theme-card-mode">βοΈ Light</div>
</div>
</div>
<div class="theme-card" data-theme="royal-dark">
<div class="theme-card-preview" style="background: linear-gradient(135deg, #1e3a8a, #2b4c9e);">
<span style="color: #c7d2fe;">Aa</span>
<div class="theme-card-accent" style="background: #6495ed;"></div>
</div>
<div class="theme-card-body">
<div class="theme-card-name">Royal</div>
<div class="theme-card-mode">π Dark</div>
</div>
</div>
</div>
<div class="theme-pair">
<div class="theme-card" data-theme="lemon-light">
<div class="theme-card-preview" style="background: linear-gradient(135deg, #fffde7, #fff9c4);">
<span style="color: #f57f17;">Aa</span>
<div class="theme-card-accent" style="background: #fbbf24;"></div>
</div>
<div class="theme-card-body">
<div class="theme-card-name">Lemon</div>
<div class="theme-card-mode">βοΈ Light</div>
</div>
</div>
<div class="theme-card" data-theme="lemon-dark">
<div class="theme-card-preview" style="background: linear-gradient(135deg, #1a1200, #2d2000);">
<span style="color: #fef3c7;">Aa</span>
<div class="theme-card-accent" style="background: #d97706;"></div>
</div>
<div class="theme-card-body">
<div class="theme-card-name">Lemon</div>
<div class="theme-card-mode">π Dark</div>
</div>
</div>
</div>
<div class="theme-pair">
<div class="theme-card" data-theme="silver-light">
<div class="theme-card-preview" style="background: linear-gradient(135deg, #fafafa, #eeeeee);">
<span style="color: #424242;">Aa</span>
<div class="theme-card-accent" style="background: #6b7280;"></div>
</div>
<div class="theme-card-body">
<div class="theme-card-name">Silver</div>
<div class="theme-card-mode">βοΈ Light</div>
</div>
</div>
<div class="theme-card" data-theme="silver-dark">
<div class="theme-card-preview" style="background: linear-gradient(135deg, #1a1f2e, #252d3d);">
<span style="color: #e2e8f0;">Aa</span>
<div class="theme-card-accent" style="background: #94a3b8;"></div>
</div>
<div class="theme-card-body">
<div class="theme-card-name">Silver</div>
<div class="theme-card-mode">π Dark</div>
</div>
</div>
</div>
<div class="theme-pair">
<div class="theme-card" data-theme="charcoal-light">
<div class="theme-card-preview" style="background: linear-gradient(135deg, #f5f5f5, #e0e0e0);">
<span style="color: #212121;">Aa</span>
<div class="theme-card-accent" style="background: #78909c;"></div>
</div>
<div class="theme-card-body">
<div class="theme-card-name">Charcoal</div>
<div class="theme-card-mode">βοΈ Light</div>
</div>
</div>
<div class="theme-card" data-theme="charcoal-dark">
<div class="theme-card-preview" style="background: linear-gradient(135deg, #1a202c, #2d3748);">
<span style="color: #e2e8f0;">Aa</span>
<div class="theme-card-accent" style="background: #78909c;"></div>
</div>
<div class="theme-card-body">
<div class="theme-card-name">Charcoal</div>
<div class="theme-card-mode">π Dark</div>
</div>
</div>
</div>
<div class="theme-pair">
<div class="theme-card" data-theme="christmas-light">
<div class="theme-card-preview" style="background: linear-gradient(135deg, #F5F8FC, #EDF2F8);">
<span style="color: #1A2A1E;">Aa</span>
<div class="theme-card-accent" style="background: #2D6B3F;"></div>
</div>
<div class="theme-card-body">
<div class="theme-card-name">Christmas</div>
<div class="theme-card-mode">βοΈ Day</div>
</div>
</div>
<div class="theme-card" data-theme="christmas-dark">
<div class="theme-card-preview" style="background: linear-gradient(135deg, #0A1A10, #1A3020);">
<span style="color: #F0ECE4;">Aa</span>
<div class="theme-card-accent" style="background: #5CB875;"></div>
</div>
<div class="theme-card-body">
<div class="theme-card-name">Christmas</div>
<div class="theme-card-mode">π Eve</div>
</div>
</div>
</div>
<div class="theme-pair">
<div class="theme-card" data-theme="unicorn-light">
<div class="theme-card-preview" style="background: linear-gradient(135deg, #fce4ec, #f8bbd0);">
<span style="color: #880e4f;">Aa</span>
<div class="theme-card-accent" style="background: #e91e90;"></div>
</div>
<div class="theme-card-body">
<div class="theme-card-name">Unicorn</div>
<div class="theme-card-mode">βοΈ Light</div>
</div>
</div>
<div class="theme-card" data-theme="unicorn-dark">
<div class="theme-card-preview" style="background: linear-gradient(135deg, #1a0e24, #4a2660);">
<span style="color: #f8bbd0;">Aa</span>
<div class="theme-card-accent" style="background: #e91e90;"></div>
</div>
<div class="theme-card-body">
<div class="theme-card-name">Unicorn</div>
<div class="theme-card-mode">π Dark</div>
</div>
</div>
</div>
<div class="theme-pair">
<div class="theme-card" data-theme="dreamy-light">
<div class="theme-card-preview" style="background: linear-gradient(135deg, #efebe9, #d7ccc8);">
<span style="color: #3e2723;">Aa</span>
<div class="theme-card-accent" style="background: #8d6e63;"></div>
</div>
<div class="theme-card-body">
<div class="theme-card-name">Dreamy</div>
<div class="theme-card-mode">βοΈ Light</div>
</div>
</div>
<div class="theme-card" data-theme="dreamy-dark">
<div class="theme-card-preview" style="background: linear-gradient(135deg, #1c1410, #4e342e);">
<span style="color: #d7ccc8;">Aa</span>
<div class="theme-card-accent" style="background: #a1887f;"></div>
</div>
<div class="theme-card-body">
<div class="theme-card-name">Dreamy</div>
<div class="theme-card-mode">π Dark</div>
</div>
</div>
</div>
<div class="theme-pair">
<div class="theme-card" data-theme="grayve-light">
<div class="theme-card-preview" style="background: linear-gradient(135deg, #f8f9fa, #e0f7fa);">
<span style="color: #006064;">Aa</span>
<div class="theme-card-accent" style="background: #00838f;"></div>
</div>
<div class="theme-card-body">
<div class="theme-card-name">Grayve</div>
<div class="theme-card-mode">βοΈ Light</div>
</div>
</div>
<div class="theme-card" data-theme="grayve-dark">
<div class="theme-card-preview" style="background: linear-gradient(135deg, #1a1e21, #343A40);">
<span style="color: #DEE2E6;">Aa</span>
<div class="theme-card-accent" style="background: #00bcd4;"></div>
</div>
<div class="theme-card-body">
<div class="theme-card-name">Grayve</div>
<div class="theme-card-mode">π Dark</div>
</div>
</div>
</div>
<div class="theme-pair">
<div class="theme-card" data-theme="mint-light">
<div class="theme-card-preview" style="background: linear-gradient(135deg, #f0faf5, #d4f5e4);">
<span style="color: #1a2e25;">Aa</span>
<div class="theme-card-accent" style="background: #2e9e6e;"></div>
</div>
<div class="theme-card-body">
<div class="theme-card-name">Mint</div>
<div class="theme-card-mode">βοΈ Light</div>
</div>
</div>
<div class="theme-card" data-theme="mint-dark">
<div class="theme-card-preview" style="background: linear-gradient(135deg, #0f172a, #1e3a2f);">
<span style="color: #d4f5e4;">Aa</span>
<div class="theme-card-accent" style="background: #6dd5a8;"></div>
</div>
<div class="theme-card-body">
<div class="theme-card-name">Mint</div>
<div class="theme-card-mode">π Dark</div>
</div>
</div>
</div>
<div class="theme-pair">
<div class="theme-card" data-theme="wedding-light">
<div class="theme-card-preview" style="background: linear-gradient(135deg, #FFFDF7, #FDF5E0);">
<span style="color: #2C2416;">Aa</span>
<div class="theme-card-accent" style="background: #C5A55A;"></div>
</div>
<div class="theme-card-body">
<div class="theme-card-name">Wedding</div>
<div class="theme-card-mode">βοΈ Day</div>
</div>
</div>
<div class="theme-card" data-theme="wedding-dark">
<div class="theme-card-preview" style="background: linear-gradient(135deg, #12121F, #1A1A2E);">
<span style="color: #F0E8D8;">Aa</span>
<div class="theme-card-accent" style="background: #C5A55A;"></div>
</div>
<div class="theme-card-body">
<div class="theme-card-name">Wedding</div>
<div class="theme-card-mode">π Evening</div>
</div>
</div>
</div>
<div class="theme-pair">
<div class="theme-card" data-theme="core-light">
<div class="theme-card-preview" style="background: linear-gradient(135deg, #ffffff, #f3f4f6);">
<span style="color: #111827;">Aa</span>
<div class="theme-card-accent" style="background: #4f46e5;"></div>
</div>
<div class="theme-card-body">
<div class="theme-card-name">Core</div>
<div class="theme-card-mode">βοΈ Light only</div>
</div>
</div>
</div>
<div class="theme-pair">
<div class="theme-card" data-theme="corporate-light">
<div class="theme-card-preview" style="background: linear-gradient(135deg, #f8fafc, #ffffff);">
<span style="color: #0f172a;">Aa</span>
<div class="theme-card-accent" style="background: #1e40af;"></div>
</div>
<div class="theme-card-body">
<div class="theme-card-name">Corporate</div>
<div class="theme-card-mode">βοΈ Light</div>
</div>
</div>
<div class="theme-card" data-theme="corporate-dark">
<div class="theme-card-preview" style="background: linear-gradient(135deg, #0b0f19, #151c2c);">
<span style="color: #f8fafc;">Aa</span>
<div class="theme-card-accent" style="background: #2563eb;"></div>
</div>
<div class="theme-card-body">
<div class="theme-card-name">Corporate</div>
<div class="theme-card-mode">π Dark</div>
</div>
</div>
</div>
<div class="theme-pair theme-trio">
<div class="theme-card" data-theme="admin-smooth-steel">
<div class="theme-card-preview" style="background: linear-gradient(135deg, #353a43, #3c414a);">
<div class="theme-card-accent" style="background: #3b76bc;"></div>
</div>
<div class="theme-card-body">
<div class="theme-card-name">Admin Smooth</div>
<div class="theme-card-mode">Steel</div>
</div>
</div>
<div class="theme-card" data-theme="admin-slate-steel">
<div class="theme-card-preview" style="background: linear-gradient(135deg, #252c37, #2e3644);">
<div class="theme-card-accent" style="background: #3b76bc;"></div>
</div>
<div class="theme-card-body">
<div class="theme-card-name">Admin Slate</div>
<div class="theme-card-mode">Steel</div>
</div>
</div>
<div class="theme-card" data-theme="admin-sharp-steel">
<div class="theme-card-preview" style="background: linear-gradient(135deg, #eef1f6, #283242);">
<div class="theme-card-accent" style="background: #3b76bc;"></div>
</div>
<div class="theme-card-body">
<div class="theme-card-name">Admin Sharp</div>
<div class="theme-card-mode">Steel</div>
</div>
</div>
</div>
<div class="theme-pair theme-trio">
<div class="theme-card" data-theme="admin-smooth-indigo">
<div class="theme-card-preview" style="background: linear-gradient(135deg, #353a43, #3c414a);">
<div class="theme-card-accent" style="background: #5b63a8;"></div>
</div>
<div class="theme-card-body">
<div class="theme-card-name">Admin Smooth</div>
<div class="theme-card-mode">Indigo</div>
</div>
</div>
<div class="theme-card" data-theme="admin-slate-indigo">
<div class="theme-card-preview" style="background: linear-gradient(135deg, #252c37, #2e3644);">
<div class="theme-card-accent" style="background: #5b63a8;"></div>
</div>
<div class="theme-card-body">
<div class="theme-card-name">Admin Slate</div>
<div class="theme-card-mode">Indigo</div>
</div>
</div>
<div class="theme-card" data-theme="admin-sharp-indigo">
<div class="theme-card-preview" style="background: linear-gradient(135deg, #eef1f6, #283242);">
<div class="theme-card-accent" style="background: #5b63a8;"></div>
</div>
<div class="theme-card-body">
<div class="theme-card-name">Admin Sharp</div>
<div class="theme-card-mode">Indigo</div>
</div>
</div>
</div>
<div class="theme-pair theme-trio">
<div class="theme-card" data-theme="admin-smooth-teal">
<div class="theme-card-preview" style="background: linear-gradient(135deg, #353a43, #3c414a);">
<div class="theme-card-accent" style="background: #2a8178;"></div>
</div>
<div class="theme-card-body">
<div class="theme-card-name">Admin Smooth</div>
<div class="theme-card-mode">Teal</div>
</div>
</div>
<div class="theme-card" data-theme="admin-slate-teal">
<div class="theme-card-preview" style="background: linear-gradient(135deg, #252c37, #2e3644);">
<div class="theme-card-accent" style="background: #2a8178;"></div>
</div>
<div class="theme-card-body">
<div class="theme-card-name">Admin Slate</div>
<div class="theme-card-mode">Teal</div>
</div>
</div>
<div class="theme-card" data-theme="admin-sharp-teal">
<div class="theme-card-preview" style="background: linear-gradient(135deg, #eef1f6, #283242);">
<div class="theme-card-accent" style="background: #2a8178;"></div>
</div>
<div class="theme-card-body">
<div class="theme-card-name">Admin Sharp</div>
<div class="theme-card-mode">Teal</div>
</div>
</div>
</div>
</div>/**
* Choose a Theme - from public/showcase/themes.
*/
export function mount(root) {
function updateActiveStates() {
const current = Domma.theme.get();
$('.theme-card').removeClass('active');
$(`.theme-card[data-theme="${current}"]`).addClass('active');
}
$('.theme-card').on('click', function () {
Domma.theme.set($(this).data('theme'));
});
updateActiveStates();
}Admin Theme Builder
Pick a finish, then an accent. Nine looks, two clicks.
admin-sharp-steel<div class="dm-card" style="padding: var(--dm-space-5);">
<div class="dm-label">1 Β· Finish</div>
<div class="btn-group" id="admin-finish" role="group" aria-label="Finish" style="display:flex; gap:8px; margin-bottom:20px;">
<button type="button" class="btn" data-finish="smooth">Smooth</button>
<button type="button" class="btn" data-finish="slate">Slate</button>
<button type="button" class="btn active" data-finish="sharp">Sharp</button>
</div>
<div class="dm-label">2 Β· Accent</div>
<div id="admin-accent" style="display:flex; gap:12px; margin-top:8px;">
<button type="button" class="admin-acc active" data-accent="steel" data-tooltip="Steel Blue" aria-label="Steel Blue accent" style="flex:1; height:44px; border-radius:8px; border:2px solid var(--dm-text); background:#3b76bc;"></button>
<button type="button" class="admin-acc" data-accent="indigo" data-tooltip="Indigo" aria-label="Indigo accent" style="flex:1; height:44px; border-radius:8px; border:2px solid transparent; background:#5b63a8;"></button>
<button type="button" class="admin-acc" data-accent="teal" data-tooltip="Teal" aria-label="Teal accent" style="flex:1; height:44px; border-radius:8px; border:2px solid transparent; background:#2a8178;"></button>
</div>
<div class="dm-mt-4 dm-text-sm dm-text-secondary">Theme: <code id="admin-current">admin-sharp-steel</code></div>
</div>
<div class="dm-card" id="admin-preview" style="padding:0; overflow:hidden;">
<div style="display:flex; min-height:300px;">
<aside style="width:160px; background:var(--dm-sidebar-bg); color:var(--dm-sidebar-text); padding:16px;">
<strong style="color:var(--dm-text-inverse,#fff); display:block; margin-bottom:16px;">Admin</strong>
<div style="background:var(--dm-primary); color:var(--dm-primary-text, #fff); padding:8px; border-radius:6px; margin-bottom:4px;">Dashboard</div>
<div style="padding:8px;">Users</div>
<div style="padding:8px;">Reports</div>
</aside>
<div style="flex:1; background:var(--dm-background); padding:16px;">
<div class="dm-grid dm-gap-3 dm-mb-4" style="grid-template-columns:repeat(3,1fr);">
<div class="dm-card" style="padding:12px;"><div class="dm-text-xs dm-text-muted">Revenue</div><div style="font-size:20px; font-weight:700;">Β£48k</div></div>
<div class="dm-card" style="padding:12px;"><div class="dm-text-xs dm-text-muted">Orders</div><div style="font-size:20px; font-weight:700;">1,204</div></div>
<div class="dm-card" style="padding:12px;"><div class="dm-text-xs dm-text-muted">Active</div><div style="font-size:20px; font-weight:700;">317</div></div>
</div>
<div id="admin-preview-table"></div>
<div class="dm-mt-4" style="display:flex; gap:8px; align-items:center;">
<button type="button" class="btn btn-primary">Save</button>
<button type="button" class="btn">Cancel</button>
<span class="badge badge-success">Active</span>
</div>
</div>
</div>
</div>/**
* Admin Theme Builder - from public/showcase/themes.
*/
export function mount(root) {
E.tooltip('[data-tooltip]');
// --- Admin Theme Builder ---
(function initAdminBuilder() {
const state = { finish: 'sharp', accent: 'steel' };
const $current = $(root).find('#admin-current');
const $finish = $(root).find('#admin-finish');
const $accent = $(root).find('#admin-accent');
if (!$finish.length || !$accent.length || !$current.length) return;
// Wire real tooltips so data-tooltip isn't dead markup (one instance per button)
if (window.Domma && Domma.elements && E.tooltip) {
$accent.find('.admin-acc').each((i, btn) => {
E.tooltip(btn, { trigger: 'hover', position: 'top' });
});
}
if (window.Domma && Domma.tables) {
Domma.tables.create('#admin-preview-table', {
data: [
{ customer: 'Acme Ltd', plan: 'Pro', status: 'Active' },
{ customer: 'Globex', plan: 'Team', status: 'Trial' },
{ customer: 'Initech', plan: 'Free', status: 'Active' }
],
columns: [
{ key: 'customer', title: 'Customer' },
{ key: 'plan', title: 'Plan' },
{ key: 'status', title: 'Status' }
],
pagination: false
});
}
function apply() {
const name = `admin-${state.finish}-${state.accent}`;
$current.text(name);
Domma.theme.set(name);
}
$finish.on('click', '[data-finish]', function () {
state.finish = $(this).data('finish');
$finish.find('[data-finish]').removeClass('active');
$(this).addClass('active');
apply();
});
$accent.on('click', '[data-accent]', function () {
state.accent = $(this).data('accent');
$accent.find('[data-accent]').css('border-color', 'transparent');
$(this).css('border-color', 'var(--dm-text)');
apply();
});
})();
}<div class="audit-bar"><div>
<strong>Live preview</strong>
<p class="text-muted" style="margin: 0; font-size: 0.85rem;">Switch themes above; every component on this page reflects your choice instantly.</p>
</div>
<div class="audit-actions">
<span class="audit-summary idle" id="audit-summary">Audit off</span>
<button type="button" class="audit-toggle" id="audit-toggle">
<span data-icon="eye" data-icon-size="16"></span>
<span id="audit-toggle-text">Highlight contrast issues</span>
</button>
<button type="button" class="audit-toggle" id="audit-dump" title="Copy failures to clipboard for sharing">
<span data-icon="clipboard" data-icon-size="16"></span>
<span>Copy failures</span>
</button>
</div>
</div>/**
* Admin Theme Builder - from public/showcase/themes.
*/
export function mount(root) {
const colours = [
{name: 'Primary', var: '--dm-primary'},
{name: 'Secondary', var: '--dm-secondary'},
{name: 'Success', var: '--dm-success'},
{name: 'Danger', var: '--dm-danger'},
{name: 'Warning', var: '--dm-warning'},
{name: 'Info', var: '--dm-info'},
{name: 'Background', var: '--dm-background'},
{name: 'Surface', var: '--dm-surface'},
{name: 'Text', var: '--dm-text'},
{name: 'Border', var: '--dm-border'}
];
// ββ Contrast checker (WCAG AA) ββββββββββββββββββββββββββββββββββββββββ
let auditEnabled = false;
const $toggleBtn = $(root).find('#audit-toggle');
const $toggleText = $(root).find('#audit-toggle-text');
const $summary = $(root).find('#audit-summary');
function parseRgb(str) {
if (!str) return null;
const m = str.match(/rgba?\(([^)]+)\)/);
if (!m) return null;
const parts = m[1].split(',').map(p => parseFloat(p.trim()));
if (parts.length < 3) return null;
const a = parts[3] === undefined ? 1 : parts[3];
return { r: parts[0], g: parts[1], b: parts[2], a };
}
function relLuminance(c) {
const norm = v => {
v /= 255;
return v <= 0.03928 ? v / 12.92 : Math.pow((v + 0.055) / 1.055, 2.4);
};
return 0.2126 * norm(c.r) + 0.7152 * norm(c.g) + 0.0722 * norm(c.b);
}
function contrastRatio(fg, bg) {
const l1 = relLuminance(fg);
const l2 = relLuminance(bg);
const [light, dark] = l1 > l2 ? [l1, l2] : [l2, l1];
return (light + 0.05) / (dark + 0.05);
}
// Alpha-composite "src" over "dst" using standard "over" operator.
// Returns an opaque colour (a=1) approximating the rendered result.
function composite(src, dst) {
if (!src) return dst;
if (src.a >= 0.999) return { r: src.r, g: src.g, b: src.b, a: 1 };
const a = src.a + dst.a * (1 - src.a);
const r = (src.r * src.a + dst.r * dst.a * (1 - src.a)) / (a || 1);
const g = (src.g * src.a + dst.g * dst.a * (1 - src.a)) / (a || 1);
const b = (src.b * src.a + dst.b * dst.a * (1 - src.a)) / (a || 1);
return { r: Math.round(r), g: Math.round(g), b: Math.round(b), a: 1 };
}
function effectiveBg(el) {
// Walk up the ancestor chain collecting every translucent layer, then
// composite them top-down so a `rgba(brand, 0.15)` over a dark surface
// resolves to its real visual colour rather than the brand colour itself.
const layers = [];
let cur = el;
while (cur && cur !== document.documentElement) {
const bg = parseRgb(getComputedStyle(cur).backgroundColor);
if (bg && bg.a > 0) {
layers.push(bg);
if (bg.a >= 0.999) break;
}
cur = cur.parentElement;
}
let base = parseRgb(getComputedStyle(document.body).backgroundColor) || { r: 255, g: 255, b: 255, a: 1 };
if (!base.a || base.a < 0.999) base = { r: base.r || 255, g: base.g || 255, b: base.b || 255, a: 1 };
// layers[0] is the element itself, layers[N] is closest opaque ancestor; composite from back to front
for (let i = layers.length - 1; i >= 0; i--) base = composite(layers[i], base);
return base;
}
function hasOwnText(el) {
return Array.from(el.childNodes).some(n => n.nodeType === 3 && n.textContent.trim().length > 1);
}
function clearAuditMarks() {
$('.audit-fail').removeClass('audit-fail').removeAttr('data-audit-ratio');
}
// Web-component hosts present known false-positives for background detection
// (their Shadow DOM slots reparent children for layout while keeping them in
// light DOM, which confuses ancestor-walk-based bg detection). Skip elements
// inside these wrappers; the underlying button colours are tested elsewhere.
function isInsideUnauditedHost(el) {
let cur = el;
while (cur && cur !== document.body) {
const tag = cur.tagName ? cur.tagName.toLowerCase() : '';
if (tag === 'domma-tooltip' || tag === 'domma-number-badge') return true;
cur = cur.parentElement;
}
return false;
}
function runAudit() {
clearAuditMarks();
let failures = 0;
$('.audit-section *').each((i, el) => {
if (!hasOwnText(el)) return;
if (isInsideUnauditedHost(el)) return;
const styles = getComputedStyle(el);
if (styles.visibility === 'hidden' || styles.display === 'none') return;
const fg = parseRgb(styles.color);
const bg = effectiveBg(el);
if (!fg || !bg || fg.a < 0.1) return;
const ratio = contrastRatio(fg, bg);
const fontSize = parseFloat(styles.fontSize);
const fontWeight = parseInt(styles.fontWeight) || 400;
const isLarge = fontSize >= 24 || (fontSize >= 18.66 && fontWeight >= 700);
const threshold = isLarge ? 3 : 4.5;
if (ratio < threshold) {
el.classList.add('audit-fail');
el.setAttribute('data-audit-ratio', ratio.toFixed(2));
failures++;
}
});
$summary.removeClass('idle has-issues clean');
if (failures === 0) {
$summary.text('No issues found').addClass('clean');
} else {
$summary.text(`${failures} contrast issue${failures === 1 ? '' : 's'}`).addClass('has-issues');
}
}
$toggleBtn.on('click', () => {
auditEnabled = !auditEnabled;
$toggleBtn.toggleClass('active', auditEnabled);
$toggleText.text(auditEnabled ? 'Hide contrast issues' : 'Highlight contrast issues');
if (auditEnabled) {
runAudit();
} else {
clearAuditMarks();
$summary.removeClass('has-issues clean').addClass('idle').text('Audit off');
}
});
// ββ Dump failures βββββββββββββββββββββββββββββββββββββββββββββββββββββ
// Collects every failing element with its CSS-path-style selector and
// copies a structured report to the clipboard for sharing.
function describeElement(el) {
const parts = [];
let cur = el;
while (cur && cur !== document.body && parts.length < 5) {
let s = cur.tagName.toLowerCase();
if (cur.id) s += `#${cur.id}`;
else if (cur.className && typeof cur.className === 'string') {
const classes = cur.className.trim().split(/\s+/).slice(0, 4).join('.');
if (classes) s += `.${classes}`;
}
parts.unshift(s);
cur = cur.parentElement;
}
return parts.join(' > ');
}
$(root).find('#audit-dump').on('click', async () => {
let report = '';
try {
const allThemes = (Domma.theme && Domma.theme.listThemes) ? Domma.theme.listThemes() : [];
const originalTheme = (Domma.theme && Domma.theme.get) ? Domma.theme.get() : null;
const wasEnabled = auditEnabled;
report = `# Contrast failure dump - generated ${D().toISOString()}\n`;
report += `Themes scanned: ${allThemes.length}\n\n`;
const summary = [];
for (const theme of allThemes) {
// Apply theme and let CSS settle before measuring
if (Domma.theme && Domma.theme.set) Domma.theme.set(theme);
// Two RAFs + a settle delay so style/layout recalculation finishes
// before the audit reads computed values; otherwise dumps occasionally
// capture transitional state (e.g. half-applied theme classes).
await new Promise(r => requestAnimationFrame(() => requestAnimationFrame(r)));
await new Promise(r => setTimeout(r, 200));
clearAuditMarks();
runAudit();
const fails = [];
$('.audit-fail').each((i, el) => {
try {
const styles = getComputedStyle(el);
const fg = parseRgb(styles.color);
const bg = effectiveBg(el);
fails.push({
ratio: parseFloat(el.getAttribute('data-audit-ratio')) || 0,
path: describeElement(el),
text: (el.textContent || '').trim().slice(0, 60).replace(/\s+/g, ' '),
fg: fg ? `rgb(${fg.r},${fg.g},${fg.b})` : (styles.color || 'unknown'),
bg: bg ? `rgb(${bg.r},${bg.g},${bg.b})` : 'unknown',
fontSize: parseFloat(styles.fontSize) || 0,
fontWeight: parseInt(styles.fontWeight) || 400
});
} catch (innerErr) {
fails.push({ ratio: 0, path: '(error)', text: String(innerErr.message || innerErr), fg: '?', bg: '?', fontSize: 0, fontWeight: 0 });
}
});
fails.sort((a, b) => a.ratio - b.ratio);
summary.push({ theme, count: fails.length });
report += `\nββββββββββββββββββββββββββββββββββββββββββββββ\n`;
report += `THEME: ${theme} - ${fails.length} failures\n`;
report += `ββββββββββββββββββββββββββββββββββββββββββββββ\n`;
// Limit to 30 worst per theme to keep report manageable
const sample = fails.slice(0, 30);
sample.forEach((f, i) => {
report += `${i + 1}. [${f.ratio.toFixed(2)}:1] ${f.path}\n`;
report += ` text: "${f.text}"\n fg=${f.fg} bg=${f.bg} ${f.fontSize}px/${f.fontWeight}\n`;
});
if (fails.length > sample.length) {
report += `... and ${fails.length - sample.length} more (truncated)\n`;
}
}
// Restore original theme
if (originalTheme && Domma.theme && Domma.theme.set) Domma.theme.set(originalTheme);
if (!wasEnabled) {
clearAuditMarks();
summaryEl.classList.remove('has-issues', 'clean');
summaryEl.classList.add('idle');
summaryEl.textContent = 'Audit off';
} else {
runAudit();
}
const totalReport = '# SUMMARY\n' + summary.map(s => ` ${s.theme.padEnd(20)} ${s.count}`).join('\n') + '\n\n' + report;
report = totalReport;
console.log(report);
} catch (err) {
report = '# DUMP ERROR\n' + (err && err.stack ? err.stack : String(err));
console.error(report);
}
// Show a modal-style textarea with the report so the user can copy it
// regardless of clipboard API availability.
// .remove() on an empty collection is a no-op, so no guard is needed.
$(root).find('#audit-dump-modal').remove();
const overlay = document.createElement('div');
overlay.id = 'audit-dump-modal';
Object.assign(overlay.style, {
position: 'fixed', inset: '0',
background: 'rgba(0,0,0,0.55)',
display: 'flex', alignItems: 'center', justifyContent: 'center',
zIndex: '99999', padding: '2rem'
});
const panel = document.createElement('div');
Object.assign(panel.style, {
background: '#fff', color: '#000',
borderRadius: '8px',
maxWidth: '800px', maxHeight: '80vh', width: '100%',
display: 'flex', flexDirection: 'column', overflow: 'hidden',
boxShadow: '0 12px 40px rgba(0,0,0,0.4)'
});
const header = document.createElement('div');
header.style.cssText = 'padding: 0.75rem 1rem; border-bottom: 1px solid #ddd; display: flex; justify-content: space-between; align-items: center;';
const title = document.createElement('strong');
title.textContent = 'Contrast failure dump (Ctrl+A then Ctrl+C to copy)';
const closeBtn = document.createElement('button');
closeBtn.textContent = 'Γ';
closeBtn.style.cssText = 'background: none; border: none; font-size: 1.5rem; cursor: pointer; line-height: 1; color: #000;';
$(closeBtn).on('click', () => overlay.remove());
header.appendChild(title);
header.appendChild(closeBtn);
const ta = document.createElement('textarea');
ta.value = report;
ta.readOnly = true;
Object.assign(ta.style, {
flex: '1', width: '100%', border: 'none',
padding: '1rem', fontFamily: 'monospace', fontSize: '12px',
resize: 'none', outline: 'none', background: '#f8f9fa', color: '#000'
});
panel.appendChild(header);
panel.appendChild(ta);
overlay.appendChild(panel);
document.body.appendChild(overlay);
$(overlay).on('click', (e) => { if (e.target === overlay) overlay.remove(); });
setTimeout(() => { ta.focus(); ta.select(); }, 50);
// Best-effort attempt at modern clipboard, but never block the modal.
if (navigator.clipboard && navigator.clipboard.writeText) {
navigator.clipboard.writeText(report).catch(() => {});
}
});
}1. Branding & Surfaces
Brand colours (text on coloured background)
<div class="branding-strip"><div class="swatch-primary">Primary</div>
<div class="swatch-secondary">Secondary</div>
<div class="swatch-success">Success</div>
<div class="swatch-warning">Warning</div>
<div class="swatch-danger">Danger</div>
<div class="swatch-info">Info</div>
</div>Surface tokens (text on neutral backgrounds)
<div class="surface-strip"><div class="surf-bg">Background - body text</div>
<div class="surf-surface">Surface - card text</div>
<div class="surf-alt">Surface alt - subtle text</div>
<div class="surf-raised">Surface raised - elevated text</div>
</div>Colour palette (every theme token)
<div id="colour-palette" class="themes-colour-palette"></div>/**
* 1. Branding & Surfaces - from public/showcase/themes.
*/
export function mount(root) {
const colours = [
{name: 'Primary', var: '--dm-primary'},
{name: 'Secondary', var: '--dm-secondary'},
{name: 'Success', var: '--dm-success'},
{name: 'Danger', var: '--dm-danger'},
{name: 'Warning', var: '--dm-warning'},
{name: 'Info', var: '--dm-info'},
{name: 'Background', var: '--dm-background'},
{name: 'Surface', var: '--dm-surface'},
{name: 'Text', var: '--dm-text'},
{name: 'Border', var: '--dm-border'}
];
function renderPalette() {
const $host = $(root).find('#colour-palette');
if (!$host.length) return;
const host = $host.empty()[0];
colours.forEach(c => {
const swatch = document.createElement('div');
swatch.className = 'themes-colour-swatch';
const circle = document.createElement('div');
circle.className = 'themes-colour-circle';
circle.style.background = `var(${c.var})`;
const label = document.createElement('div');
label.className = 'themes-colour-label';
label.textContent = c.name;
swatch.appendChild(circle);
swatch.appendChild(label);
host.appendChild(swatch);
});
}
function updateActiveStates() {
const current = Domma.theme.get();
$('.theme-card').removeClass('active');
$(`.theme-card[data-theme="${current}"]`).addClass('active');
}
renderPalette();
Domma.theme.onChange(() => {
updateActiveStates();
setTimeout(() => {
renderPalette();
if (auditEnabled) runAudit();
}, 80);
});
// ββ Contrast checker (WCAG AA) ββββββββββββββββββββββββββββββββββββββββ
let auditEnabled = false;
function parseRgb(str) {
if (!str) return null;
const m = str.match(/rgba?\(([^)]+)\)/);
if (!m) return null;
const parts = m[1].split(',').map(p => parseFloat(p.trim()));
if (parts.length < 3) return null;
const a = parts[3] === undefined ? 1 : parts[3];
return { r: parts[0], g: parts[1], b: parts[2], a };
}
function relLuminance(c) {
const norm = v => {
v /= 255;
return v <= 0.03928 ? v / 12.92 : Math.pow((v + 0.055) / 1.055, 2.4);
};
return 0.2126 * norm(c.r) + 0.7152 * norm(c.g) + 0.0722 * norm(c.b);
}
function contrastRatio(fg, bg) {
const l1 = relLuminance(fg);
const l2 = relLuminance(bg);
const [light, dark] = l1 > l2 ? [l1, l2] : [l2, l1];
return (light + 0.05) / (dark + 0.05);
}
// Alpha-composite "src" over "dst" using standard "over" operator.
// Returns an opaque colour (a=1) approximating the rendered result.
function composite(src, dst) {
if (!src) return dst;
if (src.a >= 0.999) return { r: src.r, g: src.g, b: src.b, a: 1 };
const a = src.a + dst.a * (1 - src.a);
const r = (src.r * src.a + dst.r * dst.a * (1 - src.a)) / (a || 1);
const g = (src.g * src.a + dst.g * dst.a * (1 - src.a)) / (a || 1);
const b = (src.b * src.a + dst.b * dst.a * (1 - src.a)) / (a || 1);
return { r: Math.round(r), g: Math.round(g), b: Math.round(b), a: 1 };
}
function effectiveBg(el) {
// Walk up the ancestor chain collecting every translucent layer, then
// composite them top-down so a `rgba(brand, 0.15)` over a dark surface
// resolves to its real visual colour rather than the brand colour itself.
const layers = [];
let cur = el;
while (cur && cur !== document.documentElement) {
const bg = parseRgb(getComputedStyle(cur).backgroundColor);
if (bg && bg.a > 0) {
layers.push(bg);
if (bg.a >= 0.999) break;
}
cur = cur.parentElement;
}
let base = parseRgb(getComputedStyle(document.body).backgroundColor) || { r: 255, g: 255, b: 255, a: 1 };
if (!base.a || base.a < 0.999) base = { r: base.r || 255, g: base.g || 255, b: base.b || 255, a: 1 };
// layers[0] is the element itself, layers[N] is closest opaque ancestor; composite from back to front
for (let i = layers.length - 1; i >= 0; i--) base = composite(layers[i], base);
return base;
}
function hasOwnText(el) {
return Array.from(el.childNodes).some(n => n.nodeType === 3 && n.textContent.trim().length > 1);
}
function clearAuditMarks() {
$('.audit-fail').removeClass('audit-fail').removeAttr('data-audit-ratio');
}
// Web-component hosts present known false-positives for background detection
// (their Shadow DOM slots reparent children for layout while keeping them in
// light DOM, which confuses ancestor-walk-based bg detection). Skip elements
// inside these wrappers; the underlying button colours are tested elsewhere.
function isInsideUnauditedHost(el) {
let cur = el;
while (cur && cur !== document.body) {
const tag = cur.tagName ? cur.tagName.toLowerCase() : '';
if (tag === 'domma-tooltip' || tag === 'domma-number-badge') return true;
cur = cur.parentElement;
}
return false;
}
function runAudit() {
clearAuditMarks();
let failures = 0;
$('.audit-section *').each((i, el) => {
if (!hasOwnText(el)) return;
if (isInsideUnauditedHost(el)) return;
const styles = getComputedStyle(el);
if (styles.visibility === 'hidden' || styles.display === 'none') return;
const fg = parseRgb(styles.color);
const bg = effectiveBg(el);
if (!fg || !bg || fg.a < 0.1) return;
const ratio = contrastRatio(fg, bg);
const fontSize = parseFloat(styles.fontSize);
const fontWeight = parseInt(styles.fontWeight) || 400;
const isLarge = fontSize >= 24 || (fontSize >= 18.66 && fontWeight >= 700);
const threshold = isLarge ? 3 : 4.5;
if (ratio < threshold) {
el.classList.add('audit-fail');
el.setAttribute('data-audit-ratio', ratio.toFixed(2));
failures++;
}
});
$summary.removeClass('idle has-issues clean');
if (failures === 0) {
$summary.text('No issues found').addClass('clean');
} else {
$summary.text(`${failures} contrast issue${failures === 1 ? '' : 's'}`).addClass('has-issues');
}
}
}2. Typography
H3 - Engineering team headcount
H4 - Customer satisfaction up four points
H5 - Migration completed last Tuesday
H6 - Last updated by Darryl on 22 April 2026
Standard paragraph text - the rain in Spain falls mainly on the plain. This is a hyperlink with an inline strong, an inline emphasis, and some inline code. Visited links should also have legible colour.
"Design is not just what it looks like and feels like. Design is how it works." - Steve Jobs
Muted helper text - usually used for form hints or metadata.
Secondary text - slightly de-emphasised body copy.
const order = { id: 'INV-2026-0042', customer: 'Acme Ltd', total: 4250.00 };
Domma.http.post('/api/invoices', order).then(res => {
E.toast('Invoice saved', { type: 'success' });
});
3. Buttons
Solid variants
<div class="audit-row"><button type="button" class="btn btn-primary">Primary</button>
<button type="button" class="btn btn-secondary">Secondary</button>
<button type="button" class="btn btn-success">Success</button>
<button type="button" class="btn btn-warning">Warning</button>
<button type="button" class="btn btn-danger">Danger</button>
<button type="button" class="btn btn-info">Info</button>
<button type="button" class="btn btn-ghost">Ghost</button>
</div>Outline & size variants
<div class="audit-row"><button type="button" class="btn btn-outline">Outline</button>
<button type="button" class="btn btn-primary btn-sm">Small primary</button>
<button type="button" class="btn btn-primary btn-lg">Large primary</button>
<button type="button" class="btn btn-primary" disabled="">Disabled primary</button>
<button type="button" class="btn btn-secondary" disabled="">Disabled secondary</button>
</div>Buttons with icons
<div class="audit-row"><button type="button" class="btn btn-primary"><span data-icon="check" data-icon-size="16"></span> Approve</button>
<button type="button" class="btn btn-danger"><span data-icon="trash" data-icon-size="16"></span> Delete</button>
<button type="button" class="btn btn-info"><span data-icon="info" data-icon-size="16"></span> Details</button>
</div>4. Forms
Bare HTML inputs (.form-input)
<div class="audit-form-grid"><div>
<label for="in-text">Customer name</label>
<input id="in-text" type="text" class="form-input" value="Alice Bloggs">
</div>
<div>
<label for="in-email">Email address</label>
<input id="in-email" type="email" class="form-input" value="alice@example.com">
</div>
<div>
<label for="in-pass">Password</label>
<input id="in-pass" type="password" class="form-input" value="hunter2hunter2">
</div>
<div>
<label for="in-num">Order quantity</label>
<input id="in-num" type="number" class="form-input" value="42">
</div>
<div>
<label for="in-date">Invoice date</label>
<input id="in-date" type="date" class="form-input" value="2026-04-22">
</div>
<div>
<label for="in-select">Status</label>
<select id="in-select" class="form-input">
<option>Draft</option>
<option selected="">In review</option>
<option>Approved</option>
<option>Rejected</option>
</select>
</div>
<div style="grid-column: 1 / -1;">
<label for="in-textarea">Notes</label>
<textarea id="in-textarea" class="form-input" rows="3">Customer requested next-day delivery; left a note to wrap securely.</textarea>
</div>
<div>
<label><input type="checkbox" checked=""> Subscribe to weekly digest</label><br>
<label><input type="checkbox"> Receive marketing emails</label>
</div>
<div>
<label><input type="radio" name="r-priority" checked=""> Standard priority</label><br>
<label><input type="radio" name="r-priority"> Express priority</label>
</div>
<div>
<label for="in-file">Upload signed PDF</label>
<input id="in-file" type="file" class="form-input">
</div>
</div>F.create() - blueprint-driven form
<div id="audit-form-host"></div>/**
* 4. Forms - from public/showcase/themes.
*/
export function mount(root) {
// F.create() - schema, data, options β renderTo(selector)
if (window.F && F.create) {
try {
const blueprint = {
name: { type: M.types.string, required: true, label: 'Customer name' },
email: { type: M.types.string, required: true, label: 'Email', formConfig: { inputType: 'email' } },
country: { type: M.types.string, label: 'Country', formConfig: { component: 'select', options: ['United Kingdom', 'United States', 'France', 'Germany'] } },
notes: { type: M.types.string, label: 'Notes', formConfig: { component: 'textarea' } }
};
F.create(blueprint, {}, { layout: 'grid', columns: 2, submitLabel: 'Save customer' }).renderTo('#audit-form-host');
} catch (err) {
$(root).find('#audit-form-host').append(
$('<div class="alert alert-warning"></div>').text(
'F.create() blueprint demo unavailable: ' + err.message
)
);
}
}
}5. Cards - header / footer combinations
Four standard combinations
Body only. No header, no footer. Just a single padded surface.
Header only
Header above, body below. No footer.
Body with a footer beneath. No header.
Header and footer
Full sandwich - header, body, and footer.
<div class="audit-grid"><div class="card">
<div class="card-body">
<p class="card-text"><strong>Body only.</strong> No header, no footer. Just a single padded surface.</p>
</div>
</div>
<div class="card">
<div class="card-header"><h4 class="card-title">Header only</h4></div>
<div class="card-body">
<p class="card-text">Header above, body below. No footer.</p>
</div>
</div>
<div class="card">
<div class="card-body">
<p class="card-text">Body with a footer beneath. No header.</p>
</div>
<div class="card-footer">Footer content</div>
</div>
<div class="card">
<div class="card-header"><h4 class="card-title">Header and footer</h4></div>
<div class="card-body">
<p class="card-text">Full sandwich - header, body, and footer.</p>
</div>
<div class="card-footer">Footer content</div>
</div>
</div>Hover & primary variants
Hover card
Hover the card to see the elevation effect adjust to the active theme.
card-primary
Solid primary-tinted card. Watch this one carefully on light themes - header text often breaks here.
<div class="audit-grid"><div class="card card-hover">
<div class="card-header"><h4 class="card-title">Hover card</h4></div>
<div class="card-body"><p class="card-text">Hover the card to see the elevation effect adjust to the active theme.</p></div>
</div>
<div class="card card-primary">
<div class="card-header"><h4 class="card-title">card-primary</h4></div>
<div class="card-body"><p class="card-text">Solid primary-tinted card. Watch this one carefully on light themes - header text often breaks here.</p></div>
<div class="card-footer">Footer content</div>
</div>
</div>Accent cards (left-border colour cue + label)
Primary card-accent
Default accent - uses --dm-primary for the left border.
Success card-accent-success
Tinted accent with a green left border. Use for confirmation and completion states.
Warning card-accent-warning
Yellow accent. Lemon-light theme has historically broken this combination.
Danger card-accent-danger
Red accent. Used for destructive confirmation flows.
Info card-accent-info
Blue accent. Mint-light historically had cyan-on-mint contrast issues here.
Collapsible card
Click the header to toggle. State persists to localStorage.
<div class="audit-grid"><div class="card card-accent">
<div class="card-header"><h4 class="card-title"><span class="accent-tag primary">Primary</span> card-accent</h4></div>
<div class="card-body"><p class="card-text">Default accent - uses <code>--dm-primary</code> for the left border.</p></div>
</div>
<div class="card card-accent card-accent-success">
<div class="card-header"><h4 class="card-title"><span class="accent-tag success">Success</span> card-accent-success</h4></div>
<div class="card-body"><p class="card-text">Tinted accent with a green left border. Use for confirmation and completion states.</p></div>
</div>
<div class="card card-accent card-accent-warning">
<div class="card-header"><h4 class="card-title"><span class="accent-tag warning">Warning</span> card-accent-warning</h4></div>
<div class="card-body"><p class="card-text">Yellow accent. Lemon-light theme has historically broken this combination.</p></div>
</div>
<div class="card card-accent card-accent-danger">
<div class="card-header"><h4 class="card-title"><span class="accent-tag danger">Danger</span> card-accent-danger</h4></div>
<div class="card-body"><p class="card-text">Red accent. Used for destructive confirmation flows.</p></div>
</div>
<div class="card card-accent card-accent-info">
<div class="card-header"><h4 class="card-title"><span class="accent-tag info">Info</span> card-accent-info</h4></div>
<div class="card-body"><p class="card-text">Blue accent. Mint-light historically had cyan-on-mint contrast issues here.</p></div>
</div>
<div class="card card-collapsible" id="audit-card-collapsible">
<div class="card-header"><h4 class="card-title">Collapsible card</h4></div>
<div class="card-body"><p class="card-text">Click the header to toggle. State persists to localStorage.</p></div>
</div>
</div>/**
* 5. Cards - header / footer combinations - from public/showcase/themes.
*/
export function mount(root) {
if (E.card) E.card(root.querySelector('#audit-card-collapsible'), { collapsible: true });
}6. Badges, Pills & Number Badges
Badges
Primary
Secondary
Success
Warning
Danger
Info
Pills
Primary pill
Secondary pill
Light pill
Outline pill
Outline primary
Small
Large
Number badges (notification counters)
<div class="audit-row"><button type="button" class="btn btn-primary" id="numbadge-host-1" style="position:relative;">Inbox</button>
<button type="button" class="btn btn-secondary" id="numbadge-host-2" style="position:relative;">Tasks</button>
<button type="button" class="btn btn-danger" id="numbadge-host-3" style="position:relative;">Alerts</button>
</div>/**
* 6. Badges, Pills & Number Badges - from public/showcase/themes.
*/
export function mount(root) {
if (E.numberBadge) {
E.numberBadge(root.querySelector('#numbadge-host-1'), { count: 12 });
E.numberBadge(root.querySelector('#numbadge-host-2'), { count: 4 });
E.numberBadge(root.querySelector('#numbadge-host-3'), { count: 99, max: 99 });
}
}7. Alerts
Primary alert. System maintenance scheduled for 02:00-04:00 on Sunday.
Success. All overnight CI jobs completed without errors.
Warning. Disk usage on prod-db-01 is at 85% - consider archiving older logs.
Danger. Webhook delivery to Stripe failed three consecutive times.
Info. A new release of Domma is available - see the changelog.
8. Tabs & Accordion
Tab content one - body text rendered inside the active panel. Headings, links and emphasised text should remain legible across all themes.
Tab content two - secondary tab. Inline link visibility check.
Tab content three - settings tab content.
Accordion body text. The chevron icon and the header background should both be readable.
Body of the second section.
<div id="audit-tabs" class="tabs">
<div class="tab-list" role="tablist">
<button type="button" class="tab-item active" data-tab="0" role="tab">Overview</button>
<button type="button" class="tab-item" data-tab="1" role="tab">Activity</button>
<button type="button" class="tab-item" data-tab="2" role="tab">Settings</button>
</div>
<div class="tab-content">
<div class="tab-panel active" role="tabpanel"><p>Tab content one - body text rendered inside the active panel. Headings, links and emphasised text should remain legible across all themes.</p></div>
<div class="tab-panel" role="tabpanel"><p>Tab content two - secondary tab. <a href="#">Inline link</a> visibility check.</p></div>
<div class="tab-panel" role="tabpanel"><p>Tab content three - settings tab content.</p></div>
</div>
</div>
<div id="audit-accordion" class="accordion" style="margin-top: 1rem;">
<div class="accordion-item">
<button class="accordion-header" type="button">First section header</button>
<div class="accordion-body"><p>Accordion body text. The chevron icon and the header background should both be readable.</p></div>
</div>
<div class="accordion-item">
<button class="accordion-header" type="button">Second section header</button>
<div class="accordion-body"><p>Body of the second section.</p></div>
</div>
</div>/**
* 8. Tabs & Accordion - from public/showcase/themes.
*/
export function mount(root) {
// ββ Initialise every Domma component βββββββββββββββββββββββββββββββββ
E.tabs(root.querySelector('#audit-tabs'));
E.accordion(root.querySelector('#audit-accordion'));
}9. Tables
Bare HTML table (.table)
| ID | Customer | Status | Total |
|---|---|---|---|
| INV-0001 | Acme Ltd | Paid | Β£4,250.00 |
| INV-0002 | Globex | Pending | Β£1,180.00 |
| INV-0003 | Initech | Overdue | Β£760.00 |
| INV-0004 | Soylent Corp | Paid | Β£12,500.00 |
T.create() - sortable, searchable, paginated
<div id="audit-table-host"></div>/**
* 9. Tables - from public/showcase/themes.
*/
export function mount(root) {
if (window.T && T.create) {
const data = [
{ id: 'INV-0001', customer: 'Acme Ltd', status: 'Paid', total: 4250.00 },
{ id: 'INV-0002', customer: 'Globex', status: 'Pending', total: 1180.00 },
{ id: 'INV-0003', customer: 'Initech', status: 'Overdue', total: 760.00 },
{ id: 'INV-0004', customer: 'Soylent Corp', status: 'Paid', total: 12500.00 },
{ id: 'INV-0005', customer: 'Stark Inds.', status: 'Paid', total: 8700.50 },
{ id: 'INV-0006', customer: 'Wayne Ent.', status: 'Pending', total: 3200.00 },
{ id: 'INV-0007', customer: 'Oscorp', status: 'Overdue', total: 1900.00 },
{ id: 'INV-0008', customer: 'Tyrell Corp', status: 'Paid', total: 5450.75 }
];
T.create('#audit-table-host', {
data,
columns: [
{ key: 'id', label: 'Invoice' },
{ key: 'customer', label: 'Customer' },
{ key: 'status', label: 'Status' },
{ key: 'total', label: 'Total', format: v => 'Β£' + v.toLocaleString('en-GB', { minimumFractionDigits: 2 }) }
],
pagination: true,
pageSize: 5,
striped: true
});
}
}10. Lists & List Groups
- Onboarding session with the new client
- Migrate legacy invoices to the new system
- Review Q2 procurement reports
- Stage the migration
- Run the dry-run script
- Cut over and verify
- API key
- A unique identifier used to authenticate requests.
- Rate limit
- 1,000 requests per hour, per key.
<div class="row"><div class="col-6">
<div class="audit-label">Standard lists</div>
<ul>
<li>Onboarding session with the new client</li>
<li>Migrate legacy invoices to the new system</li>
<li>Review Q2 procurement reports</li>
</ul>
<ol>
<li>Stage the migration</li>
<li>Run the dry-run script</li>
<li>Cut over and verify</li>
</ol>
<dl>
<dt>API key</dt>
<dd>A unique identifier used to authenticate requests.</dd>
<dt>Rate limit</dt>
<dd>1,000 requests per hour, per key.</dd>
</dl>
</div>
<div class="col-6">
<div class="audit-label">List group with colour variants</div>
<div id="audit-listgroup" class="list-group">
<div class="list-group-item list-group-item-action">Q1 sales report</div>
<div class="list-group-item list-group-item-action active">Selected item</div>
<div class="list-group-item list-group-item-action list-group-item-primary">Primary tinted</div>
<div class="list-group-item list-group-item-action list-group-item-success">Success tinted</div>
<div class="list-group-item list-group-item-action list-group-item-warning">Warning tinted</div>
<div class="list-group-item list-group-item-action list-group-item-danger">Danger tinted</div>
<div class="list-group-item list-group-item-action list-group-item-info">Info tinted</div>
<div class="list-group-item list-group-item-action disabled">Disabled item</div>
</div>
</div>
</div>/**
* 10. Lists & List Groups - from public/showcase/themes.
*/
export function mount(root) {
E.listGroup(root.querySelector('#audit-listgroup'), { selectable: true, multiSelect: false });
}11. Navigation Components
Breadcrumbs
<div id="audit-breadcrumbs"></div>/**
* 11. Navigation Components - from public/showcase/themes.
*/
export function mount(root) {
E.breadcrumbs(root.querySelector('#audit-breadcrumbs'), {
items: [
{ text: 'Home', url: '#' },
{ text: 'Showcase', url: '#' },
{ text: 'Themes', active: true }
],
separator: 'chevron'
});
}Pagination
βΉ Prev
1
2
3
4
β¦
12
Next βΊ
Button group - single-select
<div id="audit-bg-single" class="btn-group" role="group">
<button type="button" class="btn btn-outline active" data-value="d">Daily</button>
<button type="button" class="btn btn-outline" data-value="w">Weekly</button>
<button type="button" class="btn btn-outline" data-value="m">Monthly</button>
<button type="button" class="btn btn-outline" data-value="q">Quarterly</button>
</div>/**
* 11. Navigation Components - from public/showcase/themes.
*/
export function mount(root) {
E.buttonGroup(root.querySelector('#audit-bg-single'), { mode: 'single' });
}Dropdown
<div id="audit-dropdown" class="dropdown" style="display:inline-block;">
<button type="button" class="btn btn-secondary dropdown-toggle">Actions βΎ</button>
<div class="dropdown-menu">
<a href="#" class="dropdown-item">Edit record</a>
<a href="#" class="dropdown-item">Duplicate</a>
<a href="#" class="dropdown-item">Archive</a>
<hr>
<a href="#" class="dropdown-item text-danger">Delete permanently</a>
</div>
</div>/**
* 11. Navigation Components - from public/showcase/themes.
*/
export function mount(root) {
E.dropdown(root.querySelector('#audit-dropdown'));
}12. Loaders & Progress
<div class="audit-grid"><div class="audit-block"><div class="audit-label">Spinner</div><div id="loader-spin" style="height:48px;"></div></div>
<div class="audit-block"><div class="audit-label">Dots</div><div id="loader-dots" style="height:48px;"></div></div>
<div class="audit-block"><div class="audit-label">Pulse</div><div id="loader-pulse" style="height:48px;"></div></div>
<div class="audit-block"><div class="audit-label">Bars</div><div id="loader-bars" style="height:48px;"></div></div>
</div>/**
* 12. Loaders & Progress - from public/showcase/themes.
*/
export function mount(root) {
E.loader(root.querySelector('#loader-spin'), { type: 'spinner', size: 'medium' }).show();
E.loader(root.querySelector('#loader-dots'), { type: 'dots', size: 'medium' }).show();
E.loader(root.querySelector('#loader-pulse'), { type: 'pulse', size: 'medium' }).show();
E.loader(root.querySelector('#loader-bars'), { type: 'bars', size: 'medium' }).show();
}Progress bars
<div class="progress-bar" style="width: 32%;">32%</div>
<div class="progress-bar progress-bar-success" style="width: 100%;">Complete</div>
<div class="progress-bar progress-bar-warning" style="width: 67%;">67%</div>
<div class="progress-bar progress-bar-danger" style="width: 12%;">12%</div>13. Tooltips
<div class="audit-row"><button type="button" class="btn btn-primary" data-tooltip="Tooltip on top" data-tooltip-position="top">Hover (top)</button>
<button type="button" class="btn btn-secondary" data-tooltip="Tooltip on bottom" data-tooltip-position="bottom">Hover (bottom)</button>
<button type="button" class="btn btn-info" data-tooltip="Tooltip on left" data-tooltip-position="left">Hover (left)</button>
<button type="button" class="btn btn-warning" data-tooltip="Tooltip on right" data-tooltip-position="right">Hover (right)</button>
</div>14. Modals, Dialogs & Toasts
Trigger buttons
<div class="audit-row"><button type="button" class="btn btn-primary" id="audit-open-modal">Open modal</button>
<button type="button" class="btn btn-info" id="audit-open-alert">Show alert dialog</button>
<button type="button" class="btn btn-warning" id="audit-open-confirm">Show confirm dialog</button>
<button type="button" class="btn btn-secondary" id="audit-open-prompt">Show prompt dialog</button>
<button type="button" class="btn btn-success" id="audit-toast-success">Toast: success</button>
<button type="button" class="btn btn-danger" id="audit-toast-error">Toast: error</button>
</div>/**
* 14. Modals, Dialogs & Toasts - from public/showcase/themes.
*/
export function mount(root) {
// Modal trigger
function buildPopupModal() {
const m = document.createElement('div');
m.id = 'audit-popup-modal';
m.className = 'modal';
const header = document.createElement('div');
header.className = 'modal-header';
const title = document.createElement('h5');
title.className = 'modal-title';
title.textContent = 'Modal popup';
const closeX = document.createElement('button');
closeX.className = 'modal-close';
closeX.setAttribute('aria-label', 'Close');
closeX.textContent = 'Γ';
header.appendChild(title);
header.appendChild(closeX);
const body = document.createElement('div');
body.className = 'modal-body';
const p = document.createElement('p');
p.textContent = 'This is a popup modal. Inspect the header, body, footer and close-button contrast.';
body.appendChild(p);
const footer = document.createElement('div');
footer.className = 'modal-footer';
const btnCancel = document.createElement('button');
btnCancel.className = 'btn btn-secondary modal-close';
btnCancel.textContent = 'Cancel';
const btnConfirm = document.createElement('button');
btnConfirm.className = 'btn btn-primary modal-close';
btnConfirm.textContent = 'Confirm';
footer.appendChild(btnCancel);
footer.appendChild(btnConfirm);
m.appendChild(header);
m.appendChild(body);
m.appendChild(footer);
document.body.appendChild(m);
return m;
}
$(root).find('#audit-open-modal').on('click', () => {
if (!$(root).find('#audit-popup-modal').length) buildPopupModal();
E.modal(root.querySelector('#audit-popup-modal')).open();
});
$(root).find('#audit-open-alert').on('click', () => {
E.alert('Alert dialog content', { title: 'Notice' });
});
$(root).find('#audit-open-confirm').on('click', () => {
E.confirm('Are you sure you want to proceed with this action?', { title: 'Confirm action' });
});
$(root).find('#audit-open-prompt').on('click', () => {
E.prompt('Enter your name:', { title: 'Identify yourself', inputValue: 'Darryl' });
});
$(root).find('#audit-toast-success').on('click', () => {
E.toast('Operation completed successfully', { type: 'success' });
});
$(root).find('#audit-toast-error').on('click', () => {
E.toast('Something went wrong - please try again', { type: 'error' });
});
}Inline modal (forced visible for inspection)
Confirm payment
Are you sure you want to process the payment of Β£4,250.00 to Acme Ltd?
A receipt will be emailed to the registered billing address.
<div class="modal-header"><h5 class="modal-title">Confirm payment</h5></div>
<div class="modal-body">
<p>Are you sure you want to process the payment of <strong>Β£4,250.00</strong> to <strong>Acme Ltd</strong>?</p>
<p class="text-secondary">A receipt will be emailed to the registered billing address.</p>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary">Cancel</button>
<button type="button" class="btn btn-primary">Confirm payment</button>
</div>15. Slideovers
<div class="audit-row"><button type="button" class="btn btn-primary" id="slideover-left">From left</button>
<button type="button" class="btn btn-primary" id="slideover-right">From right</button>
<button type="button" class="btn btn-primary" id="slideover-top">From top</button>
<button type="button" class="btn btn-primary" id="slideover-bottom">From bottom</button>
</div>/**
* 15. Slideovers - from public/showcase/themes.
*/
export function mount(root) {
function openSlideover(position) {
const id = 'audit-slideover-' + position;
let host = $('#' + id)[0];
if (!host) {
host = document.createElement('div');
host.id = id;
document.body.appendChild(host);
}
E.slideover('#' + id, {
position,
title: 'Slideover - ' + position,
content: 'This is the slideover panel. Inspect heading colour, body text and the close-button contrast.',
backdrop: true,
backdropClose: true
}).open();
}
$(root).find('#slideover-left').on('click', () => openSlideover('left'));
$(root).find('#slideover-right').on('click', () => openSlideover('right'));
$(root).find('#slideover-top').on('click', () => openSlideover('top'));
$(root).find('#slideover-bottom').on('click', () => openSlideover('bottom'));
}16. Autocomplete & Pillbox
<div class="row"><div class="col-6">
<div class="audit-label">Autocomplete</div>
<input id="audit-autocomplete" class="form-input" placeholder="Type a countryβ¦">
</div>
<div class="col-6">
<div class="audit-label">Pillbox (multi-select)</div>
<input type="text" id="audit-pillbox" class="form-input" placeholder="Add tagsβ¦">
</div>
</div>/**
* 16. Autocomplete & Pillbox - from public/showcase/themes.
*/
export function mount(root) {
E.autocomplete(root.querySelector('#audit-autocomplete'), {
data: ['United Kingdom', 'United States', 'France', 'Germany', 'Spain', 'Italy', 'Japan', 'Australia', 'Brazil', 'Canada', 'Norway', 'Sweden', 'Denmark', 'Finland', 'Netherlands'],
minChars: 1,
maxResults: 8
});
E.pillbox(root.querySelector('#audit-pillbox'), {
data: ['JavaScript', 'TypeScript', 'CSS', 'HTML', 'Domma', 'Node.js', 'PHP', 'Python', 'Rust', 'Go'],
value: ['JavaScript', 'Domma'],
creatable: true,
placeholder: 'Add a tagβ¦'
});
}17. Progression - Timeline & Roadmap
Timeline mode
<div id="audit-timeline"></div>/**
* 17. Progression - Timeline & Roadmap - from public/showcase/themes.
*/
export function mount(root) {
E.progression(root.querySelector('#audit-timeline'), {
mode: 'timeline',
items: [
{ year: 2023, title: 'Founded', description: 'Company established and first round of seed funding raised.' },
{ year: 2024, title: 'Series A', description: 'Closed a Β£10m Series A led by Acme Ventures.' },
{ year: 2025, title: 'Expansion', description: 'Opened offices in Berlin, Paris, and Amsterdam.' },
{ year: 2026, title: 'Public release', description: 'Domma 1.0 ships to general availability.' }
]
});
}Roadmap mode (status colours)
<div id="audit-roadmap"></div>/**
* 17. Progression - Timeline & Roadmap - from public/showcase/themes.
*/
export function mount(root) {
E.progression(root.querySelector('#audit-roadmap'), {
mode: 'roadmap',
items: [
{ id: 1, title: 'API v2', status: 'completed', description: 'New REST endpoints with pagination.' },
{ id: 2, title: 'Mobile app', status: 'in-progress', description: 'iOS and Android clients in beta.' },
{ id: 3, title: 'Analytics', status: 'planned', description: 'Self-service reporting dashboard.' },
{ id: 4, title: 'Compliance', status: 'blocked', description: 'Awaiting legal review.' }
]
});
}18. Carousel
Slide one - primary background
Caption text rendered against the primary brand colour.
Slide two - success background
Same caption pattern using success colour.
Slide three - warning background
Common contrast hot-spot in light themes.
<div id="audit-carousel" class="carousel" style="max-width: 600px; margin: 0 auto;">
<div class="carousel-inner">
<div class="carousel-item active">
<div style="background: var(--dm-primary); color: var(--dm-primary-text, #fff); padding: 3rem 1rem; text-align:center; border-radius:8px;">
<h3 style="color: inherit; margin: 0;">Slide one - primary background</h3>
<p style="color: inherit; margin: 0.5rem 0 0;">Caption text rendered against the primary brand colour.</p>
</div>
</div>
<div class="carousel-item">
<div style="background: var(--dm-success); color: var(--dm-success-text, #fff); padding: 3rem 1rem; text-align:center; border-radius:8px;">
<h3 style="color: inherit; margin: 0;">Slide two - success background</h3>
<p style="color: inherit; margin: 0.5rem 0 0;">Same caption pattern using success colour.</p>
</div>
</div>
<div class="carousel-item">
<div style="background: var(--dm-warning); color: var(--dm-warning-text, #fff); padding: 3rem 1rem; text-align:center; border-radius:8px;">
<h3 style="color: inherit; margin: 0;">Slide three - warning background</h3>
<p style="color: inherit; margin: 0.5rem 0 0;">Common contrast hot-spot in light themes.</p>
</div>
</div>
</div>
</div>/**
* 18. Carousel - from public/showcase/themes.
*/
export function mount(root) {
E.carousel(root.querySelector('#audit-carousel'), { autoplay: true, interval: 5000, pauseOnHover: true });
}19. Code & quotes
Inline code: const x = Domma.theme.set('mint-light') should remain readable.
function audit() {
const issues = [];
document.querySelectorAll('*').forEach(el => {
if (failsContrast(el)) issues.push(el);
});
return issues;
}
Usage
CSS Only
JavaScript API
<div class="themes-component-grid"><div class="themes-preview-card">
<h3>CSS Only</h3>
<div class="themes-code-block language-html">
<span class="comment"><!-- Include themes CSS --></span>
<<span class="keyword">link</span> <span class="function">rel</span>=<span class="string">"stylesheet"</span> <span class="function">href</span>=<span class="string">"domma-themes.css"</span>>
<span class="comment"><!-- Apply via class --></span>
<<span class="keyword">body</span> <span class="function">class</span>=<span class="string">"dm-theme-grayve-dark"</span>>
</div>
</div>
<div class="themes-preview-card">
<h3>JavaScript API</h3>
<div class="themes-code-block language-javascript">
<span class="comment">// Initialise with a theme</span>
Domma.theme.<span class="function">init</span>({
theme: <span class="string">'grayve-dark'</span>,
autoDetect: <span class="keyword">true</span>
});
<span class="comment">// Switch at runtime</span>
Domma.theme.<span class="function">set</span>(<span class="string">'ocean-light'</span>);
<span class="comment">// Query current state</span>
Domma.theme.<span class="function">get</span>(); <span class="comment">// 'grayve-dark'</span>
Domma.theme.<span class="function">getBase</span>(); <span class="comment">// 'grayve'</span>
Domma.theme.<span class="function">isDark</span>(); <span class="comment">// true</span>
</div>
</div>
</div>