# Lumi UI and theme system

Lumi UI is the project-native design layer used by every page rendered through the
shared EJS layout. It keeps route behavior and page-specific JavaScript separate
from visual tokens and reusable components.

## Files

- `src/web/public/lumi-tokens.css`: semantic colors, spacing, radii, shadows,
  typography, compatibility aliases, and reduced-motion behavior.
- `src/web/public/lumi-layout.css`: application shell, sidebar, content
  containers, responsive grids, stacks, clusters, and mobile navigation.
- `src/web/public/lumi-components.css`: buttons, forms, cards, tables, lists,
  badges, alerts, tabs, modals, empty/loading/error states, stateful action
  buttons, and tooltips.
- `src/web/public/lumi-state-button.js` and
  `src/web/views/partials/state-button.ejs`: reusable multi-state button
  behavior for submit/loading/success actions.
- `src/web/public/lumi-interactions.js`: progressive interaction layer for
  server-sent events, no-auto-refresh notices, dirty settings save bars,
  expandable settings containers, refresh prompts, and soft navigation.
- `src/web/public/lumi-request.js`: shared enhanced request parser and the
  `{ ok, message, fieldErrors, data, patch, redirect, reloadRequired }`
  response contract.
- `src/web/public/lumi-page.js`: opt-in page mount/unmount lifecycle. It aborts
  page-owned listeners and requests before running the registered teardown.
- `src/web/public/lumi-dialog.js`: shared focus trap, background isolation,
  Escape policy, scroll lock, and opener restoration for modal surfaces.
- `src/web/public/command-management.js` and `command-policies.js`: lifecycle-owned
  custom-command editing, targeted collection refresh, policy search, inherited
  summaries, and scoped save reconciliation.
- `src/web/public/update-management.js`: lifecycle-owned repository checks,
  progress subscriptions, recovery actions, snapshot-policy validation, and
  restart reconnection without document reloads.
- `src/services/web-events.js`: small role-aware Server-Sent Events bus exposed
  at `GET /api/events` for authenticated users.
- `src/web/public/styles.css`: layered legacy/base styles kept during migration.
  It must not become a second component library. New general-purpose styling
  belongs in the Lumi UI files.
- `src/web/views/partials/page-header.ejs`: standard page title and description.
- `src/web/views/partials/theme-vars.ejs`: safe active-theme variables for shell
  and standalone pages.
- `src/services/themes.js`: built-in themes, custom theme CRUD, validation,
  migration, fallback handling, and active-theme selection.

Use `lumi-stack`, `lumi-cluster`, `lumi-split`, `lumi-grid`, `page-header`,
`button-group`, `card`, `panel`, `table-wrap`, `empty-state`, `loading-state`,
and `status-indicator` before adding one-off layout rules. Preserve existing IDs,
field names, data attributes, and JavaScript hooks when restyling a page.

## Product-wide UX principles

Lumi follows the interaction principles catalogued by DesignMotionHQ across the
entire product, not only on individual redesigns. Treat these as review criteria
for core pages, plugins, overlays, dialogs, and future features:

- Use proximity and whitespace to make related controls read as a group before
  adding borders or extra containers. Keep within-group spacing tighter than the
  space between groups.
- Match surface hierarchy to the task. Routine, non-blocking work belongs in the
  page, an anchored popover, or an edge drawer that preserves context. Reserve a
  modal for a decision or task that genuinely blocks the rest of the interface.
- Keep disclosure predictable. Use native buttons and details/summary controls,
  expose their state to assistive technology, and avoid layout jumps that move
  the control a person just used.
- Give every interactive element a visible focus state. DOM order must match the
  visual reading order, and closing a temporary surface returns focus to its
  opener.
- Motion explains spatial or state changes and never delays routine work. Honor
  reduced-motion preferences and avoid decorative animation that competes with
  status, content, or controls.

New reusable behavior belongs in the shared Lumi UI layer. A feature stylesheet
may compose those patterns for its own layout, but should not create a competing
interaction model.

## Interaction Rules

Pages should not self-refresh for state or progress changes. Core connection
recovery now displays a notice instead of calling `window.location.reload()`.
Server-originated events use `GET /api/events` with explicit event names such as
`server:status`, `server:warning`, `ai:model_status`, and
`data:new_available`. Admin-only events must be published with `{ role: "admin" }`.
Browser consumers subscribe through `window.LumiEvents.subscribe(...)` so a page
uses one shared event connection. The connection is closed before full navigation
and on `pagehide`, preventing long-lived streams from starving the next document
request. Public pages do not connect to the authenticated event endpoint.

List/data updates should announce that new data exists and show a refresh prompt.
The shared refresh prompt uses a 3-second cooldown before another refresh can be
requested. It does not replace list contents automatically.

Forms that represent page settings should add `data-lumi-settings-form`.
Action-only forms must not use that attribute. The shared dirty-state layer
tracks original values, marks changed fields with theme-aware unsaved styling,
shows a top Save changes bar, warns before accidental navigation, and clears
markers only after successful saves.

Settings saves use `LumiRequest`, update each successful form's canonical
snapshot, keep failed forms dirty, map returned field errors to their controls,
and return focus to the last edited field. The save bar stays stable while
scrolling and provides both Save and Revert. A successful ordinary save never
reloads the page. Handlers may set `reloadRequired: true` only when a runtime
restart or equivalent context replacement genuinely requires it.

Entity forms that can safely patch their current region use
`data-lumi-enhanced-form`. They retain an ordinary method/action fallback,
receive local pending and result text, preserve input after failure, and emit
`lumi:form-saved` with the normalized response for feature-owned reconciliation.

Buttons should use `partials/state-button.ejs` for submit, loading, success, or
error states. Single-state and multi-state buttons share the same Lumi button
tokens. Hidden states stay measurable with `data-state-hidden`, so the button
width is based on the widest state and state changes do not shift surrounding
layout. Use `.input-action-row` for desktop file/input + action pairs such as
ZIP updates and navigation icon uploads; the row stacks on mobile.

Destructive POST forms should provide context through `data-confirm-title`,
`data-confirm-text`, and `data-confirm-label`. Dynamic JavaScript-only
destructive actions can call `window.LumiConfirm.destructive({ title, text,
label })` to reuse the same modal. The helper keeps vague default confirmation
copy out of normal admin flows and returns focus after cancel/confirm.

Expandable settings rows use `data-lumi-expandable-settings` on a `<details>`
container. Preview text can be wired with `data-placeholder-preview="#field-id"`;
known placeholders such as `{gifter_username}`, `{item_name}`,
`{creator_username}`, and `{amount_display}` render with plausible sample values
without changing the saved template.

Soft navigation is deliberately opt-in with `data-lumi-soft-nav`. A route may
opt in only after its page code uses `LumiPage.register(...)` and its feature
assets are already available. The lifecycle unmounts the previous page before
replacing `main.content`, then updates title, history, active navigation, scroll,
and heading focus. Arbitrary inline scripts are never re-executed. All other
links retain normal browser navigation.

Sidebar navigation sections behave as an accordion. Opening one `.nav-section`
closes the other expanded sections while preserving the active page highlight
and `aria-expanded` state. On mobile the sidebar is a modal drawer: its closed
content is inert and hidden from the accessibility tree, focus is contained
while open, Escape and the scrim close it, and focus returns to the opener.

## Cascade, widths, and state surfaces

The stylesheet contract is declared as `@layer reset, tokens, base, layout,
components, utilities, features, overrides`. Tokens, layout, and shared
components own their respective generic selectors; plugin and editor CSS belongs
to `features`. Temporary specificity exceptions belong to `overrides` and need
a removal note.

Rendered pages default to `standard` width. Pass `pageWidth` as `prose`, `form`,
`standard`, `wide`, or `workspace` to the shared layout. Workspace width is for
editors such as overlays, not ordinary settings pages. The document remains the
primary vertical scroll container; bounded logs, tables, dialogs, canvas
surfaces, and explicitly labelled workspace editor panes are the exceptions.

Use `empty-state`, `loading-state`, `partial-state`, `error-state`,
`offline-state`, and `permission-state` with specific recovery copy. Status is
never color-only. Persistent service degradation uses a banner, field/component
errors stay inline, low-severity completion may use a dismissible notice, and a
dialog is reserved for a genuine decision blocker.

## Themes

Lumi ships with six read-only themes: Lumi Default, Lumi Dark, Lumi Light, High
Contrast, Midnight, and Soft Aurora. Admins select them from **Admin > Theming**.
Built-in themes cannot be renamed, edited, or deleted.

Open a theme's **More actions** section and duplicate it to create an editable
custom theme. Custom themes can be previewed in light and dark mode, saved,
applied globally, renamed, duplicated, or deleted. Deleting the active custom
theme falls back to Lumi Default.

The compact editor exposes common colors first. Advanced controls cover
background glows, raised surfaces, links, buttons, inputs, focus rings, radius,
shadow strength, and spacing scale. Typography controls use constrained font
presets plus bounded base-size, heading-scale, and control-density ranges. The
server accepts only six-digit hex colors, supported font presets, bounded metric
values, and readable text/button/input contrast.

Draft values are isolated to preview roots. The compact preview and pop-out
preview update colors, role colors, metrics, spacing, and typography before
save, but the editor shell and live site keep the active saved theme until the
admin saves/applies the custom theme. The compact preview is hidden on narrow
phone layouts; use the Preview action to open the synchronized pop-out instead.

The compact preview includes representative headings, pills, cards, buttons,
state buttons, inputs, toggles, alerts/statuses, badges, tables, modal samples,
dirty-state markers, and spacing samples. The pop-out uses a faithful Lumi page
shell with safe sample data so admins can test draft themes against dashboard,
settings, logs, AI, and component-like layouts without exposing real admin data.

Missing or invalid stored values are replaced from the custom theme's built-in
base. Existing installations with modified legacy `theme_light_*`,
`theme_dark_*`, or `theme_role_*` settings are migrated once into a custom
**Migrated Theme** and selected automatically. The legacy `/admin/theming` POST
route remains supported and writes into an editable custom theme.

Run `npm run verify:webui` to compile every EJS view and exercise built-in theme
validation plus custom duplicate, apply, edit validation, typography validation,
stateful theme actions, localhost login rendering, rename, and delete.

Run `npm run verify:ux` for the request/lifecycle/dialog/drawer/cascade and
overlay regression contracts. Run `npm run test:ui` for the temporary-database
Playwright matrix at 320, 390, 768, 1280, and 1920 px with axe, keyboard drawer,
no-reload settings, and overlay scroll/save checks.

## Localhost Login

Development builds opened from `localhost`, `127.0.0.1`, or `::1` show a
**Localhost Login** option. It defaults to username `admin` and password `admin`
unless those settings have already been changed. The option is not inserted into
the login list, cannot be used, and does not satisfy setup requirements for
non-localhost requests.

Admins can change the localhost username and password from **Admin > Settings**
when the settings page itself is accessed through localhost. Leaving the
password field blank keeps the existing password.

## Lumi AI Settings And Feedback

Lumi AI's main Selected model dropdown lists only installed/downloaded models.
If the configured model is missing, the settings page shows a warning and saving
requires selecting an installed model. Main context, gate context, and output
token budgets use shared presets from Tiny (256) through Extra extended
(32768). Unsupported freeform values are rejected server-side.

AI feedback supports `feedback_kind` values `strict_correction` and
`instruction_based`; instruction-based feedback is the default because most
reviews are guidance for future replies rather than exact replacement answers.
Feedback tags include `wrong_tool_usage` for cases where the model called the
wrong tool or failed to call an expected tool. Review, edit, and implementation
views show both the kind and tag so admins can tell direct answer corrections
from broader tool-calling or instruction guidance.

Model/runtime downloads and the combined Start/Restart runtime control use the
Lumi state button behavior. Enhanced browsers start downloads and runtime
actions through fetch, update button state/progress in place, and avoid hard
page refreshes; the underlying POST routes remain available for non-JavaScript
fallbacks.

## Homepage Content

Admins configure homepage external link buttons from Admin > Settings with the
Homepage content builder. It writes the existing `homepage_link_buttons` JSON
setting behind the scenes. Each entry may include `enabled`, `label`,
`description`, `url`, `icon_mode`, `icon_url`, `fetched_favicon_url`,
`permission` (`public`, `user`, `mod`, `admin`), and `sort_order`. Entries can
be added, duplicated, moved up/down, removed with contextual confirmation,
enabled/disabled, and previewed in place. Links open in a new tab with
`rel="noopener noreferrer"` and are filtered server-side by permission.

Admins configure priority-based hero entries with the same builder; it writes
the existing `homepage_hero_entries` JSON setting behind the scenes. The
homepage renders the first enabled, available entry the current user can access.
Hero entries support type, priority/order, permission, source/embed/image URLs,
video IDs, availability mode, mutually exclusive autoplay modes, duration
fields, fallback behavior, and a live card preview. The builder shows
video-only controls only for stream/video types and image/embed/source fields
only when they apply. Slow external availability checks are intentionally
avoided; entries fail closed if required local configuration is missing.

## Admin Dashboard And Logs

The admin dashboard polls `GET /api/admin/dashboard-metrics` for process
uptime, memory, plugin counts, content counts, and recent log severity totals.
The dashboard renders lightweight SVG graphs using Lumi tokens and does not add
a frontend framework dependency.

The logs page provides server-side range, severity, component, activity type,
search, and limit filters in a responsive control bar. Summary totals reflect
the complete filtered result rather than only the visible page. Entries expose
their source, category, stable event name, and request ID, and matching events
arrive live through the existing authenticated event stream. Downloads retain
the active component/activity/search filters and support readable text or JSON
Lines. Retention controls remain collapsed until needed.

## Updates And Local-Only Files

Admin update controls use the same progressive action patterns as other Lumi
actions. `/admin/updates` is split into Core and Plugins expandable sections
with collapsed summaries, warning/danger badges, safe target versions, changelog
ranges, migration notes, recovery state, snapshot/revert availability, and live
progress from `GET /admin/updates/events`.

Core and plugin update success keeps the current document available while Lumi
restarts, reports reconnect attempts, and refreshes the update workspace in
place after the service is reachable. Repository checks and snapshot cleanup
settings reconcile locally, with field-linked validation errors. ZIP update
controls remain available but are hidden under advanced/manual reveal sections
because ZIPs may bypass repository metadata and compatibility checks.

See [Update system](updates.md) and [Recovery mode](recovery-mode.md) for the
semver policy, compatibility bridge behavior, snapshots, revert limits, recovery
markers, and manual safe-mode triggers.

The repository ignores local-only coordination and credential artifacts such as
`codex-guidelines`, `Twitch.png`, and `twitch-credentials-lumi.png`. Plugin
runtime data stays excluded from source control; runtime folders are recreated
by the plugin data-directory initializer.

## Visual references

- [Home, desktop](screenshots/lumi-home-desktop.png)
- [Home, 360px mobile](screenshots/lumi-home-mobile.png)
- [Custom theme editor, desktop](screenshots/lumi-theme-editor-desktop.png)
- [Custom theme editor, 360px mobile](screenshots/lumi-theme-editor-mobile.png)

The broad sidebar and content structure remains in place. Theme controls moved
from one long raw color form into the Theme Studio library and grouped custom
editor; no navigation destination or non-theme control was relocated.
