# Lumen ## Packages One repo holds the Lumen family. Each folder `packages/` is published as `@columenlabs/lumen-`, and every package ships at the same version from one release, so an app pins one number for all of them. | Folder | Package | What it holds | Status | |---|---|---|---| | [`packages/ui`](https://github.com/columenlabs/lumen/blob/main/packages/ui) | [`@columenlabs/lumen-ui`](https://www.npmjs.com/package/@columenlabs/lumen-ui) | The components, the shadcn parts under `ui/*`, and `theme.css` | Published | | [`packages/ui-types`](https://github.com/columenlabs/lumen/blob/main/packages/ui-types) | [`@columenlabs/lumen-ui-types`](https://www.npmjs.com/package/@columenlabs/lumen-ui-types) | The view model: view config, formulas, domains and joins, the data catalog, frames, chart formulas. No React | Published | | [`packages/ship`](https://github.com/columenlabs/lumen/blob/main/packages/ship) | [`@columenlabs/lumen-ship`](https://www.npmjs.com/package/@columenlabs/lumen-ship) | Release notes written by Claude, the changelog What's new reads, lint guards | Published | | [`packages/auth`](https://github.com/columenlabs/lumen/blob/main/packages/auth) | [`@columenlabs/lumen-auth`](https://www.npmjs.com/package/@columenlabs/lumen-auth) | Sign-in checks: Cloudflare Access tokens and service tokens, verified in the Worker | Published | | [`packages/ops`](https://github.com/columenlabs/lumen/blob/main/packages/ops) | [`@columenlabs/lumen-ops`](https://www.npmjs.com/package/@columenlabs/lumen-ops) | Ops meters to Analytics Engine (every data query by its shape, every tool call with its latency and code hash) and signed webhooks out; later page errors and email | Published | | [`packages/ai`](https://github.com/columenlabs/lumen/blob/main/packages/ai) | [`@columenlabs/lumen-ai`](https://www.npmjs.com/package/@columenlabs/lumen-ai) | Serving an MCP server from a Worker, read-only tool sets and metered tool calls; later a product-neutral assistant | Published | | [`packages/tasks`](https://github.com/columenlabs/lumen/blob/main/packages/tasks) | [`@columenlabs/lumen-tasks`](https://www.npmjs.com/package/@columenlabs/lumen-tasks) | Background jobs: a ledger of the work a person started (state, progress, result), cooperative cancel, a reaper, and the routes the header Tasks list calls | Published | | [`packages/person`](https://github.com/columenlabs/lumen/blob/main/packages/person) | [`@columenlabs/lumen-person`](https://www.npmjs.com/package/@columenlabs/lumen-person) | One store per person: a Durable Object holding every app's tables for that person, migrations that fail per app, one shared alarm, and backup, restore and delete per app or whole | Published | | [`packages/vault`](https://github.com/columenlabs/lumen/blob/main/packages/vault) | `@columenlabs/lumen-vault` | Per-person sealing: an envelope codec and a regional codec, both byte for byte with what apps store today, and unlock events (the per-person Durable Object is `lumen-person`) | Published only after the owner approves each release (see its README) | A planned package has no folder until its first code moves in. Each package's own README says what it exports and how to use it. ### What may join A part becomes a Lumen package only when all four hold: 1. Two or more apps run it. 2. It names no product (the kit is neutral: apps pass their names and data in through `configureLumen` and `registerDomains`). 3. It moves from working code in an app, never written fresh here. 4. It owns no app's data: it gives the shapes and the machinery, the app keeps its rows. ## For coding agents - [`llms.txt`](https://lumen.columenlabs.com/llms.txt): what Lumen is, each package, and links to the rest. - [`llms-full.txt`](https://lumen.columenlabs.com/llms-full.txt): every README plus every component's props and an example, in one file. - [Component manifest](https://lumen.columenlabs.com/docs/manifests/components.json) (JSON). `bun run --filter=lumen-docs dev` also serves Storybook's MCP at `http://localhost:6006/mcp`. ## The UI kit at a glance | | Part | What it does | |---|---|---| | πŸͺŸ | **App shell** | `LumenShell`: sidebar (icons-only collapse, a drawer on phone), header, status bar, Settings, profile menu and ⌘K Find | | 🧱 | **Blocks** | `LumenLayout` and `LumenGrid` (a page of cards you can rearrange), `LumenWidget` (a card with its header, link square and controls), sign-in and Security, Settings sections, tabs | | πŸ“Š | **Views and charts** | `LumenView` and `LumenTable` (filter, group, pivot, totals), `LumenCalendar`, `LumenBoard`, `LumenCard`, `LumenChart`, `LumenEChart`, price and growth charts | | πŸŽ›οΈ | **Pickers and panels** | `LumenSelector`, `LumenDrawer`, `LumenConfirm`, `LumenForm`, `LumenDateInput`, `LumenTabs`, `LumenPlayer` | | 🧩 | **Primitives** | The shadcn/ui parts under `ui/*` and `theme.css` | Install and use: [packages/ui/README.md](https://github.com/columenlabs/lumen/blob/main/packages/ui/README.md). ### Pages people can rearrange Give `LumenLayout` an `onConfigChange` and every page can be changed by the person using it: **Edit layout** moves and sizes the widgets on a 6-column grid, and each widget's **Edit widget** changes its type, fields and controls. Both write back into the view config. Try both live on the docs' [Edit layout](https://lumen.columenlabs.com/docs/?path=/docs/blocks-edit-layout--docs) and [Edit widget](https://lumen.columenlabs.com/docs/?path=/docs/blocks-edit-widget--docs) pages. > **Status: 0.x.** Any minor version may break things until 1.0. ## Develop ```bash bun install bun run lint && bun run type-check && bun run build # build writes out/ and docs/dist bun run --filter=lumen-docs dev # the docs at http://localhost:6006 ``` The docs site (`docs/`) is Storybook over the kit's source plus the files agents read. `docs/wrangler.jsonc` serves `docs/dist` at lumen.columenlabs.com: | Path | What | |---|---| | `/docs/` | Storybook: every component on sample data, with Controls and Show code | | `/llms.txt` | The index for agents ([llmstxt.org](https://llmstxt.org) format) | | `/llms-full.txt` | Every README and every component's props in one file | | `/.md` | Each package's README (`/ui.md`, `/ui-types.md`, `/ship.md`, `/auth.md`, `/ops.md`, `/ai.md`) | All of it is built from the READMEs and the stories (`docs/scripts/llms.ts`), so nothing is written twice. Add or change a story in `docs/stories/` with the part, and keep each package's README current with its exports. To try a change inside an app before it is published, use `bun link` in `out/` and `bun link @columenlabs/lumen-ui` in the app. ## Releases Every merge to `main` publishes every package at the next patch version and tags it ([publish.yml](https://github.com/columenlabs/lumen/blob/main/.github/workflows/publish.yml)), with notes on what changed in each release: see the [releases](https://github.com/columenlabs/lumen/releases). A new minor is cut only by hand (Run workflow with bump = minor). ## License MIT, see [LICENSE](https://github.com/columenlabs/lumen/blob/main/LICENSE). Third-party notices are in [NOTICE](https://github.com/columenlabs/lumen/blob/main/NOTICE). # How to use: Lumen parts: how to use them One page per part an app builds screens from. Read the page before you add or change one of these in an app; a code reviewer checks the screen against it. The patterns behind them are in [`DESIGN.md`](https://github.com/columenlabs/lumen/blob/main/DESIGN.md), the blocking list in [`REVIEW.md`](https://github.com/columenlabs/lumen/blob/main/REVIEW.md). Each page has the same four headings: - **Use it for**: what the part is for, and what to reach for instead. - **Anatomy**: what the part draws, top to bottom. The app passes data into these slots and draws nothing around them. - **Do / Don't**: βœ“ and βœ— code, the mistakes agents make most. - **Checked by**: the `lumen-ship check` rule that fails CI when the page is broken, where one exists. | Part | Page | Use it for | |---|---|---| | `LumenDrawer` + `DrawerSection` | [drawer.md](https://github.com/columenlabs/lumen/blob/main/docs/parts/drawer.md) | The details or editor of one row, beside the list | | `LumenForm`, `SaveButton`, `FormIssue` | [form.md](https://github.com/columenlabs/lumen/blob/main/docs/parts/form.md) | Fields that save together | | `LumenSettings`, `LumenExpandList` | [settings.md](https://github.com/columenlabs/lumen/blob/main/docs/parts/settings.md) | An app's Settings page | | `LumenShell bar`, `useStatus` | [status-bar.md](https://github.com/columenlabs/lumen/blob/main/docs/parts/status-bar.md) | What is true right now, nobody has to act on | | `LumenSelector` | [selector.md](https://github.com/columenlabs/lumen/blob/main/docs/parts/selector.md) | Every picker | | `LumenView`, `LumenLayout` | [table.md](https://github.com/columenlabs/lumen/blob/main/docs/parts/table.md) | Every list of rows | | `LumenFilterPanel` | [filter-panel.md](https://github.com/columenlabs/lumen/blob/main/docs/parts/filter-panel.md) | Browsing a big list by its fields' values, beside it | | `LumenCalendar` | [calendar.md](https://github.com/columenlabs/lumen/blob/main/docs/parts/calendar.md) | Things on days: month, week and agenda | | `LumenBoard` | [board.md](https://github.com/columenlabs/lumen/blob/main/docs/parts/board.md) | Items by stage, in columns | | `LumenPlayer` | [player.md](https://github.com/columenlabs/lumen/blob/main/docs/parts/player.md) | A video, an audio file, or slides over audio | | `LumenShell tabs` | [tab-bar.md](https://github.com/columenlabs/lumen/blob/main/docs/parts/tab-bar.md) | The main pages at the bottom of a phone screen | ## Three rules every page shares 1. **Icon first.** An action is an `IconButton`: the icon, its name in a tooltip and its `aria-label`, no text on the button. Save is `SaveButton`, Run, Test, Refresh, Copy, Edit and Delete are `IconButton`s. ([DESIGN.md, Actions: icon first](https://github.com/columenlabs/lumen/blob/main/DESIGN.md#actions-icon-first)) 2. **Details on demand.** No grey hint line under a field, a section title or a page title. Most labels need nothing; the few that do get an `InfoTip`. ([DESIGN.md, Details on demand](https://github.com/columenlabs/lumen/blob/main/DESIGN.md#details-on-demand-infotip)) 3. **Only the kit's overlays.** An app never imports `ui/sheet`, `ui/dialog`, `ui/select`, `ui/command` or `ui/combobox`: drawers are `LumenDrawer`, confirms `LumenConfirm`, pickers `LumenSelector`. ([DESIGN.md, Popups](https://github.com/columenlabs/lumen/blob/main/DESIGN.md#popups-always-a-way-out)) ## Checked by `lumen-ship check parts ` counts each rule's breaks per folder against a baseline that may only fall (`scripts/parts.baseline`, one ` ` a line). Rules: | Rule | Fails on | |---|---| | `hint-text` | `

`: a grey line under a field or title | | `text-action` | a `)} ``` - βœ— Notes or a description on the card. βœ“ One line; the rest is in the drawer. - βœ— A drag-and-drop library for moves. βœ“ `onMove`; the Move to menu works by keyboard and touch. - βœ— Columns that stretch the page wider than the screen. βœ“ The board scrolls inside itself. ## Checked by Review and the app's interaction smoke (AGENTS.md, UI gate 1 and 3: reach and lines). # How to use: Calendar: `LumenCalendar` ## Use it for Things that happen on days: a program's sessions, a plan's tasks by due date, a schedule. Month, Week (with a time axis) and Agenda, over plain data the app passes in. - Rows to sort, filter and total: `LumenView` (see [table.md](https://github.com/columenlabs/lumen/blob/main/docs/parts/table.md)), with a date column. - One date to pick in a form: `LumenDateInput`. - Things by stage rather than by day: `LumenBoard` (see [board.md](https://github.com/columenlabs/lumen/blob/main/docs/parts/board.md)). ## Anatomy ``` β–¦ Month β–€ Week ☰ Agenda βœ“ β€Ή β€Ί LumenTabs; Today, previous, next as IconButtons October 2026 [Talks] [Workshops] [Deadlines] the period (one line), category chips (ColorChip toggles) Sun Mon Tue Wed Thu Fri Sat weekdays from weekStartsOn 4 5 ● 9:00 Kickoff… … a cell: day, up to 3 one-line events, "+N more" Wed, Oct 7 Today the picked day's list (always on a narrow box) 9:00 AM ● Kickoff and goals Talks one line: time, dot, title (truncates), category ``` - `events`: `{ id, title, start, end?, allDay?, category?, color? }`. `start` and `end` are instants (`Date` or ISO string), or a plain `YYYY-MM-DD` for a whole day; a plain end is inclusive. - `categories`: `{ id, label, color }`. Their chips filter; an event takes its category's colour unless it has its own. - `onSelect(event)` opens the app's drawer; `onRangeChange({ start, end, view })` fires with the instants on screen, so the app reads only those days. - `timeZone` (an IANA zone, the browser's by default) is the zone every day and time is shown in. - View and date are uncontrolled (`defaultView`, `defaultDate`) or controlled (`view` + `onViewChange`, `date` + `onDateChange`). - On a box under 640px wide (a phone, a narrow drawer) Month shows dots with the picked day's list under the grid, and Agenda is the default view. Week's axis scrolls inside its own box and opens at the first event or 7 AM. ## Do / Don't ```tsx // βœ“ plain data in, the app's drawer out; fetch by the range on screen setOpen(e.id)} onRangeChange={({ start, end }) => setRange([start, end])} /> // βœ— a hand-made month grid, text buttons, times built from local Date methods

{days.map((d) =>
{d.getDate()}…
)}
``` - βœ— Wrapping an event's title onto a second line. βœ“ It truncates; the full title is its hover. - βœ— Loading every event the app has. βœ“ Read by `onRangeChange` and pass that range's events. - βœ— A second category filter of the app's own above it. βœ“ `categories`; the chips are the filter. - βœ— `h-full` on the calendar's card. βœ“ It is as tall as its content; Week scrolls inside itself. ## Checked by Review and the app's interaction smoke (AGENTS.md, UI gate 1 and 3: reach and lines). # How to use: Drawer: `LumenDrawer` + `DrawerSection` ## Use it for The details or the editor of one row, opened from a list, a card or a link. A right-side panel on desktop whose width drags (320px to 60%), a bottom sheet on a phone. - A list whose rows each open it: pass `dock=""`, so it sits beside the page and the next row swaps it in place. - A yes or no before something destructive: `LumenConfirm`, not a drawer. - A whole new screen of its own: a page (a route with a `LumenLayout`), not a drawer. ## Anatomy ``` β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ Title Γ— β”‚ title (one line, truncates before the Γ—) β”‚ description β”‚ one muted line, optional: what this is ("Active Β· Oct 16") β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€ β”‚ β–Έ βš™ Section 3 None β”‚ DrawerSection: chevron, icon, title, count, short note β”‚ summary while closed β”‚ summary: what it is set to, only while closed β”‚ β–Ύ βš™ Section β”‚ β”‚ fields / FactList / table β”‚ body: the section's own parts β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€ β”‚ 12 runs πŸ—‘ β–Ά βœ“ β”‚ footerStart Β· actions (data, drawn as icons) Β· footer: SaveButton β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ ``` - Every body is `DrawerSection`s. No heading, card or divider of the app's own between them. - Sections start folded; `defaultOpen` the one the person came for. - Label and value pairs are `FactList`. Fields are the kit's inputs with an `InfoTip` where one is truly needed. - The footer holds the drawer's actions, right-aligned: `actions` as data (`{ icon, label, onClick, busy, destructive }`), which the drawer draws as icon buttons in order, destructive first, then `SaveButton` in `footer`. With neither there is no footer; the Γ— closes it. ## Do / Don't ```tsx // βœ“ data in the kit's slots; actions are icons remove.mutate() }, { icon: PlayIcon, label: 'Run now', busy: run.isPending, onClick: () => run.mutate() }, ]} footer={ save.mutate()} />}> … … // βœ— text buttons, a hand-made header, a grey hint line

Page

The address read on each run.

``` - βœ— A `Sheet` or `Dialog` from `ui/*` for a side panel. βœ“ `LumenDrawer`. - βœ— Tabs at the top of a drawer to split its sections. βœ“ `DrawerSection`s that fold; `LumenTabs` only when the drawer shows two different things (a job and its runs). - βœ— `h-full` on a card inside it: the drawer scrolls, a card is as tall as its content. ## Checked by `hint-text`, `text-action`, `raw-overlay` (see [README](https://github.com/columenlabs/lumen/blob/main/docs/parts/README.md#checked-by)). # How to use: Filter panel: `LumenFilterPanel` ## Use it for A list big enough that people browse it by facets (company, tag, place, level, date): each field becomes a section of values with counts, ticked in place, beside the list. It is the filter box laid out as a panel. Both read and write one `useLumenFilter` state, so a tick is a chip in the box and a chip's Γ— unticks it in the panel. - A short list, or one or two fields: the filter box alone (`LumenTokenizedFilter`). - A choice that switches the whole page (All, Review, Pending): `LumenTabs`. ## Anatomy ``` desktop phone β”Œ Filters 3 β†Ί ⇀ ┐ [≑3] [chips…] [β–½ 3] [chips…] (filter icon, count) β”‚ Company (i) ⊘ β”‚ β”Œβ”€β”€β”€β”€ β–¬ ───────────┐ β”‚ β˜‘ Cloudflare 41 β”‚ β”‚ Filters Γ— β”‚ bottom sheet, β”‚ ☐ Apple 212 β”‚ β”‚ 3 on β†Ί β”‚ same body β”‚ 61 more β”‚ β”‚ Company (i) β”‚ β”‚ Fit (number) β”‚ β”‚ β˜‘ Cloudflare 41 β”‚ β”‚ [From] – [To] β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ ``` - **Counts** come from `useFacetCounts`, the box's own: a field counts the rows the other fields keep, so picking one value never hides its siblings. Over one page of a server's rows, pass the server's counts as `facets` (from the same find query that returns the page). The kit never counts a page as if it were every row. - **Uncounted**: when a server cannot count a field without reading every row (a word search over a big set), it sends that field's values with `counted: false`. The panel and the box list the values to tick and show no numbers. Never send a full scan's counts on a request to fill them in. - **Callbacks** (`onTokens`, `paging.onSort`, `groups.onGroup`) are heard when the value changes, never because the function is new, so an inline callback cannot loop (`useTell`). - **Hide** (⇀) folds the panel to the filter icon in the filter row. Whether it is open is kept per device, per page `id`. A phone starts closed and opens the sheet from that icon. - **⊘** on a field leaves its ticked values out instead of keeping them, like Exclude on a chip. - A field with more than six values shows the most common first, with "N more" (and a search past ten). "No " sits on top when some rows have no value. ## Do / Don't ```tsx // βœ“ on a page: the layout draws the panel, the icon and the sheet // βœ“ outside a layout: one state, both views const filter = useLumenFilter(rows, fields); const panel = useFilterPanel('stores'); // βœ— a second filter state for the panel, or checkbox lists of the app's own const [picked, setPicked] = useState([]); ``` - βœ— Counting a server's page in the browser. βœ“ `facets` from the server's find query. - βœ— A count over every row on each request. βœ“ Kept counts, a bounded read, or `counted: false`. - βœ— A grey hint line under a section. βœ“ `tips` puts it in an InfoTip. - βœ— Text buttons (Clear all, Apply). βœ“ Ticks apply at once; Clear and Hide are icons. ## Checked by `raw-overlay`, `hint-text` and `text-action` (see [README](https://github.com/columenlabs/lumen/blob/main/docs/parts/README.md#checked-by)). # How to use: Form: `LumenForm`, `SaveButton`, `FormIssue` ## Use it for Fields that save together: an editor in a drawer, a settings section, a new-thing form. - A flat list of fields: `LumenForm` with `fields` (data, one object per field) and `onSave`. - A form the app lays out itself (sections, a preview beside it): the kit's inputs, and `SaveButton` with the form's `FormIssue`s. - One value changed in place (a name, a cell): `EditInputCell` or `RenameField`, no form and no Save. ## Anatomy ``` Name [ Lisbon store ] label: one line, no hint under it API key (i) [ β€’β€’β€’β€’3f9a ] an InfoTip only where a reader would guess wrong Route [ Public internet β–Ύ ] select / multi: LumenSelector ⚠ 2 to fix βœ“ SaveButton: issues chip, then the icon ``` - Every rule a value must meet is a `check` on its field (`LumenForm`) or a `FormIssue` (your own layout). The person sees them only through `SaveButton`: the chip counts them, Save takes them to the first one. - `disabled` on `SaveButton` means only "nothing changed"; never use it to block a save that has problems. - A test of the settings (Test it, Send test) is an `IconButton` beside Save. Its result shows on screen, never in a tip. ## Do / Don't ```tsx // βœ“ fields as data; the kit draws labels, inputs and Save String(v).startsWith('https://') || 'Must start with https://' }, { id: 'key', label: 'API key', kind: 'secret', saved: 'β€’β€’β€’β€’3f9a', info: 'Read only. Made in the provider’s console.' }, ]} initial={job} onSave={(v) => save.mutate(v)} busy={save.isPending} /> // βœ“ own layout: issues to SaveButton, field ids to jump to const issues: FormIssue[] = url.startsWith('https://') ? [] : [{ where: 'Address', message: 'Must start with https://', target: 'job-url' }]; save.mutate()} /> // βœ— a silent disabled Save, a text button, a hint under the field

Must start with https://

``` - βœ— `hint` on a `LumenFormField` (deprecated; it now draws as an (i) too). βœ“ Leave it out, or `info`, which the form draws as an `InfoTip` beside the label. - βœ— An error message under a field before the person tries to save. βœ“ The `SaveButton` chip. - βœ— Cancel as a text button. βœ“ `onCancel` on `LumenForm` draws an Γ— icon labelled Cancel; in a drawer leave it out, the drawer's Γ— and Esc close it. ## Checked by `hint-text`, `text-action` (see [README](https://github.com/columenlabs/lumen/blob/main/docs/parts/README.md#checked-by)). # How to use: Player: `LumenPlayer` ## Use it for A recording to watch or listen to in a drawer or a page: a video, an audio file, or slides in step with audio. The browser's own `