@dennisl0731/oh-my-ui
v0.4.0
Published
Shared CSS + Tailwind preset + lucide icons (React + vanilla SVG sprite) + nav/toast/copy helpers for the oh-my-* family. Warm-paper aesthetic, earthy-brown accents, per-service colorways.
Maintainers
Readme
@dennisl0731/oh-my-ui
Shared CSS + Tailwind preset for the oh-my-* family.
This package is intentionally a design foundation, not a locked UI kit. It gives you:
- design tokens via CSS variables
- a Tailwind preset wired to those tokens
- a small nav/theme bootstrap script that enhances existing markup
- Lucide icon re-exports (React) and a vanilla
icons.svgsprite for server-rendered apps - JS helpers for toast + clipboard copy (works on plain HTTP)
It does not force page structure, copy, layout, or product-specific components.
What's new in 0.4.0
- Apps menu + host-derived cross-app links in
nav.js. Supply a registry viawindow.__OMI_APPS__ordata-apps="<url.json>";.omi-appsdropis filled with links built from the current host (window.omiOrigin(sub)), so they survive reverse-proxy base/port changes..omi-naventries now acceptsubtoo. No project data baked into the package. See the Apps menu section.
What's new in 0.2.4
- New CSS primitives promoted from the oh-my-* apps (were re-implemented in
several app
<style>blocks):.omi-switch— toggle switch (.smvariant). Wrap a checkbox +.track..omi-meter— progress / capacity bar (.lg/.smsizes;.fill.warn,.fill.full;.indeterminate)..omi-details— collapsible disclosure (<summary>+.omi-chevron+.omi-summary-note; body in.omi-details-body)..omi-mini-player— fixed bottom media bar (.mp-ico/.mp-title/.mp-audio)..omi-log-console— floating streaming-log panel (.lc-head/.lc-chiprunning|done|error/.lc-body/.lc-linestep|ok|warn|err)..omi-usage-tip— provider-status popover (.dot.ok/.bad,.nm)..omi-hinttext + pill variants.omi-pill.info,.omi-pill.accent,.omi-pill.lg.
- Gallery: new
controlspage inexamples/visual/showcasing the above.
What's new in 0.2.3
- JS helpers in
nav.js:window.omiToast(...),window.omiCopy(text), and a declarative<button data-omi-copy="…">auto-wire (with HTTPS-fallback so copy works on plain-HTTP dev servers). icons.svg— vanilla SVG<symbol>sprite for non-React consumers (Flask, Jinja, any server-rendered app). Reference via<svg><use href="…/icons.svg#i-clipboard"/></svg>.- New CSS primitives:
.omi-dropzone,.omi-asset-card(+.omi-asset-grid),.omi-ai-badge,.omi-theme-picker.compact,.omi-scroll-clean. - Press feedback:
.omi-btn.pressedanimation +.confirmed::afterfloating "✓ copied" chip — automatically applied bydata-omi-copy. - Behavior change:
.omi-panel-bodyno longer auto-scrolls. Short static panels in flex column layouts no longer get surprise scrollbars. Add the.scrollmodifier on dashboard panels that genuinely need overflow.
Install
npm install @dennisl0731/oh-my-uiConsumer app peer deps:
npm install tailwindcss reactlucide-react and the common Tailwind plugins used by the preset are bundled through this package.
What You Get
1. CSS tokens and shared primitives
Import once:
@import '@dennisl0731/oh-my-ui/omi.css';
@tailwind components;
@tailwind utilities;Skip @tailwind base. omi.css already ships its own base/reset styles.
The stylesheet includes:
- warm layered surfaces
- shared accent/border/text tokens
- button, input, panel, dialog, card, pill, log, markdown, and editor primitives
- stat cards, alerts, toolbars, tables, skeletons, menu/popover/drawer/toast shells
- code and document block wrappers
- service-specific accent overrides such as
body.service-imageandbody.service-slide
Default service accent is the earthy tan palette. body.service-sage restores the older sage accent.
2. Built-in themes
Theme switching is driven by data-omi-theme on <html>.
Built-in themes:
paperforestoceansunsetgrapegraphiterosedark— dark, GitHub-stylelight— light, GitHub-stylecatppuccin— dark, Catppuccin Mochanord— dark, Nord
Example:
<html data-omi-theme="ocean">These themes change the global surface/text/accent tokens. Service classes like service-image still work on top by only shifting the accent family.
The seven warm-paper themes (paper…rose) are light. The four ported
themes add dark colorways (dark, catppuccin, nord) plus a neutral
light. Dark themes re-skin the bundled components by overriding the
neutral surface channels (--omi-surface, --omi-surface-2,
--omi-surface-3, --omi-shadow-rgb, --omi-scrim-rgb); small semantic
chips (status pills, thinking/tool blocks) keep their accent tints.
3. Tailwind preset
Use the preset in tailwind.config.js:
import omiPreset from '@dennisl0731/oh-my-ui/tailwind-preset';
/** @type {import('tailwindcss').Config} */
export default {
presets: [omiPreset],
content: ['./index.html', './src/**/*.{js,ts,jsx,tsx}'],
};The preset adds:
- token-backed colors such as
bg-surface-1,text-fg,bg-accent,border-edge - token-backed fonts and radii
shadow-softandshadow-lift- centered container defaults
prose-omitypography theme@tailwindcss/formswithstrategy: 'class'@tailwindcss/typography@tailwindcss/aspect-ratio@tailwindcss/container-queries
This is the closest thing to an "industrial standard" baseline for app work: forms, prose, media ratio handling, responsive container logic, and tokenized colors all work out of the box.
4. Icons
React consumers:
import { Check, Plus, Trash2 } from '@dennisl0731/oh-my-ui/icons';Everything from lucide-react is re-exported unchanged.
Vanilla / server-rendered consumers:
The package ships an SVG <symbol> sprite at icons.svg. Inline it once
per page (or serve it as a static file and reference via <use>):
{# Jinja partial: inline once #}
{% include '_icons.html' %} <!-- or copy icons.svg verbatim -->
<svg class="icon" aria-hidden="true">
<use href="#i-clipboard"/>
</svg>Sprite IDs are i-<lucide-icon-name> — e.g. i-clipboard, i-image,
i-pen-line, i-settings, i-refresh-cw, i-trash-2, i-sparkles,
i-bug, i-check, i-x, i-chevron-right/down/left/up, i-arrow-left/right,
i-search, i-eye, i-copy, i-file, i-folder, i-download, i-upload,
i-presentation, i-brain, i-info, i-alert-circle, i-check-circle,
i-x-circle, i-more-horizontal, i-menu, i-sun, i-moon, i-plus,
i-edit, i-scan-text.
Recommended icon CSS:
.icon { width: 1em; height: 1em; vertical-align: -0.15em; flex-shrink: 0; display: inline-block; }Icons use stroke="currentColor" so they inherit the surrounding text color.
5. JS helpers (nav.js)
nav.js exposes three globals:
// Toast — uses .omi-toast / .omi-toast-stack from omi.css. Creates the
// stack container on first call if not already in the DOM.
window.omiToast({ title: 'Saved', body: 'Theme updated.', kind: 'success' });
window.omiToast('Quick title', 'optional body', 'error'); // legacy positional
// Clipboard write with HTTPS-context fallback. Always returns Promise<boolean>.
const ok = await window.omiCopy('text to copy');
// Re-scan the DOM for [data-omi-copy] after dynamic inserts.
window.omiWireCopy(rootElement?);The declarative form auto-wires on initial page load:
<button data-omi-copy="ssh user@host"
data-omi-copy-label="ssh command">
copy
</button>On click: press animation → clipboard write (with fallback) → ✓ floater on
success → toast (title: 'Copied' or 'Copy failed', body = the label).
Buttons that already opt out via data-omi-copy-wired are skipped on rescan,
so calling omiWireCopy() repeatedly is safe.
Theme Switching
nav.js no longer injects product UI. It only enhances markup you already render.
Minimal example
<div class="omi-topnav">
<div class="omi-brand">oh-my-ui</div>
<nav class="omi-nav"></nav>
<label class="omi-theme-picker">
<span>Theme</span>
<select data-omi-theme-switch></select>
</label>
</div>
<script>
window.__OMI_NAV__ = [
{ id: 'image', label: 'image', href: 'http://localhost:5006/' },
{ id: 'slide', label: 'slide', href: 'http://localhost:5008/' },
{ id: 'skill', label: 'skill', href: 'http://localhost:5009/' }
];
window.__OMI_THEME__ = {
defaultTheme: 'paper'
};
</script>
<script src="/node_modules/@dennisl0731/oh-my-ui/js/nav.js" data-service="image"></script>Behavior
- if
window.__OMI_NAV__exists,.omi-navwill be filled from that config - if a
select[data-omi-theme-switch]exists, it will be wired to theme state - if the select has no options, built-in themes are injected automatically
- selected theme is persisted to
localStorageunderomi-theme - theme changes emit
documenteventomi:themechange
Optional theme config
<script>
window.__OMI_THEME__ = {
defaultTheme: 'forest',
storageKey: 'my-app-theme',
themes: [
{ id: 'paper', label: 'Paper' },
{ id: 'forest', label: 'Forest' },
{ id: 'ocean', label: 'Ocean' }
]
};
</script>Apps menu (.omi-appsdrop) — host-derived cross-app links
The package ships no app list (that's project data). Supply your own and
nav.js fills any .omi-appsdrop container. Two ways:
<!-- inline -->
<script>
window.__OMI_APPS__ = [
{ id: 'clipboard', sub: 'clipboard', path: '/', label: 'clipboard', icon: 'clipboard' },
{ id: 'podcast', sub: 'rss', path: '/podcast', label: 'podcast', icon: '<svg …>' }
];
</script>
<script src="…/nav.js" data-service="clipboard"></script>
<!-- or a settings file, fetched once -->
<script src="…/nav.js" data-service="clipboard" data-apps="/omi/apps.json"></script>Each entry: {id, sub, path, label, icon}. icon is either a sprite id
(clipboard → icons.svg#i-clipboard) or a raw <svg …> string. No list →
the container is left untouched (so hardcoded markup still works).
Hrefs are built by window.omiOrigin(sub), which swaps only the leading host
label and keeps the current scheme/apex/port — so links keep working when the
reverse proxy changes the base domain or port. .omi-nav entries accept the
same sub (+ optional path) instead of a hardcoded href/port. Bare-IP /
localhost / single-label hosts fall back to same-origin.
Tailwind Utilities
Common aliases:
| Class | Token |
| --- | --- |
| bg-surface-{1,2,3} | --tw-bg-{1,2,3} |
| text-fg | --tw-fg |
| text-fg-soft | --tw-fg-soft |
| text-muted | --tw-muted |
| bg-accent, bg-accent-deep | --tw-accent* |
| border-edge, border-edge2 | --omi-border* |
| font-sans, font-heading, font-mono | --omi-font-* |
| rounded, rounded-lg, rounded-full | --omi-radius* |
Opacity modifiers work:
<div class="bg-fg/5 text-fg border border-edge"></div>Recommended Next Components
These primitives are now available as styling building blocks:
- tables:
.omi-table-wrap,.omi-table,.omi-table-actions,.omi-table-empty - alerts:
.omi-alert.info|success|warn|error - stat cards:
.omi-stat-grid,.omi-stat-card,.omi-stat-value,.omi-stat-delta - toolbars:
.omi-toolbar,.omi-toolbar-group,.omi-segmented - list rows:
.omi-list,.omi-list-row,.omi-list-row-meta,.omi-list-row-actions - loading states:
.omi-skeleton.line|title|block - overlays:
.omi-menu,.omi-popover,.omi-drawer,.omi-toast-stack,.omi-toast - code/docs:
.omi-codeblock,.omi-doc-block - ingestion:
.omi-dropzone(0.2.3) - gallery:
.omi-asset-grid,.omi-asset-card(+.omi-asset-card-thumb|meta|name|submeta|actions|check) (0.2.3) - status:
.omi-ai-badge.ok|bad(0.2.3) - chrome:
.omi-theme-picker.compact,.omi-scroll-clean(0.2.3) - feedback animations:
.omi-btn.pressed,.omi-btn.confirmed(0.2.3, auto-applied bydata-omi-copy)
They are intentionally presentational. Behavior such as keyboard navigation, focus management, escape handling, portal mounting, and toast lifecycles should stay in the consumer app or a later headless helper layer.
Primitive Examples
Table
<div class="omi-table-wrap">
<table class="omi-table">
<thead>
<tr>
<th>Name</th>
<th>Status</th>
<th></th>
</tr>
</thead>
<tbody>
<tr>
<td>Experiment A</td>
<td><span class="omi-pill ok">Ready</span></td>
<td>
<div class="omi-table-actions">
<button class="omi-btn secondary sm">Open</button>
</div>
</td>
</tr>
</tbody>
</table>
</div>Alert
<div class="omi-alert warn">
<div class="omi-alert-body">
<strong>Review needed</strong>
Config changed and requires confirmation before publish.
</div>
</div>Toolbar
<div class="omi-toolbar">
<div class="omi-toolbar-group">
<input class="omi-input" placeholder="Search">
<div class="omi-segmented">
<button class="active">All</button>
<button>Open</button>
<button>Closed</button>
</div>
</div>
<div class="omi-toolbar-spacer"></div>
<div class="omi-toolbar-group">
<button class="omi-btn secondary">Filter</button>
<button class="omi-btn primary">New</button>
</div>
</div>Stat Cards
<div class="omi-stat-grid">
<div class="omi-stat-card">
<div class="omi-stat-label">Success Rate</div>
<div class="omi-stat-value">93.2%</div>
<div class="omi-stat-meta">
<span class="omi-stat-delta up">+4.1%</span>
vs last week
</div>
</div>
</div>Skeleton
<div class="omi-card">
<div class="omi-card-body">
<div class="omi-skeleton title"></div>
<div style="height:10px"></div>
<div class="omi-skeleton line"></div>
<div style="height:8px"></div>
<div class="omi-skeleton line"></div>
</div>
</div>Drawer and Toast
<aside class="omi-drawer open">
<div class="omi-drawer-header">
<div class="omi-drawer-title">Details</div>
</div>
<div class="omi-drawer-body">...</div>
</aside>
<div class="omi-drawer-backdrop"></div>
<div class="omi-toast-stack">
<div class="omi-toast success">
<div class="omi-toast-title">Saved</div>
<div class="omi-toast-body">Theme settings were updated.</div>
</div>
</div>Toasts are usually spawned from JS — see window.omiToast(...) above.
Drop zone (.omi-dropzone)
Paste / drag-drop / click-to-pick file ingestion:
<div class="omi-dropzone" id="dz" tabindex="0" role="button">
<span class="omi-dropzone-icon">
<svg class="icon"><use href="#i-clipboard"/></svg>
</span>
<div class="omi-dropzone-title">
Paste an image with <kbd>Ctrl</kbd>+<kbd>V</kbd>
</div>
<div class="omi-dropzone-hint">
drag & drop a file<span class="sep">·</span>or click
</div>
<input type="file" hidden>
</div>The app owns the JS — wire paste / dragover / drop / click handlers
and toggle the .active class on dragover for the highlight pulse. Idle
state has a subtle lifted shadow; hover gets an accent ring; active gets
solid border + accent fill.
Asset card (.omi-asset-card + .omi-asset-grid)
Tile-based gallery for screenshots, images, or any thumbnailable asset:
<div class="omi-asset-grid">
<div class="omi-asset-card">
<img class="omi-asset-card-thumb" src="/thumb/1" alt="">
<div class="omi-asset-card-meta">
<div class="omi-asset-card-name">screenshot_2026-06-02.png</div>
<div class="omi-asset-card-submeta">
<span>77 KB</span>
<span class="sep">·</span>
<span>screenshots</span>
</div>
</div>
<div class="omi-asset-card-actions">
<button class="omi-icon-btn" data-omi-copy="/path/to/file"
title="Copy path">
<svg class="icon"><use href="#i-copy"/></svg>
</button>
<button class="omi-icon-btn" title="Edit">
<svg class="icon"><use href="#i-pen-line"/></svg>
</button>
<button class="omi-icon-btn danger" title="Delete">
<svg class="icon"><use href="#i-trash-2"/></svg>
</button>
</div>
<div class="omi-asset-card-check">
<svg class="icon"><use href="#i-check"/></svg>
</div>
</div>
</div>Apply .selected to a card to mark it; apply .selecting to the grid to
always-show the check overlay during multi-select. Without those, the check
appears only on hover.
AI status badge (.omi-ai-badge)
<span class="omi-ai-badge ok" title="Claude AI available"></span>
<span class="omi-ai-badge bad" title="AI not configured"></span>
<span class="omi-ai-badge" title="AI status unknown"></span>Pure CSS: the dot is a ::before pseudo-element. Toggle the .ok / .bad
class from JS based on your AI/service health check.
Compact theme picker (.omi-theme-picker.compact)
<label class="omi-theme-picker compact" title="Choose theme">
<span>theme</span> <!-- hidden in compact mode; kept for a11y -->
<select data-omi-theme-switch></select>
</label>The compact modifier hides the label, drops the background/padding, and
gives the <select> a borderless-pill style that fits cleanly in a top nav.
Node graphs (.omi-flow — React Flow + elkjs)
A theme-only, opt-in restyle of @xyflow/react
to the warm-paper palette. No JS is bundled — install the libs in your app:
npm install @xyflow/react elkjsimport '@xyflow/react/dist/style.css'; // 1. React Flow base styles
import '@dennisl0731/oh-my-ui/omi.css'; // 2. then this theme
<ReactFlow className="omi-flow" nodes={nodes} edges={edges}>
<Background />
<Controls />
<MiniMap />
</ReactFlow>Adding className="omi-flow" themes nodes, edges, handles, controls,
minimap, and the background pattern — mostly via React Flow's own
--xy-* CSS variables, so per-service colorways (data-omi-theme) and
dark mode flow through automatically.
elkjs only computes node x/y positions, so it needs no styling. Three
optional accent classes are available for nodes returned from an ELK
layout (set on a node's className): omi-flow__node--root,
omi-flow__node--accent, omi-flow__node--muted.
Design Direction Ideas
The package can support more than one visual attitude while keeping the same API:
- editorial workspace: denser typography, stronger document styling, better table and footnote treatments
- builder console: more structured panels, status colors, logs, inspectors, split panes
- gallery mode: richer cards, media-first layouts, asset metadata chips
- research notebook: citations, callouts, figure wrappers, code-result pair layouts
That is the right level for this repo: shared language plus shared primitives, not fixed page compositions.
CSS-only Usage
<link rel="stylesheet" href="/node_modules/@dennisl0731/oh-my-ui/css/omi.css">
<script src="/node_modules/@dennisl0731/oh-my-ui/js/nav.js"></script>If you only want theme behavior, render your own select with data-omi-theme-switch.
Testing
This repo now includes a minimal Playwright visual regression harness.
Files:
examples/visual/index.html: static preview shellexamples/visual/preview.js: deterministic scenes for each primitive grouptests/tokens.spec.js: CSS token contract checkstests/visual.spec.js: screenshot regression checksplaywright.config.js: local static server + browser config
Install test deps:
npm install
npx playwright installRun token checks:
npm run test:tokensGenerate or refresh screenshots:
npm run test:update-snapshotsRun visual regression against committed baselines:
npm run test:visualRecommended workflow:
- add or change a primitive in
omi.css - update the matching scene in
examples/visual/preview.js - regenerate snapshots intentionally
- review image diffs before committing
- commit snapshot baselines together with the style change
The important rule is that screenshot baselines are part of the contract. If a visual change is intended, update them in the same PR. If the change is not intended, the diff should fail review.
Publishing
npm publish --access public