# 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: 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:

Title

...
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:
  • Item
The chevron arrow on the trigger is drawn by CSS — no inner icon needed on ` 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:
Maintenance 22:00 UTC.
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:
Name Email Actions
...
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
...
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:
Help text.
Error message.
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-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:
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:
...

APP

    ...
...
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:
…
…
…
  • …
  • …

Copyright …

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).
…vertical navgroup…
…
…
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.
  1. Projects
  2. Current

Title

…buttons…
Strict pattern (avatar/breadcrumbs/actions optional, markup shape is not). Variants: .neura-page-header-hero | -marketing ## Navigation Navgroups + breadcrumbs + pagination. CSS-only.
  • Current
  • Item 7
Heading
  • Item
Horizontal: .neura-navgroup-horizontal with .neura-navgroup-primary (inline-start) and -secondary (inline-end) groups; badges welcome in items.
  1. Parent
  2. Current
Pagination — current page is nav-selected and NOT a link; long ranges truncate:
  1. First
  2. Prev
  3. 1
  4. 2
  5. …
  6. Next
  7. Last
## 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. Name 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.
  1. Details
  2. Configure
  3. 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