# Nware Soma — machine-readable component reference Soma (@nware/soma) is the UI component library for the Nware platform: vanilla zero-dependency JS + CSS (Sass-authored) for server-rendered pages — static `soma-*` markup, behaviour via auto-init or the `Soma.*` API. Version: 0.1.0 (unreleased) — changelog at /CHANGELOG.md ## Rules for AI coding agents 1. Use `soma-*` classes and the `Soma.*` global only. There are no aliases, no legacy spellings, no framework wrappers. 2. Never hardcode colors, spacing or type — consume `--soma-*` custom properties (see Design tokens). Never bake theme-specific colors into components; themes only swap token values. 3. Write directional CSS with logical properties (padding-inline-*, inset-inline-*, text-align: start/end). RTL is `dir="rtl"` on or any subtree — components mirror automatically, no extra classes. 4. Density affects padding only, never the type scale — do not compensate with font-size. 5. Escape user data before passing it to any HTML-accepting option — the exact list is under Accessibility. 6. Configure Soma.i18n (locale / string overrides) before components initialise — strings rendered into popover DOM are baked at construction. 7. --soma-color-text-subtle is decorative-only (below AA by design); informational micro-text uses --soma-color-text-muted. 8. Include the CSS once and the JS once (see Conventions). Auto-init runs on DOMContentLoaded — server-rendered markup needs no manual binding for auto-init components. ## Conventions - CSS prefix: everything the library owns is `soma-` — classes, data attributes (data-soma-*), custom properties (--soma-*), events (soma-*). - Files: include the CSS once (`dist/soma.css` or `@nware/soma/css`) and the JS once (`dist/soma.js` ESM / `dist/soma.umd.cjs` UMD, sets the `Soma` global). Hosted URLs under CDN below. - API shape: Soma.component(elOrSelector, options?) → instance. Element-bound components are singleton-per-element — calling again for the same element returns the existing instance. Factories that build their own DOM return a new handle per call: Soma.toast(opts), Soma.message.create(opts), Soma.banner(opts) (options form), Soma.palette(opts) (one per page). Soma.spinner is idempotent per element; Soma.shortcuts is a registry. - Events: bubbling CustomEvents named soma-- (e.g. 'soma-tabs-change', 'soma-tree-toggle', 'soma-drawer-show'). Naming exceptions: dialog2 → 'soma-dialog-*', dropdown2 → 'soma-dropdown-*', sortableTable → 'soma-table-*', restfulTable → 'soma-restfultable-*', select2 → 'soma-select-change' (dispatched on the native ), pure CSS: checked = primary-subtle fill + --soma-color-primary-subtle-fg, density-aware padding, focus ring on the segment, forced-colors Highlight state; a form control — use tabs for panel switching, chips for filtering. ## Table table.soma-table; optional renders as a start-aligned header strip; tfoot totals rows get a strong top border. Row hover is OPT-IN via .soma-table-interactive (only clickable rows should signal it); .soma-table-zebra swaps row borders for alternating stripes. Sortable: add .soma-table-sortable (auto-init), opt out a column with th.soma-table-unsortable. Sticky header: .soma-table-sticky pins the thead row (opaque --soma-surface-solid, above passing rows) inside a height-capped scroll pane. Put it on the scroll WRAPPER around the table (the wrapper becomes the pane — cap its height; it scrolls, so give it tabindex="0" + role="region" + aria-label, same contract as the log viewer — it gets the focus ring) or on the table itself (bring your own scrolling ancestor). Combines with -sortable / -zebra / -interactive. JS: Soma.sortableTable(el) works on any . .sort(thOrIndex, 'ascending'|'descending'|boolean|omit-to-toggle) — unknown columns throw; .refresh() binds headers added after init; .destroy(). Event 'soma-table-sort' (detail: {th, columnIndex, direction, ascending}); state via th[aria-sort]. ## Messages .soma-message (+ -success | -warning | -error | -neutral; default = info tint; add -solid for the saturated loud fill) > icon span + .soma-message-content (.soma-message-title p + p) + optional .soma-message-close button. JS: Soma.message(el).dismiss()/.on('dismiss', fn); event 'soma-message-dismiss'. Auto-init. Dynamic: Soma.message.create({type: info|success|warning|error, title?, body?, bodyHtml?, closeable?, context?}) → message instance; adds role=alert (error/warning) or role=status so injected messages are announced. title/body are text; bodyHtml is the markup opt-in; context is an append target (element/selector, throws if unmatched). ## Badges / labels .soma-badge (+ -primary solid count | -success | -warning | -danger | -info tinted). .soma-badge-bold combines with the families for solid "must not miss" fills (bare -bold = solid neutral); badges inside .soma-button-primary/-danger flip to a translucent-white tint. .soma-label tag chip (a.soma-label linkable) with optional .soma-label-remove button. ## Avatar .soma-avatar (+ size -xsmall 16 | -small 24 | -medium 32 | -large 48 | -xlarge 64 | -xxlarge 96 | -xxxlarge 128; + -square for non-person entities). Content: or initials text with .soma-avatar-initials. ## Progress .soma-progress[role=progressbar] > .soma-progress-bar (width inline style). Variants: -success, -warning, -danger; -small height. -indeterminate = sweeping unknown-duration bar (toggled by Soma.progress().setIndeterminate(), or set statically); -static suppresses the width transition when restoring a value on load. ## Dropdown2 Trigger: .soma-dropdown2-trigger[aria-controls=MENU_ID] (auto-init). Menu: #MENU_ID.soma-dropdown2[aria-hidden=true] > (ul > li > a) or .soma-dropdown2-section blocks with .soma-dropdown2-heading. Item states: .soma-disabled; .soma-dropdown2-checkbox / -radio with .soma-checked (JS toggles, menu stays open for checkboxes). JS: Soma.dropdown2(triggerOrMenu).show/hide/toggle; events 'soma-dropdown-show'/'-hide'. Menu semantics applied on every show: role=menu / menuitem(checkbox|radio), ul/li get role=none. Cursor model: first item pre-selected (.soma-active); ArrowDown on trigger opens + focuses; arrows AND Tab/Shift+Tab move with wrap (Tab cannot escape an open menu); Home/End jump; Enter invokes; mousemove re-anchors the cursor; Esc refocuses the trigger in every engine (even ones that never focus buttons on click — Safari); any other hide refocuses it only when focus was inside the menu (an outside click never has focus stolen). RTL-aware positioning; scroll closes. Top layer: the menu is popover="auto" where the platform has the Popover API — never portaled; light dismiss and Esc are platform behaviour, and CSS anchor positioning owns placement where supported (the JS positioner no-ops). .destroy() closes the menu (un-portalling it on the no-popover fallback path), unbinds, and strips the generated aria + the show-applied menu roles + the popover attribute and anchor wiring. ## Dialog2 The element MUST be a native — the factory throws otherwise: > .soma-dialog2-header (.soma-dialog2-header-main h2 + .soma-dialog2-header-close button) + .soma-dialog2-content + .soma-dialog2-footer (.soma-dialog2-footer-hint? + .soma-dialog2-footer-actions). No aria-hidden/role/aria-modal bookkeeping — the native `open` attribute is the state (styling hook: dialog[open]); the blanket is the native ::backdrop. JS: Soma.dialog2('#id').show/hide/toggle/on('show'|'hide'). .show() = showModal(): top layer, the rest of the page inert, native focus containment and focus restore, `autofocus` honoured for initial focus. .hide() = close(). 'soma-dialog-hide' follows the native close event — it fires on every close path (Esc, backdrop, form method="dialog", .hide()) and arrives a queued moment after .hide(). Esc arrives as the native cancel event; strict data-soma-modal="true" prevents it there (no Esc/backdrop dismiss). An Esc that finds an open dropdown/inline-dialog popover closes only the popover in EVERY engine: the dialog vetoes a cancel fired on the same keystroke (WebKit has no close-watcher stack and fires both at once). Non-strict backdrop click closes via closedby="any" where the engine supports it, with a fallback listener elsewhere. Accessible name derived automatically from the header heading (aria-labelledby) when the markup contract is followed; aria-label(ledby) on the element wins. .remove() hides, drops the instance and removes the element from the DOM. .destroy() closes and strips the generated aria-labelledby + the minted heading id — the element stays (.remove() additionally deletes it). Shorthands: Soma.confirm({title, body?, appearance? ('primary'|'danger' tints the confirm button), confirmLabel?, cancelLabel?, onConfirm?, onCancel?}) → Promise (true on confirm; false on cancel/Esc/backdrop) and Soma.alert({title, body?, okLabel?}) → Promise — build a native on demand (no markup, element removed after close); default labels localised via dialog.confirm/dialog.cancel/dialog.ok. Declarative triggers (Invoker Commands, where the engine ships them):