123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126# Guest UI
The shared UI kit for all 18 Lattice guest apps: React primitives and host-matched CSS for Lattice guest apps. The kit uses the guest project's existing React dependency and no component library, remote fonts, or network requests. Storage and events remain the app's responsibility through `window.lattice`.
## Start an app
Import components from `../../shared/ui.jsx` in `apps/<app-id>/index.jsx`. `build.mjs` bundles only imported JavaScript and creates a local `ui.css` for every app. CSS imported by an app is also linked as `app.css`.
```jsx
import React, { useState } from "react";
import { createRoot } from "react-dom/client";
import {
Page,
Header,
Button,
Input,
Row,
EmptyState,
Modal,
Toast,
useToast,
} from "../../shared/ui.jsx";
function App() {
const [open, setOpen] = useState(false);
const [name, setName] = useState("");
const { toast, notify, dismissToast } = useToast();
return (
<Page>
<Header
title="Reading list"
actions={
<Button variant="primary" onClick={() => setOpen(true)}>
Add book
</Button>
}
/>
<EmptyState title="No books yet" />
{open && (
<Modal title="Add book" onClose={() => setOpen(false)}>
<form
onSubmit={(event) => {
event.preventDefault();
if (!name.trim()) return;
// Persist through window.lattice here, then close after success.
setOpen(false);
notify(`Added ${name.trim()}`);
}}
>
<Input
label="Title"
value={name}
onChange={(event) => setName(event.target.value)}
required
/>
<Button type="submit" variant="primary">
Add
</Button>
</form>
</Modal>
)}
<Toast message={toast} onDismiss={dismissToast} />
</Page>
);
}
createRoot(document.getElementById("root")).render(<App />);
```
Run `cd lattice/guest-apps && bun run build`. Open the app in Lattice to see the styled page and dialog. `bun run build:watch` watches app sources and the shared kit.
## Component API
| Export | Props and behavior |
| ---------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `Page` | Semantic main, centered 1000px layout; accepts HTML main props and `className`. |
| `Header` | `title`, optional `subtitle`, `actions`, `children`. One page heading, actions and a theme icon at the right. |
| `Button` | Native button props; defaults to `type="button"`. `variant`: `primary`, `secondary` (default), `ghost`, `danger`. |
| `Input` | Native input props; `label`, `hint`, `error`, `multiline` for textarea. Associates labels and messages with an ID. Supply `aria-label` when omitting `label`. |
| `Row` | `leading`, `title`, `subtitle`, `actions`, `children`, HTML div props. Put rows in `.ui-panel`. Use a button in `title` for an actionable row. |
| `EmptyState` | `title`, optional explanatory children and `action`. |
| `Modal`, `Sheet` | `title`, `onClose`, `children`, optional `actions`, `busy`, `wide`. Mount only while open. `busy` prevents dismissal. `wide` expands the dialog. Focus containment, Escape/backdrop dismissal, focus restoration, scrollable body. Sheet aligns right. |
| `Toast` | `message`, optional `onDismiss`, `tone` (`success` default or `danger`). Live status, or alert for danger; auto-dismiss after five seconds when `onDismiss` exists. |
| `useToast` | Returns `{ toast, notify(message), dismissToast }` for a single message. |
Disable submit controls while saving. Handle errors before closing dialogs. Give icon buttons accessible names. Render selects natively with a label or `aria-label`.
## Styles and themes
Tokens in [tokens.css](tokens.css) match `src/App.css`: `--page`, `--surface`, `--surface-secondary`, `--surface-hover`, `--border`, `--text-primary`, `--text-secondary`, `--text-muted`, `--accent`, `--accent-soft`, `--accent-hover`, `--success`, `--success-bg`, `--warning`, `--warning-bg`, `--danger`, `--danger-bg`, `--shadow`, `--scrim`.
The default follows `prefers-color-scheme`. `Header` follows the system until its theme button is clicked, then sets a light/dark override for the lifetime of the document. The button is always the last header action. A document root `data-theme="light"` or `data-theme="dark"` sets the initial override for previews. No theme is persisted. The font stack uses Inter when installed and falls back to the system sans serif.
Layout helpers: `.ui-actions`, `.ui-toolbar`, `.ui-panel`, `.ui-muted`, `.ui-badge` (also `-accent` / `-danger`), `.ui-avatar`, `.ui-error-text`. Scope app-specific CSS with an app class. Global control styles intentionally apply to native inputs, selects, and textareas.
## Optional form and storage helpers`forms.jsx` exports `RecordForm` (`title`, `record`, `fields`, `onSave`, `onClose`, `busy`, `validate`, `error`, `prepare`, `wide`) and `ConfirmDelete` (`title`, children, `onConfirm`, `onClose`, `busy`, `error`). Field definitions accept `name`, `label`, `type`, `required`, `multiline`, `rows`, `options`, `defaultValue`, `hint`, `placeholder`, `disabled`, `min`, `max`, `step`, `inputMode`, `wide`, and `advanced`. Options are values or `{ value, label }` objects. Advanced fields appear under More details. `prepare(draft)` runs before validation; errors from preparation or saving remain in the open form. Name/title/URL/folder values are trimmed; free text is preserved. These helpers use the same `Modal`, `Field`, and `Button` primitives.
`data.js` exports `useStore({ setup, load, listeners })`: `setup()` creates the app's tables, `load()` returns rows, and `listeners` maps declared listener names to async functions. It returns `{ rows, ready, error, busy, act }`. Use `await act(async () => { /* write, then emit */ })` for UI mutations; it returns `true` on success and `false` after exposing an error. Listener errors propagate to the host. Work is serialized, and tables are ready before an incoming listener runs. Mount one store for the lifetime of a guest document: the host bridge does not provide an unlisten API.
Call `inserted(table, sql, params)` inside a store action or listener to insert and read the resulting row without interleaving other app writes. Table names are app-owned constants; user values belong in parameters. Other helpers are `fieldsOf(row, keys)`, `tagsOf(text)`, `localDate(date?)`, `displayTime(timestamp)` (SQLite UTC or ISO timestamp), and `httpUrl(text)` (HTTP(S) URL or `null`). See the [Contacts app](../apps/com.lattice.contacts/index.jsx) for a complete example and the [browser harness](../dev/README.md) for local verification.
## Additional primitives
Import these from `ui.jsx`; forms come from `forms.jsx`.
| Export | Contract |
| ------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `Field` | `label`, optional `hint`, `error`, `id`, `wide`, `className`, and one native control child. Associates the label and hint with that control. `Input` and `Select` use this wrapper. |
| `Select` | Native select props, `label`, `hint`, `error`, option children. |
| `Search` | `value`, `onChange(value)`, `placeholder` (also the accessible name). Native search input. |
| `Stats` | `items`: `[value, label]` pairs, displayed as three equal columns. |
| `ErrorNotice` | Optional `error`, `clear()`, `pending`. Error is an alert; pending is a status notice. |
| `FormActions` | `busy`, `onCancel`, optional submit `label` (Save by default). Disables both actions while busy. |
Shared layouts: `.ui-form-grid` and `.ui-span` for forms, `.ui-records` for lists, `.ui-master-detail` for list/detail columns, `.ui-card-grid` for cards, `.ui-list-header` for section headings and controls, `.ui-panel-pad` for padded panels. Forms and list/detail layouts collapse on narrow screens. Keep domain styles (book covers, pantry lots, journal calendars) in each app's `style.css`; do not override page headers or button dimensions.
## Record apps`records.jsx` exports `RecordApp({ store, config })` and `RecordActions({ row, actions, title })`. Reading, Workouts, Shared Expenses, and Feed Reader use this optional controller for search, filters, sample choice, create/edit/delete dialogs, and an optional detail pane. It composes `Page`, `Header`, `RecordForm`, and `ConfirmDelete`; it has no separate theme, modal, or form implementation.
The config defines `singular`, `plural`, `subtitle`, `navigationLabel`, `listLabel`, `searchPlaceholder`, `emptyTitle`, `emptyDescription`, `fields`, `samples`, and functions `title(row)`, `search(row)`, `sort(a,b)`, `filters(rows)`, `matches(row,filter)`, `stats(rows)`, `renderCard(row,actions,selected)`. Filters return `{ id, label, count }`; stats return `{ value, label }`. Optional hooks are `prepare(data,previous)`, `renderDetail(row,actions)`, and `extraLabel` / `renderExtra({store,onClose,onDone})`.
Record fields use `key`, `label`, and optional `required`, `numeric`, `decimal`, `scale`, `options`, `default()`, `help`, `placeholder`, `multiline`, `rows`, `wide`, `advanced`. `RecordApp` adapts them to `RecordForm`, converts numeric display units using `scale`, and validates against `store.inputSchema`. `RecordActions` calls `actions.edit(row)` / `actions.remove(row)`; record views can also call `actions.act(row,patch)` or `actions.open(row)`.
`record-store.js` contains the existing manifest-backed record store; `database.js` contains the table helpers used by Recipes, Pantry, and Code Inbox. Their manifests and database schemas remain app-owned. `format.js` contains optional date, amount, and duration formatters. `data.js` also exports `useModel(model)` for subscribable models and `useOperations(refresh)` for guarded UI mutations.