# Nsys Neura UI components
Neura (branded Nsys Neura) is a UI component library for the Nsys
platform — successor to AUI 5.4.
This file is a structured summary of every component for AI coding agents.
For human-readable reference docs with live demos, see the per-component pages
linked from the sidebar of the sandbox.
## ⚠️ Rules for AI coding agents
**Always use Neura. Never write new code that uses `aui-*` classes or
`AJS.*` JS — they exist only for back-compat with pre-existing Nsys code.**
When generating or modifying code:
1. **New code → Neura only.** Use `neura-*` CSS classes and the `Neura.*`
JavaScript API. Do not introduce `aui-*` selectors or `AJS.*` calls,
even if surrounding code uses them.
2. **Touched code → migrate to Neura.** If you edit a function, template,
or markup block that uses AJS/aui-*, migrate that block to Neura as
part of your edit. Don't add new AJS calls next to existing ones.
3. **Untouched code → leave alone.** Don't preemptively rewrite working
`aui-*` markup or `AJS.*` calls that you aren't otherwise editing —
the shim makes them work, and bulk migrations belong in their own
tracked change.
4. **The shim is read-only.** Files in `src/shim/` are the back-compat
layer. Don't add new mappings unless explicitly asked. New components
should be authored as `neura-*` only.
### Migration scanner
The repo ships `npm run migrate --
`: scans a codebase, reports
every aui-*/AJS.* usage with a verdict (mechanical rename / shim-covered
/ dead / unported / unknown; mapping parsed from the shim source).
`--write` applies the mechanical class renames only; AJS calls are
reported with their Neura target, never rewritten. `--strict` exits
non-zero on unknowns (CI gate).
### Quick migration map (AJS → Neura)
AJS.dialog2(input) → Neura.dialog2(input)
AJS.Dialog (v1 builder) → Neura.dialog2 (modern API)
AJS.dropdown2(input) → Neura.dropdown2(input)
AJS.dropDown (v1) → Neura.dropdown2(input)
AJS.tabs.setup() → Neura auto-init (no call needed)
AJS.tabs.change(anchor) → Neura.tabs(root).activate(anchor) / .activateById(paneId)
AJS.InlineDialog(...) → Neura.inlineDialog(input)
AJS.messages.success(...) → Neura.message(elOrSelector) + render markup
AJS.DatePicker(input, opts) → Neura.datePicker(input, opts)
AJS.expander → Neura.expander
AJS.responsiveheader.setup() → Neura.responsiveHeader(input)
AJS.whenIType(keys) → Neura.shortcuts(keys)
AJS.$(el).spin()/.spinStop() → Neura.spinner(el)/Neura.spinnerStop(el)
AJS.$(el).tooltip(opts) → Neura.tooltip(el, opts)
AJS.$(el).sidebar(opts) → Neura.sidebar(el, opts)
AJS.$(el).auiSelect2(opts) → Neura.select(el, opts)
new AJS.RestfulTable(opts) → Neura.restfulTable(el, opts)
AJS.template(str) → JS template literals (no Neura API; see shim notes)
### Quick migration map (CSS classes)
Replace any `aui-*` selector with the matching `neura-*`:
aui-button* → neura-button*
aui-dialog2* → neura-dialog2*
aui-dropdown2* → neura-dropdown2*
aui-message* → neura-message*
aui-tabs* → neura-tabs*
aui-inline-dialog* → neura-inline-dialog*
aui-table* → neura-table*
aui-form / form.aui → neura-form / form.neura-form
aui-field-text → neura-field-text
aui-lozenge* → neura-lozenge*
aui-avatar* → neura-avatar*
aui-page-panel* → neura-page-panel*
aui-icon-{name} → neura-icon-{name}
The shim's `_aui-classes.scss` has the full mapping if you encounter a
class not listed here — but for new code, always pick the `neura-*` form
directly from each component's reference page.
### What "shim" means
A *shim* is a thin translation layer. The user-facing label is "AUI
compatibility"; source files use "shim" (`src/shim/`) because that's the
engineering term. See `aui-compatibility.html` for full details.
## Conventions
- CSS prefix: `neura-` (canonical). Legacy `aui-*` classes are aliased onto
the `neura-*` rules via Sass `@extend` for back-compat.
- JS public API on `window.Neura.{component}(elOrSelector)` returning a
singleton-per-element instance.
- Every component with a JS API supports `.on('event', fn)` / `.off('event', fn)`.
- Auto-init runs on `DOMContentLoaded` for: dropdown2, message, banner, tabs,
inline-dialog, responsive-header, date-picker, expander.
- Dialogs are imperative (no auto-init). Trigger with `Neura.dialog2('#id').show()`.
- License: Apache 2.0.
## Buttons
CSS-only. No JS API.
Markup:
Click
Link as button
Variants (combine with `.neura-button`):
.neura-button-primary — main action, blue gradient
.neura-button-link — link-style (no background)
.neura-button-subtle — borderless until hover, common for icon buttons
.neura-button-compact — reduced height, modifier on any variant
States:
disabled — native disabled attribute (preferred)
aria-disabled="true" — visually disabled but focusable
aria-pressed="true" — toggle button, pressed state
Composition:
.neura-buttons — wrapper that fuses adjacent button borders
.neura-button-split-main / .neura-button-split-more — split button halves
.neura-dropdown2-trigger — adds a CSS-drawn chevron arrow (use with aria-controls)
Icon inside button: ` `.
Icon-only button: include `aria-label`.
## Dialogs
JS API: `Neura.dialog2(elOrSelector).{show, hide, toggle, remove, on, off}`
plus the `.isOpen` property. Imperative — no auto-init. Returns singleton
per element. `remove()` closes, forgets the singleton, and removes the
element (for dynamically-built one-shot dialogs).
Markup contract:
Sizes (pick one): -small (400px), -medium (600px), -large (800px), -xlarge (980px).
Strict modal — disables Esc and backdrop dismiss, hides close button:
data-neura-modal="true"
Optional header slots:
.neura-dialog2-header-actions — inline action button(s) next to title
.neura-dialog2-header-secondary — right-aligned content (e.g. search input)
Optional footer slot:
.neura-dialog2-footer-hint — left-aligned hint text
JS — open / close:
Neura.dialog2('#x').show();
Neura.dialog2('#x').hide();
JS — react to lifecycle:
Neura.dialog2('#x').on('show', () => { ... });
Neura.dialog2('#x').on('hide', () => { ... });
// DOM events 'neura-dialog-show' / 'neura-dialog-hide' bubble and are
// NOT cancelable ('show' fires after open + focus trap; 'hide' after
// close + focus restore). One document-level listener observes every
// dialog (replaces AUI's global AJS.dialog2.on bus, which the shim
// does not provide):
document.addEventListener('neura-dialog-show', (e) => e.target.id);
Focus is trapped inside the dialog while open and restored to the previously-
focused element on close.
## Dropdowns
JS API: `Neura.dropdown2(triggerOrMenu).{show, hide, toggle, on, off}`
Auto-init binds every `.neura-dropdown2-trigger[aria-controls]` on load.
Markup contract:
Menu
The chevron arrow on the trigger is drawn by CSS — no inner icon needed
on `` elements with the trigger class.
Sectioned menu — use multiple `.neura-dropdown2-section` blocks:
Item modifiers:
.active — current/highlighted
.disabled — not clickable
.neura-dropdown2-checkbox — multi-select; click toggles `.checked`
.neura-dropdown2-radio — single-select within a ``
.neura-dropdown2-sub-trigger — opens nested menu (combine with -trigger)
Submenu:
More options
Header alignment menu:
.neura-dropdown2-in-header + data-dropdown2-alignment="left|right"
Re-init after dynamically inserting triggers:
import('@neura/js/components/dropdown2.js').then(m => m.autoInit());
The menu portals to on show so it escapes overflow:hidden ancestors.
Closes on outside click and Esc.
## Inline dialog
JS API: `Neura.inlineDialog(triggerOrPopover).{show, hide, toggle, on, off}`
plus `.isOpen`. Auto-init. No options object — positioning is automatic
(8px below the trigger, inline-start aligned + RTL-aware, flips above
with the bottom-arrow modifier when out of room, viewport-clamped) and
the popover closes on outside click, Esc, or page scroll. Events on the
POPOVER element, bubbling, not cancelable:
`neura-inline-dialog-show` / `neura-inline-dialog-hide`
(on('show'/'hide') maps to these). Trigger carries `.active` +
aria-expanded while open. AUI InlineDialog options map: width → CSS;
offsets/onTop → automatic; onHover/showDelay → use tooltip;
hideDelay/fadeTime → none; persistent → strict-modal dialog2;
url/cacheContent → load in on('show'); init/hideCallback → on('show'/'hide');
closeOthers/addActiveClass/displayShadow → always; noBind → call
Neura.inlineDialog(trigger) for late markup.
Markup:
Show
Default popover width ≈ 320px. For forms inside, use `.neura-form-top-label` +
`.neura-field-full` to avoid horizontal clipping.
## Tabs
JS API: `Neura.tabs(rootOrSelector).on('change', fn)` — auto-init.
Markup:
Variants:
.neura-tabs-horizontal — tabs on top, panel below
.neura-tabs-vertical — tabs on left, panel on right
.neura-tabs-disabled — clicks ignored
Match tab `href="#tab-N"` to pane `id="tab-N"`. Initial active state set by
`.active-tab` on the menu item + `.active-pane` on the panel.
## Messages
JS API: `Neura.message(elOrSelector)` — auto-init; `.dismiss()`,
`.on('dismiss', fn)` (the `neura-message-dismiss` CustomEvent, fired just
before removal). Closeable messages auto-bind their close button (no JS
needed for default behavior).
Dynamic creation:
const m = Neura.message.create({
type: 'success', // info | warning | error | success | hint
title: 'Saved', // optional, plain text
body: 'All good.', // plain text; bodyHtml for markup
closeable: true, // default false
context: '.flash-area', // element/selector to append into;
}); // omit → insert m.el yourself
// Adds the per-variant icon + live-region role (alert for
// error/warning, status otherwise). Returns the instance.
Markup:
Variants:
.neura-message-info — neutral notification (icon: neura-icon-info)
.neura-message-warning — caution (icon: neura-icon-triangle-alert)
.neura-message-error — destructive (icon: neura-icon-circle-alert)
.neura-message-success — confirm (icon: neura-icon-check)
.neura-message-hint — tip (icon: neura-icon-info)
Closeable — add `.neura-message-closeable` + close button:
...
Solid — add `.neura-message-solid` for a loud banner-palette fill
(info/warning/success only; error is already solid, hint has no solid
form). Solid info uses the brand primary token (theme-aware).
Neura-native modifier — no aui-* alias:
…
## Banners
Full-width page-top bar for system-wide state (maintenance, license
warnings, lost connectivity). Neura-native — no AUI 5.4 equivalent, no
aui-*/AJS alias. Persists until closed; sits in flow above the header
(never overlaps). Announcement variant uses the brand primary token
(theme-aware); warning/error use status tokens. Auto-init wires
.neura-banner-close on static markup.
Markup:
Variants: .neura-banner-announcement | -warning | -error
JS:
const b = Neura.banner({ type: 'announcement'|'warning'|'error',
body, // plain text (bodyHtml for markup)
close: 'never'|'manual' }); // default 'never'
Neura.banner('#el'); // wrap existing/static markup
b.close(); b.on('close', fn);
// DOM event: 'neura-banner-close' (bubbles); dynamic banners get
// role=alert (error/warning) or role=status (announcement)
## Flags
Stacking: flags stack top-right; when the stack outgrows the viewport
it scrolls (overflow-y on #neura-flag-container) with the newest flag
kept in view.
Auto-dismissing toast notifications (top-right stack). Neura-native —
no AUI 5.4 equivalent, no aui-*/AJS alias. Wraps a .neura-message in
its SOLID variant by default (chrome surfaces are solid, inline
messages subtle; solid: false opts out), so type styling and theming
carry over. role=alert for error/warning,
role=status otherwise; auto-dismiss timer pauses on hover/focus;
reduced-motion disables the slide.
JS:
const f = Neura.flag({ type: 'success'|'info'|'warning'|'error',
title, body, // plain text (bodyHtml for markup)
close: 'auto'|'manual'|'never', // default 'auto'
solid: true, // default; false = subtle message look
duration: 8000 });
f.close(); f.on('close', fn);
// DOM event: 'neura-flag-close' (bubbles)
## Tables
CSS for the table itself; sorting is the Neura-native sortable-table
component (auto-inits on `table.neura-table-sortable`; the shim's
AJS.tablessortable.setup() is a compat alias over it).
Markup:
Modifiers:
.neura-table-interactive — row hover
.neura-table-sortable — clickable + keyboard-operable headers toggle aria-sort
.neura-table-column-unsortable — per-th opt-out of sort
aria-sort values: "ascending" | "descending" | "none".
JS (no JS needed for static markup — auto-init):
const st = Neura.sortableTable('#report'); // singleton per table
st.sort(1, 'descending'); // zero-based column index or
st.refresh(); // bind headers added later
st.on('sort', (e) => e.detail); // { th, columnIndex, direction }
st.destroy();
// DOM event: 'neura-table-sort' (bubbles)
## Forms
CSS-only (date picker is the only embedded JS component).
Grouped controls — the a11y-correct radio/checkbox grouping (screen
readers announce the legend with each option); legends render only
inside this construct:
Save as
Blog post
...
Layout variants:
.neura-form — default (label left, input right)
.neura-form-top-label — labels above inputs (use in narrow containers)
.neura-form-long-label — wider label column
Markup contract:
Input variants (apply on the input):
.neura-field-text — text
.neura-field-password — password
.neura-field-textarea — textarea
.neura-field-select — single-value
.neura-field-multiselect — multi-value
.neura-field-file — file input
Width modifiers:
.neura-field-short — 75px
.neura-field-medium — 165px
.neura-field-long — 500px
.neura-field-full — 100% of group
Radio group:
Option
Checkbox group: same shape with `.neura-field-checkbox`.
Date picker is a separate top-level component — see the next section.
## Date picker
JS API: `Neura.datePicker(input, opts?).{show, hide, setValue, getValue,
setMin, setMax, on, off, destroy}`
Auto-init binds every `input.neura-date-picker-input` on load.
Markup:
Pre-populate by setting `value="YYYY-MM-DD"` on the input. Both
`YYYY-MM-DD` and `YYYY-MM-DDTHH:MM` are accepted as input.
Options:
firstDayOfWeek — 0 (Sunday), 1 (Monday, default).
Falls back to the locale's first day if `locale`
is set and this option is omitted.
locale — BCP-47 string ('fr-FR', 'ja-JP', 'en-US'...). Used
for month/day names via Intl.DateTimeFormat.
min — Date | ISO string. Days before this disable.
max — Date | ISO string. Days after this disable.
time — true to enable a 24h HH:MM picker row.
Output format becomes 'YYYY-MM-DDTHH:MM'.
timeStep — minute granularity for the time row (default 15).
format — explicit output format override (auto from `time`).
API:
const dp = Neura.datePicker('#dob', { locale: 'fr-FR' });
dp.show() / dp.hide();
dp.setValue(new Date(2025, 2, 15)); // Date or ISO string
dp.setValue('2025-03-15');
dp.setValue(null); // clear
dp.setMin('2024-01-01'); // update bound at runtime
dp.setMax(new Date());
const date = dp.getValue(); // Date instance or null
dp.on('change', (e) => console.log(e.detail.value, e.detail.date));
// detail = { value: 'YYYY-MM-DD' | 'YYYY-MM-DDTHH:MM' | '', date: Date | undefined }
dp.destroy(); // remove popover and listeners
Keyboard navigation (popover open, focus in grid):
←/→ previous/next day
↑/↓ same day previous/next week
Home/End start/end of focused week
PgUp/PgDn previous/next month
Shift+PgUp/PgDn previous/next year
Enter/Space select focused day
Esc close popover
Date range (paired pickers): Neura.dateRange(startInput, endInput, opts?)
returns a façade that wires two pickers together. Picking the start sets
the end's min; picking the end sets the start's max.
const range = Neura.dateRange('#trip-start', '#trip-end');
range.start / range.end // underlying DatePicker instances
range.getValue(); // { start: Date | null, end: Date | null }
range.on('change', (e) => /* e.detail = { start, end } */);
range.destroy();
AUI compatibility: `AJS.DatePicker(input, opts)` routes through the shim
to `Neura.datePicker`. `.aui-date-picker` aliases to `.neura-date-picker-input`.
## Header
CSS layout + responsive behavior (auto-init on `data-aui-responsive` /
`data-neura-responsive`).
Markup:
Header dropdowns use `.neura-dropdown2-in-header`. Tail alignment:
`data-dropdown2-alignment="left|right"`.
JS API: `Neura.responsiveHeader(input).destroy()`.
## Icons
CSS-only, mask-image + currentColor.
Markup:
Available glyphs:
apps, settings, user, mail, close, chevron-down, chevron-up,
chevron-left, chevron-right, check, circle-alert, triangle-alert,
info, search, plus, minus, pencil, trash, copy, eye, eye-off,
menu, more, bell
Sizes:
.neura-icon — 16×16 (default)
.neura-icon-large — 32×32
.neura-icon-small — 16×16, header-aligned
Icons inherit color from the parent (`currentColor`). Set `color` on the
parent to theme.
Adding a new icon: append to `src/icons/icons.config.mjs` with `{ name, lucide }`
mapping, then `npm run icons:build` regenerates the CSS.
## Page (entire-page skeleton)
The outside-in structure. Neura styles BOTH the class hooks and the
literal #header/#content/#footer ids (what server templates emit), so
either form works:
Direct children of #content get the 20px content padding. AUI-era body
markers (aui-layout, aui-theme-default) are inert; legacy unprefixed
`.footer-body` aliases onto neura-footer-body.
## Page layout
The page shell. CSS-only, full aui-page-panel* parity (flex internals,
identical geometry).
Width modes (on ): neura-page-focused[-small|-medium|-large|
-xlarge] (centered fixed width) | neura-page-fixed | neura-page-hybrid
Grouping: .neura-group > .neura-item; modifiers -split, -trio
Utilities: .neura-hidden | .neura-assistive | .neura-clear
Sidebar widths: .neura-page-panel-sidebar defaults to 35% (30% when a
nav cell is present via `.neura-page-panel-nav ~ .neura-page-panel-sidebar`).
Apps pin a fixed width with their own class — restate with the element
for specificity (the sibling rule outranks a bare class):
.neura-page-panel-nav ~ aside.my-sidebar,
aside.neura-page-panel-sidebar.my-sidebar { width: 340px; }
## Page header
Title band between app header and page panel. CSS-only.
Strict pattern (avatar/breadcrumbs/actions optional, markup shape is
not). Variants: .neura-page-header-hero | -marketing
## Navigation
Navgroups + breadcrumbs + pagination. CSS-only.
Horizontal: .neura-navgroup-horizontal with .neura-navgroup-primary
(inline-start) and -secondary (inline-end) groups; badges welcome in
items.
Parent
Current
Pagination — current page is nav-selected and NOT a link; long ranges
truncate:
## Utility classes
.neura-hidden — display: none; unavailable to ALL users (incl. SRs)
.neura-assistive — visually hidden, still announced by screen readers
(clip-rect; for text alternatives to visual cues)
.neura-clear — clear: both (float clearfix)
Show/hide by toggling the class (never mix with style-based show/hide —
a stale inline display wins over the class):
el.classList.toggle('neura-hidden');
Don't put critical content behind neura-hidden without a keyboard-
accessible reveal; don't abuse neura-assistive for non-spoken content.
AUI compat: the unprefixed legacy `.hidden` / `.assistive` alias onto
these.
## Typography
Element-level (no classes): plain HTML renders styled. 14px base,
20px line (1.42857). Font is a token — --neura-font-family defaults
to Arial for AUI 5.4 parity; override it to retheme:
:root { --neura-font-family: -apple-system, "Segoe UI", Roboto, sans-serif; }
Mono stack for code/kbd: --neura-font-family-mono.
Heading scale (size / weight / top margin):
h1 24px normal 30px · h2 20px normal 30px · h3 16px bold 30px
h4 14px bold 20px · h5 12px uppercase gray 20px · h6 12px gray 20px
Vertical rhythm: block elements (p, ul, ol, dl, blockquote, pre, and
opt-in components: form.neura-form, table.neura-table, .neura-tabs,
.neura-panel, .neura-group) get a uniform 10px TOP margin; no bottom
margins ever. :first-child drops its top margin; adjacent heading
levels (h1+h2, h2+h3, …) close up to 10px.
Inline semantics: links = --neura-color-link, underline on hover/focus
only, visited token; small = 12px gray; code/kbd = mono stack;
var/address/dfn/cite italic (cite prefixed with an em-dash);
q = locale quotes, gray; abbr = dotted underline + help cursor;
blockquote = start border + indent + gray.
## Lozenges
Status chips. CSS-only. SOLID by default (status chips are chrome —
same rule as flags/banners); `-subtle` modifier for the outline look.
Colors from --neura-color-status-* tokens.
Online
Variants: (default) | -success | -error | -current | -complete | -moved
Modifier: .neura-lozenge-subtle (combine with any variant)
## Badges
Numeric counters. CSS-only, one neutral style (no status colors — use a
lozenge for state). Adapts contrast inside buttons and the header.
3
## Avatars
User/project images. CSS-only. AUI-compatible layout (inline-block +
table-cell inner) — nested extra content flows below the box.
Sizes: -xsmall 16 | -small 24 | -medium 32 | -large 48 | -xlarge 64 |
-xxlarge 96 | -xxxlarge 128. Modifier: .neura-avatar-project.
## Labels
Freeform chip tags (metadata, not status). CSS-only; removal is the
consumer's click handler — e.g.:
btn.addEventListener('click', () => btn.closest('.neura-label').remove());
docs
urgent
frontend
(close glyph = nested .neura-icon-close; the closeable class
reserves the padding; split also requires the closeable class)
## Expander
Collapsible content ("Read more"). Auto-init binds
.neura-expander-trigger[aria-controls].
Teaser…
Full content.
Read more
JS: Neura.expander(triggerOrContent).{expand, collapse, toggle, on}
## Progress
Tracker (wizard steps) + indicator (bar). Indeterminate animation
freezes under prefers-reduced-motion.
Details
Configure
Review
Modifiers: .neura-progress-tracker-inverted (white dot halos, for
trackers on white surfaces); .neura-progress-indicator-static
(no width transition on the bar).
JS (indicator; no JS needed for static markup):
const bar = Neura.progress('#bar'); // singleton per indicator
bar.update(0.6); // 0..1 — sets fill width, data-value, aria-valuenow
bar.setIndeterminate(); // back to the animated bar
bar.value(); // current value, or null when indeterminate
AUI compat: AJS.progressBars.update / .setIndeterminate alias over Neura.progress.
## Panels
Generic bordered card. CSS-only.
NOTE: aui-panel is deliberately NOT aliased (AUI 5.4 gave it margin
only); .neura-panel is Neura-native styling.
…
## Toolbars
Toolbar2 — action bar with primary (inline-start) / secondary
(inline-end) groups. CSS-only; buttons inside use normal button classes.
## Quicksearch
Pill search input for the header. CSS-only styling; wire your own
search behavior.
## Spinner
CSS + tiny JS helper. Indeterminate loading ring, inherits `currentColor`.
Markup:
.neura-spinner-medium / .neura-spinner-large — 26px / 36px (base 12px)
JS:
Neura.spinner(el, { size: 'small'|'medium'|'large', color, label })
Neura.spinnerStop(el)
## Tooltip
Hover + keyboard-focus hint. Text from `title` attr (moved aside on bind),
a string, or a generator fn. `role="tooltip"` + aria-describedby while open;
Esc dismisses.
Auto-init: `[data-neura-tooltip]` (attribute value = placement).
…
JS:
const tip = Neura.tooltip(el, { placement: 'top'|'bottom'|'left'|'right'|'auto',
title, delayIn: 500, delayOut: 0, html: false });
tip.show(); tip.hide(); tip.destroy();
## Keyboard shortcuts
Key-sequence registry. "gd" = g then d; "gh gd" = alternatives; "ctrl+b"
modifiers. Never fires in inputs/textareas/selects/contenteditable or while
a modal dialog is open.
JS:
Neura.shortcuts('gd').or('gh').goTo('/dashboard');
Neura.shortcuts('c').click('#create');
Neura.shortcuts('j').moveToNextItem('.item', { focusedClass: 'focused' });
Neura.shortcuts('k').moveToPrevItem('.item');
Neura.shortcuts('ctrl+enter').execute(fn);
sc.unbind();
// also: followLink, moveToAndClick, moveToAndFocus, evaluate
## Sidebar
Drag-to-resize panel; width persists per id in localStorage. Handle is a
focusable role="separator": ←/→ resize 10px (Shift: 50px), Home/End min/max.
JS:
const sb = Neura.sidebar(el, { id: 'main-nav', minWidth: 120,
maxWidth: () => innerWidth / 2, onResize: (w) => {} });
sb.setWidth(300); sb.updatePosition(); sb.destroy();
// event: 'neura-sidebar-resize' (detail.width)
## Select
Searchable single/multi select on top of a native (kept in sync —
forms and change listeners work unchanged). Combobox-pattern ARIA. Multi
mode renders pills; Backspace in empty search removes the last pill.
JS:
const sel = Neura.select(selectEl, { placeholder, allowClear,
data: [{ id, text }], formatResult, formatSelection });
sel.val(); sel.val('a'); sel.open(); sel.close(); sel.enable(false);
// event: 'neura-select-change' (detail.value)
## RESTful table
CRUD table bound to a REST endpoint via fetch. GET resources.all,
POST/PUT/DELETE against resources.self. Click a row to edit inline;
Enter submits, Esc cancels. Footer row creates.
JS:
const rt = Neura.restfulTable(tableEl, { // singleton per element
resources: {
all: '/api/x', // required — URL (GET, JSON array) or fn(callback)
self: '/api/x', // POST self | PUT self/{id} | DELETE self/{id}
},
columns: [{ id, header, allowEdit?, readView?(value, entry) }],
// allowEdit: false → read-only when editing (create row still gets an input)
// readView returns an HTML string (escape user data yourself)
allowCreate, allowEdit, allowDelete, // all default true
autoFocus, // refocus create row after adding (default false)
noEntriesMsg, loadingMsg, // defaults localized via Neura.i18n
createPosition: 'bottom'|'top' }); // default 'bottom' (AUI was 'top')
rt.reload(); rt.getEntries(); rt.destroy();
// events on the table el: neura-restfultable-initialized |
// row-added | row-updated | row-removed (detail: { entry, table })
Server contract: entries are JSON objects and MUST carry an `id`; every
column id is a property of the entry:
GET all → [ { "id": 1, "name": "nsys-pi", "ip": "10.100.20.50" }, … ]
POST self → body { "name": "x", "ip": "y" } (no id);
response echoes stored entry incl. server-assigned id
PUT self/{id} → body = full entry merged with edits; response may echo
DELETE self/{id} → 204
POST/PUT responses may echo the stored entry — echoed fields win (how a
server-assigned id gets in); empty responses keep the submitted values.
Non-2xx aborts the operation (form row stays open); load failure renders
an inline error row.
Not ported from AUI's Backbone version: model classes, editView/
createView (readView fn only), allowReorder, reverseOrder,
deleteConfirmation, access keys, EditRow/Row sub-view methods + events.
## i18n
Neura localizes its OWN ~14 interface strings (date-picker buttons/
aria-labels, dismiss/loading labels, empty states). Month/weekday names
+ first day of week come from browser Intl for the active locale — no
translation file needed for those. App text stays the server's job.
Date INPUT format stays ISO. 42 packs built in (every EU official
language + Nordics + most-used world languages): ar bg bn cs da de el
es et fa fi fr ga he hi hr hu id is it ja ko lt lv mt nb nl pl pt ro
ru sk sl sr sv th tr uk ur vi zh zh-Hant. Aliases: zh-TW/zh-HK/zh-MO →
zh-Hant, no → nb. Neura.i18n.locales() lists them.
RTL: fully supported — set dir="rtl" on (or a subtree) and
layout, popovers, drag directions, and keyboard semantics all mirror
(horizontal arrows follow visual direction in tabs and the date grid).
ar/fa/he/ur are supported end to end.
Locale selection: Neura.i18n({locale}) > > 'en'.
Set BEFORE components render (strings read at render time).
JS:
Neura.i18n({ locale: 'cs' });
Neura.i18n({ messages: { today: 'Nyní' } }); // per-key override
Neura.i18n.register('de', { today: 'Heute', /* …14 keys */ });
Neura.i18n(); // → { locale, messages }
Keys: today, clear, dismiss, chooseDate, prevMonth, nextMonth, hour,
minute, clearSelection, searchOptions, noMatches, noEntries, loading,
resizeSidebar.
## Recipes (canonical patterns)
The docs "Recipes" page holds complete, tested patterns - generate
from these instead of improvising:
1. Confirm a destructive action: [data-confirm] trigger + ONE delegated
document click listener; dialog built with createElement, title/body
set via textContent (never innerHTML interpolation); confirm = POST
(never GET navigation); dialog.remove() on close.
2. Form in a dialog: wire submit inside a document-level
'neura-dialog-show' listener keyed on e.target.id (works for
fetched, templated, or server-rendered content); guard rewiring
with a dataset flag.
3. Server validation errors: response { status, result: {field: msg} }
mapped onto .neura-field-error slots by form.elements[name];
clear all slots first; Neura.flag on success.
## Playground
Deep link: /playground.html#code= opens the
playground with that code loaded and run; append &theme= to preview under a built-in theme. Docs HTML panels link there
via their Playground button, and the playground's Share button writes
the current editor code into the URL and copies the link.
Docs page "Playground": paste markup, preview runs against the real
built bundles with auto-init live, theme selector included. Sandboxed
iframe (allow-scripts). Link clicks never navigate the preview: a
srcdoc document resolves every href (even '#x') against the parent
URL, so the preview blocks the default and emulates fragment
scrolling; example hrefs are illustrative server routes that cannot
resolve there.
## Editor metadata
The npm package / flatpack zip ship `web-types.json` (IntelliJ-family
class completion for every neura-* class, auto-detected from
node_modules) and `editor/neura.code-snippets` (VS Code skeletons:
neura-dialog2, neura-dropdown2, neura-field-group, neura-message,
neura-banner, neura-tabs, neura-table-sortable, neura-flag,
neura-confirm).
## TypeScript
`dist/neura.d.ts` types the full Neura.* surface (instances, options,
events) and the window.Neura global. Ships in the flatpack, the zip,
and /cdn/neura.d.ts. The legacy AJS facade is deliberately untyped.
## Debug mode
Opt-in markup-contract diagnostics. Enable with
(scans after auto-init) or Neura.debug(true) (scans immediately);
Neura.debug(false) forces off; Neura.debug(null) returns to following
the attribute (an explicit true/false always wins over the attribute
until reset). Neura.debug.scan(root) re-checks injected content. Warns (console.warn,
message includes the fix) about: triggers with missing/dangling
aria-controls, tooltip targets without title / with text in
data-neura-tooltip (the value is the PLACEMENT), tabs menu links
without a matching pane, .neura-date-picker-input on a non-input,
dialogs without an id, closeable messages without a close control.
Zero cost when disabled; keep it off in production.
## Theming
Built-in brand themes: token overrides selected by a `data-neura-theme`
attribute (server-renderable; tokens cascade, so it also works on a
subtree). Default is Neura indigo (no attribute). Built-ins: `legacy`
(exact AUI 5.4 Atlassian-blue palette), `teal`, `violet`, `graphite`.
HTML:
…
JS:
Neura.theme(); // → current name ('default' if unset)
Neura.theme('teal'); // switch
Neura.theme('default'); // back to indigo
Custom themes are CSS-only — override the brand tokens under your own
attribute value (full token list in src/css/_themes.scss):
[data-neura-theme='acme'] { --neura-color-primary: …;
--neura-button-primary-bg-top: …; /* … */ }
## Design principles
Two-era strategy: 0.x–1.x renders pixel-compatible with AUI 5.4 ON
PURPOSE (migration safety; verified by pixel-diffing real portal
pages) — the brand lives in the color system, the mark, theming, and
depth (a11y/RTL/i18n). The post-parity Neura visual language arrives
at 2.0 when the shim drops. Do not "modernize" component styling
before then.
Brand colors are DERIVED, never picked: every stop generates from the
anchor #3f3d9c via the OKLCH formula in scripts/derive-palette.mjs
(WCAG-solved lightness, gamut-clamped chroma, asserted disjoint from
framework palettes). To change or extend the palette, edit hues in
that script and re-run it; never hand-pick hex values.
## Design tokens
All styling flows through `--neura-*` CSS custom properties on `:root`
(62 tokens; full reference with values + swatches on the docs site's
"Design tokens" page). Consumers may use them in their own CSS
(`color: var(--neura-color-link)`) and override them on any subtree.
The theming surface (what built-in themes override): the 5 brand colors
(`--neura-color-primary/hero/focus/link/link-visited`), the 10
`--neura-button-primary-*` gradient/hover/disabled stops, and
`--neura-header-border`. Status palettes, surfaces, borders, and grid
tokens are shared by all themes.
## Security
Escaping model: options that take TEXT are inserted with textContent and
can never introduce markup (message/flag/banner title and body, tooltip
text, select labels, restful-table cell values, date-picker labels).
These options take RAW HTML deliberately - escape untrusted values before
passing them:
Neura.message / Neura.flag / Neura.banner bodyHtml
Neura.tooltip html: true
Neura.select formatResult, formatSelection
Neura.restfulTable column readView
AJS.messages.* (shim) body (raw, AUI parity)
AJS.Dialog.addPanel (shim) HTML string content
Use Neura.escapeHtml(value) for those - it escapes & < > " ' and a
backtick, so it is safe in attribute contexts too. Do NOT use the
textContent/innerHTML round-trip as an escaper: it leaves quotes intact
and will not protect an attribute.
Neura.flag({ title: name, bodyHtml: `${Neura.escapeHtml(text)} ` });
When generating code that renders server data, prefer building nodes and
setting textContent over interpolating into innerHTML.
The library contains no eval, no new Function, and no inline event
handlers, and runs with zero violations under:
default-src 'none'; script-src 'self'; style-src 'self'; img-src 'self' data:
Consumer responsibilities: escape untrusted data before raw-HTML options;
POST + CSRF token for destructive actions (see Recipes); authorize
restful-table endpoints server-side.
Docs: /security.html - policy: SECURITY.md (security@nsys.org)
## Accessibility
Keyboard-first: every interactive component is operable without a mouse
(dialog focus trap + Esc + focus restore; dropdown virtual cursor with
wrap; date-picker grid keys with typeable input; tabs/date-grid arrows
follow VISUAL direction in RTL; sortable-table headers focusable with
Enter/Space; sidebar handle is a keyboard-resizable role="separator").
Focus rings are :focus-visible only (mouse keeps hover look). Dynamic
messages/flags/banners get live-region roles (alert for error/warning,
status otherwise) — static server-rendered ones deliberately none.
prefers-reduced-motion freezes the progress animation and slows the
spinner; forced-colors keeps mask-image icons + selection visible.
Consumer duties: aria-label icon-only triggers, label your inputs,
escape user data passed to *Html options/readView. Full reference on
the docs site's "Accessibility" page.
## AUI compatibility shim
A "shim" is a thin translation layer that intercepts API calls and forwards
them to a different implementation, letting two incompatible interfaces work
together without modifying either side. The term is jargon — the user-facing
label in the sandbox sidebar is "AUI compatibility". Source files use "shim"
(`src/shim/`) because that's the engineering term-of-art.
Drop-in replacement strategy. Replace AUI's flatpack files with `dist/neura.css`
and `dist/neura.js` and existing Velocity / `AJS.*` code keeps working.
CSS aliases — every `aui-*` class extends the matching `neura-*` rule. Source:
`src/shim/_aui-classes.scss`.
JS — `window.AJS` facade installed at module load. Source: `src/shim/ajs.js`.
Components routed to Neura:
AJS.dialog2 → Neura.dialog2
AJS.dropdown2 → Neura.dropdown2
AJS.tabs → Neura.tabs
AJS.InlineDialog → Neura.inlineDialog
AJS.messages.* → Neura.message
AJS.responsiveheader → Neura.responsiveHeader
AJS.DatePicker → Neura.datePicker
AJS.expander → Neura.expander
Legacy v1 builders re-implemented:
AJS.Dialog (full builder API on top of dialog2)
AJS.dropDown (routes to dropdown2)
AJS.tablessortable.setup() (compat alias over Neura.sortableTable)
AJS.layer / AJS.LayerManager / AJS.FocusManager
AJS.progressBars.update() / setIndeterminate() (compat aliases over Neura.progress)
Utilities (real implementations):
AJS.escapeHtml, parseHtml, format, template, contextPath, debounce,
_addID, id, isClipped, bind/unbind/trigger, toInit, keyCode,
log/warn/error, I18n.{getText, keys}, Cookie.{read, save, erase},
populateParameters, $, version
AJS.template carries AUI's FULL contract (legacy code only — new code
uses JS template literals): fill() HTML-escapes every value; raw
insertion is per-value via a "key:html" DATA key (the token stays
{key} — a {key:html} token suffix is NOT the mechanism); tokens can be
paths ({a.b}, {a["x y"]}) and calls ({fn()}); unresolved tokens remain
so partial fills chain; fillHtml() is the all-raw variant;
template.load(title) reads
Pinned paths are immutable-cached; unversioned "latest" paths revalidate
within 5 minutes of a deploy. CORS is open (Access-Control-Allow-Origin:
*), so module imports and source maps work cross-origin.
To vendor the files instead, download the flatpack archive (extracts to
neura-/ with all dist files + LICENSE + NOTICE):
https://neura.nsys.org/cdn/neura.zip (latest)
https://neura.nsys.org/cdn//neura-.zip (pinned)
## File / module map
src/css/index.scss — main stylesheet entry
src/css/_tokens.scss — --neura-* CSS custom properties
src/css/_{component}.scss — per-component styles
src/css/_icons.generated.scss — auto-generated from icons.config.mjs
src/icons/icons.config.mjs — icon → Lucide name mapping
src/js/index.js — JS entry, exposes window.Neura
src/js/components/{component}.js — per-component JS
src/js/internal/layer-manager.js — z-index + outside-click + Esc orchestration
src/js/internal/focus.js — focus trap for dialogs
src/shim/_aui-classes.scss — aui-* → neura-* CSS aliases
src/shim/ajs.js — window.AJS facade
src/shim/ajs-utils.js — utility passthrough (escapeHtml etc.)
src/shim/ajs-legacy-dialog.js — AJS.Dialog v1 builder reimpl
src/shim/ajs-tables.js — AJS.tablessortable, tables.rowStriping
dist/neura.css — compiled flatpack (drop-in)
dist/neura.js — compiled ESM bundle
dist/neura.umd.cjs — compiled UMD for non-module envs
## See also
/index.html — sandbox overview
/buttons.html — full reference page for each component
/date-picker.html
/dialogs.html
/dropdowns.html
/forms.html
/header.html
/icons.html
/inline-dialog.html
/messages.html
/tables.html
/tabs.html
/aui-compatibility.html — full shim reference