shelving 1.213.0 → 1.214.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "shelving",
3
- "version": "1.213.0",
3
+ "version": "1.214.1",
4
4
  "author": "Dave Houlbrooke <dave@shax.com>",
5
5
  "repository": {
6
6
  "type": "git",
@@ -35,6 +35,7 @@
35
35
  "./cloudflare": "./cloudflare/index.js",
36
36
  "./db": "./db/index.js",
37
37
  "./error": "./error/index.js",
38
+ "./extract": "./extract/index.js",
38
39
  "./firestore/client": "./firestore/client/index.js",
39
40
  "./firestore/lite": "./firestore/lite/index.js",
40
41
  "./firestore/server": "./firestore/server/index.js",
package/ui/README.md ADDED
@@ -0,0 +1,83 @@
1
+ # ui
2
+
3
+ A React component library for building Shelving apps — forms, content, layout, routing, dialogs, and the documentation-site components, all in one place.
4
+
5
+ The `ui` module exists so an app never hand-rolls the same form field, card, or router twice. Every component picks up its look from shared CSS and exposes its variations as plain boolean props. You build a screen by composing these pieces, and reach for a custom-styled element only when nothing here fits.
6
+
7
+ `ui` is consumed as source — it ships `.tsx` and `.module.css` files and needs a bundler that understands CSS Modules and JSX. It is not part of the root `shelving` package; import it from `shelving/ui`.
8
+
9
+ ## How components work
10
+
11
+ A few conventions run through every component (see also the React Components section of `AGENTS.md`):
12
+
13
+ - **Variants, not CSS.** Visual options are boolean props — `<Button small primary>`, `<Section narrow>`. Each maps to a class in the component's CSS Module. You never pass `style` or raw `className`.
14
+ - **Composition.** Higher-level components — a `*Page`, a `*Card` — take their identity from library components like `Card`, `Section`, `Button`, and `Tag` rather than shipping their own styling.
15
+ - **Sentence case.** Titles, headings, and button labels capitalise only the first word.
16
+ - **Theming via CSS variables.** Colour and spacing come from CSS custom properties with fallback chains, so a theme is a small set of variable overrides.
17
+
18
+ ## Module map
19
+
20
+ ### Content
21
+
22
+ | Folder | What's inside |
23
+ |---|---|
24
+ | [block](/ui/block) | Block-level content — `Card`, `Section`, `Heading`, `Table`, `List`, `Prose`, `Figure`, `Flex` |
25
+ | [inline](/ui/inline) | Inline content — `Code`, `Strong`, `Emphasis`, `Link`, `Mark`, `Small` |
26
+ | [misc](/ui/misc) | Cross-cutting pieces — `Markup`, `Tag`, `Status`, `Loading`, `Color`, `Catcher`, `Mapper` |
27
+
28
+ ### Structure
29
+
30
+ | Folder | What's inside |
31
+ |---|---|
32
+ | [app](/ui/app) | The `<App>` root component |
33
+ | [page](/ui/page) | Document-level components — `<HTML>`, `<Head>`, `<Page>` |
34
+ | [layout](/ui/layout) | Page layouts — `SidebarLayout`, `CenteredLayout` |
35
+ | [router](/ui/router) | Client-side routing — `<Navigation>`, `<Router>` |
36
+
37
+ ### Interaction
38
+
39
+ | Folder | What's inside |
40
+ |---|---|
41
+ | [form](/ui/form) | Forms and inputs — `<Form>`, `<Field>`, typed inputs, `<Button>`, `FormStore` |
42
+ | [dialog](/ui/dialog) | `<Dialog>` and `<Modal>` overlays |
43
+ | [menu](/ui/menu) | `<Menu>` and `<MenuItem>` |
44
+ | [notice](/ui/notice) | Inline and global notices |
45
+ | [transition](/ui/transition) | CSS enter / leave transitions |
46
+
47
+ ### Documentation site
48
+
49
+ | Folder | What's inside |
50
+ |---|---|
51
+ | [tree](/ui/tree) | `<TreeApp>` and the components that turn a tree into a site |
52
+ | [docs](/ui/docs) | Page and card renderers for directories, files, and code symbols |
53
+ | [util](/ui/util) | UI helper functions — context, meta, CSS class composition |
54
+
55
+ ## Quick start
56
+
57
+ A minimal single-screen app:
58
+
59
+ ```tsx
60
+ import { App, CenteredLayout, Section, Heading, Paragraph } from "shelving/ui";
61
+
62
+ function HelloApp() {
63
+ return (
64
+ <App app="My app">
65
+ <CenteredLayout>
66
+ <Section narrow>
67
+ <Heading>Hello</Heading>
68
+ <Paragraph>Welcome to the app.</Paragraph>
69
+ </Section>
70
+ </CenteredLayout>
71
+ </App>
72
+ );
73
+ }
74
+ ```
75
+
76
+ For a routed, multi-page app, wrap the tree in [`<Navigation>` and `<Router>`](/ui/router). For a documentation site, hand an extracted tree to [`<TreeApp>`](/ui/tree) — see the [extract](/extract) guide.
77
+
78
+ ## See also
79
+
80
+ - [extract](/extract) — builds the tree that the documentation components render
81
+ - [markup](/markup) — Markdown rendering used by `<Markup>` and `<Prose>`
82
+ - [store](/store) — reactive state behind `FormStore`, `NavigationStore`, and notices
83
+ - [react](/react) — store and provider hooks used alongside these components
@@ -0,0 +1,32 @@
1
+ # App
2
+
3
+ Root component for a client-side Shelving app. `<App>` applies the theme CSS class to `document.body` and provides a `Meta` context so every descendant can read or update page metadata.
4
+
5
+ Use `<App>` when mounting into an existing HTML page on the client. For server-side rendering where you need the full `<html>` document shell, use [`<HTML>`](/ui/page) instead.
6
+
7
+ ## Usage
8
+
9
+ ```tsx
10
+ import { App, Navigation, Router } from "shelving/ui";
11
+
12
+ export function MyApp() {
13
+ return (
14
+ <App app="My App" root="https://example.com/" url="/">
15
+ <Navigation>
16
+ <Router routes={{
17
+ "/": HomePage,
18
+ "/about": AboutPage,
19
+ }} />
20
+ </Navigation>
21
+ </App>
22
+ );
23
+ }
24
+ ```
25
+
26
+ `<App>` accepts all `PossibleMeta` props (`app`, `root`, `url`, `title`, `language`, `tags`, etc.) and merges them into the context it provides to children. On mount it adds the theme class to `document.body`, which activates the CSS custom property tokens defined in `App.module.css`; on unmount it removes it.
27
+
28
+ ## See also
29
+
30
+ - [`ui/page`](/ui/page) — `<HTML>` and `<Page>` for the document shell and per-page metadata
31
+ - [`ui/layout`](/ui/layout) — `SidebarLayout` and `CenteredLayout`
32
+ - [`ui/router`](/ui/router) — `<Navigation>` and `<Router>` for client-side routing
@@ -11,7 +11,7 @@
11
11
  color: var(--preformatted-color-text, var(--color-text));
12
12
  background-color: var(--preformatted-color-bg, var(--color-surface));
13
13
  border: var(--preformatted-border, var(--stroke-normal)) solid var(--preformatted-color-border, var(--color-surface));
14
- border-radius: var(--preformatted-radius, var(--radius-xsmall));
14
+ border-radius: var(--preformatted-radius, var(--radius-normal));
15
15
  tab-size: 2;
16
16
 
17
17
  /* Wrap long lines by default — newlines and indentation within wrapped lines are still preserved. */
@@ -0,0 +1,115 @@
1
+ # Block content
2
+
3
+ Block-level components for structuring page content. Every component here maps to a semantic HTML element and applies consistent spacing, typography, and layout from the design system's CSS modules.
4
+
5
+ ## Concepts
6
+
7
+ ### Semantic HTML, no custom markup
8
+
9
+ Each component is a thin styled wrapper around a single HTML element: `<Card>` renders `<article>`, `<Section>` renders `<section>`, `<Table>` renders `<table>`, and so on. Pick the component whose HTML element fits the semantic meaning — do not reach for a `<Block>` or `<Flex>` when a `<Section>` or `<List>` is the right choice.
10
+
11
+ `Section.tsx` exports all the standard landmark elements under the same variants (`narrow`, `wide`, `spacious`): `Section`, `Header`, `Footer`, `Nav`, and `Aside`.
12
+
13
+ ### Width and layout variants
14
+
15
+ Several layout components accept `narrow` and `wide` boolean props that constrain content width within the parent. `<Flex>` has richer layout control with `column`, `wrap`, `left`, `center`, `right`, `flush`, and `reverse` variants.
16
+
17
+ ### `<Prose>` for longform text
18
+
19
+ Wrap any block of mixed HTML content (paragraphs, lists, headings, code, tables) in `<Prose>` to apply cohesive longform typography in one step. All the inline and block component styles are applied as a compound class, so raw HTML elements produced by a markdown renderer just work.
20
+
21
+ ### Compound components
22
+
23
+ Some block components ship multiple pieces intended to compose:
24
+
25
+ - `Video` + `VideoButtons` + `VideoButton` + `FullscreenVideoButton`
26
+ - `Definitions` + `Definition` (term/value pairs inside a `<dl>`)
27
+ - `Address`, `PhysicalAddress`, `EmailAddress`
28
+
29
+ ## Canonical usage examples
30
+
31
+ ### Content card with a heading
32
+
33
+ ```tsx
34
+ import { Card, Heading, Paragraph } from "shelving/ui";
35
+
36
+ <Card href="/products/42" title="Open product">
37
+ <Heading level={2}>Widget Pro</Heading>
38
+ <Paragraph>The best widget on the market.</Paragraph>
39
+ </Card>
40
+ ```
41
+
42
+ `href` turns the card into a navigable overlay. Real interactive elements inside (like inline `<Link>` components) stay clickable via `position: relative; z-index: 2` rules in the stylesheet.
43
+
44
+ ### Structured page section
45
+
46
+ ```tsx
47
+ import { Section, Heading, Definitions, Definition } from "shelving/ui";
48
+
49
+ <Section narrow>
50
+ <Heading>Account details</Heading>
51
+ <Definitions row>
52
+ <Definition term="Name">Alice Smith</Definition>
53
+ <Definition term="Email">alice@example.com</Definition>
54
+ <Definition term="Plan">Pro</Definition>
55
+ </Definitions>
56
+ </Section>
57
+ ```
58
+
59
+ `<Subheading>` uses the same `level` prop as `<Heading>` but applies secondary typography styles — use it for in-section labels and panel titles.
60
+
61
+ ### Prose content from a renderer
62
+
63
+ ```tsx
64
+ import { Prose, Markup } from "shelving/ui";
65
+
66
+ <Prose>
67
+ <Markup>{article.body}</Markup>
68
+ </Prose>
69
+ ```
70
+
71
+ Wrap `<Markup>` (or any component that emits raw HTML elements) in `<Prose>` to apply consistent longform typography.
72
+
73
+ ### Flex row of cards
74
+
75
+ ```tsx
76
+ import { Flex, Card, Heading } from "shelving/ui";
77
+
78
+ <Flex wrap>
79
+ {products.map(p => (
80
+ <Card key={p.id} href={`/products/${p.id}`}>
81
+ <Heading level={3}>{p.name}</Heading>
82
+ </Card>
83
+ ))}
84
+ </Flex>
85
+ ```
86
+
87
+ ### Figure with caption
88
+
89
+ ```tsx
90
+ import { Figure, Image } from "shelving/ui";
91
+
92
+ <Figure caption="A golden retriever at the park">
93
+ <Image src="/images/dog.jpg" alt="Golden retriever" />
94
+ </Figure>
95
+ ```
96
+
97
+ ### Video with controls
98
+
99
+ ```tsx
100
+ import { Video, VideoButtons, FullscreenVideoButton } from "shelving/ui";
101
+
102
+ <Video wide>
103
+ <video src={stream.url} autoPlay muted playsInline />
104
+ <VideoButtons>
105
+ <FullscreenVideoButton />
106
+ </VideoButtons>
107
+ </Video>
108
+ ```
109
+
110
+ ## See also
111
+
112
+ - [ui/inline](/ui/inline) — inline-level text components used inside block content
113
+ - [ui/misc/Markup](/ui/misc) — renders a markup string into block and inline elements
114
+ - [ui/form](/ui/form) — interactive form elements and buttons
115
+ - [ui/layout](/ui/layout) — page-level layout structures
@@ -0,0 +1,80 @@
1
+ # Dialog
2
+
3
+ Overlay components for modals and imperative dialogs. Two approaches: mount a `<Dialog>` declaratively in the tree, or use `DialogsStore` to push dialogs imperatively from anywhere in the app.
4
+
5
+ ## Components
6
+
7
+ | Export | Purpose |
8
+ |---|---|
9
+ | `<Dialog>` | A native `<dialog>` element opened in modal mode. Closes on backdrop click, on links/buttons inside a `<nav>`, or via `<DialogCloseButton>`. |
10
+ | `<DialogCloseButton>` | A button that closes its nearest wrapping `<dialog>`. Renders an X icon by default. |
11
+ | `<DialogsContext>` | Creates a `DialogsStore` and provides it to descendants. |
12
+ | `<Dialogs>` | Renders the list of dialogs currently in the nearest `DialogsStore`. Mount once near the root of the app. |
13
+ | `DialogsStore` | An `ArrayStore<ReactElement>` with `.show(children)` and `.hideAll()`. Call `requireDialogs()` to get it from any component. |
14
+ | `<Modal>` | A non-blocking `<aside>` overlay for persistent panels (drawers, toasts, side-sheets) — not a `<dialog>`. |
15
+
16
+ ## Declarative usage
17
+
18
+ Mount `<Dialog>` directly when its lifetime matches a React state variable:
19
+
20
+ ```tsx
21
+ import { Dialog, DialogCloseButton } from "shelving/ui";
22
+
23
+ function ConfirmDelete({ onConfirm, onClose }: { onConfirm: () => void; onClose: () => void }) {
24
+ return (
25
+ <Dialog onClose={onClose}>
26
+ <p>Delete this item?</p>
27
+ <button type="button" onClick={onConfirm}>Delete</button>
28
+ <DialogCloseButton/>
29
+ </Dialog>
30
+ );
31
+ }
32
+
33
+ // In the parent:
34
+ {showConfirm && <ConfirmDelete onConfirm={handleDelete} onClose={() => setShowConfirm(false)}/>}
35
+ ```
36
+
37
+ ## Imperative usage with DialogsStore
38
+
39
+ Set up the context once near the app root, then call `.show()` from any component:
40
+
41
+ ```tsx
42
+ import { DialogsContext, Dialogs, requireDialogs } from "shelving/ui";
43
+
44
+ // App root — add <Dialogs> alongside your content.
45
+ function AppRoot() {
46
+ return (
47
+ <DialogsContext>
48
+ <AppContent/>
49
+ <Dialogs/>
50
+ </DialogsContext>
51
+ );
52
+ }
53
+
54
+ // Anywhere in the tree:
55
+ function DeleteButton({ id }: { id: string }) {
56
+ const dialogs = requireDialogs();
57
+ const open = () => dialogs.show(
58
+ <ConfirmDelete id={id} onConfirm={() => handleDelete(id)}/>
59
+ );
60
+ return <button type="button" onClick={open}>Delete</button>;
61
+ }
62
+ ```
63
+
64
+ `<Dialogs>` removes a dialog from the DOM 500 ms after it closes, giving CSS animations time to finish.
65
+
66
+ ## Modal
67
+
68
+ `<Modal>` is an `<aside>` element — use it for persistent overlays that coexist with the page rather than blocking interaction:
69
+
70
+ ```tsx
71
+ <Modal>
72
+ <NotificationPanel/>
73
+ </Modal>
74
+ ```
75
+
76
+ ## See also
77
+
78
+ - [`notice`](/ui/notice) — inline and global notices (toasts, banners)
79
+ - [`form`](/ui/form) — form components to put inside dialogs
80
+ - [`transition`](/ui/transition) — animate dialog enter / leave
@@ -0,0 +1,65 @@
1
+ # Documentation pages
2
+
3
+ Page and card renderers for the three tree element types. These are the defaults wired into [ui/tree](/ui/tree) — use them directly if you need a page or card outside the tree shell, or replace them via the `*Mapping` components.
4
+
5
+ ## Element types and their renderers
6
+
7
+ | Element type | Page renderer | Card renderer |
8
+ |---|---|---|
9
+ | `tree-directory` | `DirectoryPage` | `DirectoryCard` |
10
+ | `tree-file` | `FilePage` | `FileCard` |
11
+ | `tree-documentation` | `DocumentationPage` | `DocumentationCard` |
12
+
13
+ Each renderer receives the props of its element type (title, name, description, content, children, etc.). Card renderers also accept a `path` prop — the parent's URL path — so each card can compute its own `href` as `joinPath(path, name)`.
14
+
15
+ ## Pages
16
+
17
+ **`DirectoryPage`** renders the directory's title, its `README.md` prose via `<Markup>`, then its children as a `<TreeCards>` listing.
18
+
19
+ **`FilePage`** renders the file's title, its prose content, then its child code symbols as `<TreeCards>`. It reads the current URL pathname from context so each symbol card links to the right path.
20
+
21
+ **`DocumentationPage`** is the most detailed renderer. It renders: a `<DocumentationKind>` tag, type signatures as preformatted blocks, prose content, and conditional sections for parameters, returns, throws, and examples. Child symbols follow as cards.
22
+
23
+ ## Cards
24
+
25
+ Cards are compact link tiles used in `<TreeCards>` directory listings.
26
+
27
+ **`DirectoryCard`** and **`FileCard`** show the title and prose lead-in inside a `<Card>` linked to the element's path.
28
+
29
+ **`DocumentationCard`** shows the symbol name alongside its `<DocumentationKind>` tag, the first signature block, and a prose lead-in.
30
+
31
+ ## DocumentationKind
32
+
33
+ `<DocumentationKind>` renders a colour-coded `<Tag>` for a symbol's `kind` string. Built-in colour map:
34
+
35
+ | Kind | Colour |
36
+ |---|---|
37
+ | `function` | blue |
38
+ | `class` | purple |
39
+ | `interface` | cyan |
40
+ | `type` | pink |
41
+ | `constant` | green |
42
+ | `method` | orange |
43
+ | `property` | yellow |
44
+
45
+ Unknown kinds render as an uncoloured tag.
46
+
47
+ ## Usage
48
+
49
+ The renderers are used automatically via [ui/tree](/ui/tree). To render a single page or card manually, spread element props directly:
50
+
51
+ ```tsx
52
+ import { DirectoryPage, FilePage, DocumentationPage } from "shelving/ui";
53
+
54
+ <DirectoryPage {...directoryElement.props} />
55
+ <FilePage {...fileElement.props} />
56
+ <DocumentationPage {...documentationElement.props} />
57
+ ```
58
+
59
+ To replace a renderer for one element type across the whole site, use `<TreePageMapping>` or `<TreeCardMapping>` from [ui/tree](/ui/tree).
60
+
61
+ ## See also
62
+
63
+ - [ui/tree](/ui/tree) — `<TreeCards>` and the `*Mapping` override mechanism
64
+ - [extract](/extract) — produces the `TreeElement` tree whose props these renderers consume
65
+ - [markup](/markup) — renders the Markdown `content` field carried by each element
@@ -0,0 +1,165 @@
1
+ # Forms
2
+
3
+ Forms and inputs for shelving apps. Build validated, schema-driven forms from a handful of composable pieces, or drop in a single `<Form>` component for a fully automatic experience.
4
+
5
+ ## Concepts
6
+
7
+ ### FormStore — form state
8
+
9
+ `FormStore` is the brain of every form. It extends `DataStore` and owns:
10
+
11
+ - The current (partial) field values.
12
+ - A `messages` dictionary — error strings keyed by field name, plus a top-level `""` key for form-wide messages.
13
+ - A `validated` getter that runs the schema and returns fully-typed data or throws a string on failure.
14
+ - A `publish(name, value)` method that validates a single field, writes the result, and stores any per-field error.
15
+ - A `submit(callback)` method that validates the whole form and, if valid, runs the callback.
16
+
17
+ When `submit` calls the callback and the callback throws a plain **string**, `FormStore` parses it into field messages using the `"fieldName: message"` line format from [schema](/schema). Any non-string throw is surfaced as a global error notice. A successful return value is dispatched as a success notice.
18
+
19
+ You rarely create `FormStore` directly — `<Form>` does it for you — but you can grab it from context with `requireForm()` when you need it.
20
+
21
+ ### `<Form>` — the outer wrapper
22
+
23
+ `<Form>` creates a `FormStore` from a `schema` prop, wraps everything in an HTML `<form>`, disables the whole fieldset while busy, and calls `onSubmit` on a valid submit. If you provide no `children`, it renders `<FormFields>` (one auto-input per schema property) followed by `<FormFooter>` (submit button + error message).
24
+
25
+ ```tsx
26
+ import { Form } from "shelving/ui";
27
+ import { DATA, STRING, NumberSchema } from "shelving/schema";
28
+
29
+ const PRODUCT_SCHEMA = DATA({
30
+ name: new StringSchema({ title: "Name", min: 1, max: 100 }),
31
+ price: new NumberSchema({ title: "Price", min: 0 }),
32
+ });
33
+
34
+ export function NewProductForm() {
35
+ return (
36
+ <Form
37
+ schema={PRODUCT_SCHEMA}
38
+ submit="Create product"
39
+ onSubmit={async data => {
40
+ await createProduct(data);
41
+ return "Product created"; // dispatched as a success notice
42
+ }}
43
+ />
44
+ );
45
+ }
46
+ ```
47
+
48
+ Pass `data` to pre-populate an edit form. Pass `messages` (a string or dictionary) to seed initial field errors — useful when a server returns validation failures.
49
+
50
+ ### `<Field>` — label + input + error
51
+
52
+ `<Field>` is the visual wrapper for a single control. It renders a `<label>` with an optional title, description, and inline error message below the input. Use it when composing inputs by hand rather than relying on `<FormFields>`.
53
+
54
+ ```tsx
55
+ <Field title="Email address" message={emailError}>
56
+ <TextInput name="email" value={email} onValue={setEmail} required />
57
+ </Field>
58
+ ```
59
+
60
+ `<FormField name="…">` combines `<Field>` with `useField()` and `<SchemaInput>` in one step — it reads the schema, current value, and error state from the surrounding `FormContext` automatically.
61
+
62
+ ### Typed input components
63
+
64
+ Each input is a standalone, controlled component. All extend `ValueInputProps<O>`:
65
+
66
+ | Prop | Contract |
67
+ |---|---|
68
+ | `name` | HTML field name |
69
+ | `value` | Current value |
70
+ | `onValue(v)` | Called on every change |
71
+ | `required` | Marks the field as required |
72
+ | `disabled` | Disables the control |
73
+ | `message` | Inline error string |
74
+
75
+ The typed inputs are: `TextInput`, `NumberInput`, `DateInput`, `CheckboxInput`, `RadioInput`, `SelectInput`, `FileInput`, `ArrayInput`, `DictionaryInput`, and `DataInput`. `ArrayInput` and `DictionaryInput` both accept an `items` schema to render a repeatable list of sub-inputs with add/remove buttons.
76
+
77
+ ### `SchemaInput` / `DataInput` — schema-driven rendering
78
+
79
+ `SchemaInput` inspects a `Schema` instance and renders the right input automatically:
80
+
81
+ | Schema type | Rendered as |
82
+ |---|---|
83
+ | `StringSchema` | `<TextInput>` |
84
+ | `NumberSchema` | `<NumberInput>` (formatted on blur) |
85
+ | `DateSchema` | `<DateInput>` |
86
+ | `BooleanSchema` | `<CheckboxInput>` |
87
+ | `ChoiceSchema` (≤ 8 options) | `<ChoiceRadioInputs>` |
88
+ | `ChoiceSchema` (> 8 options) | `<SelectInput>` |
89
+ | `ArraySchema` | `<ArrayInput>` |
90
+ | `DictionarySchema` | `<DictionaryInput>` |
91
+ | `DataSchema` | `<DataInput>` |
92
+
93
+ `DataInput` renders a row of `SchemaInput` elements for each property of a nested data object, and propagates sub-field errors from a `"key: message\n…"` formatted `message` string.
94
+
95
+ ### `<Button>`, `<SubmitButton>`, `<Clickable>`
96
+
97
+ `<Clickable>` renders a `<button>` or an `<a>` depending on whether `onClick` or `href` is provided. It tracks its own busy state and shows a loading spinner when its `onClick` promise is pending. `<Button>` is `<Clickable>` with styling variants (`strong`, `plain`, `outline`, `small`, `primary`, `danger`, `success`, …). `<SubmitButton>` reads the surrounding `FormContext`, disables itself while the form is busy, and defaults to a "Save →" label.
98
+
99
+ ### Popovers and combo inputs
100
+
101
+ `<Popover>` is a layout primitive: its first child is the trigger, subsequent children appear in a floating panel when `open` is true. `<ButtonPopover>` wraps a `<Button>` trigger; `<ButtonInputPopover>` wraps a `<ButtonInput>` trigger styled to look like an input field. Use these to build date-pickers, tag selectors, and other inputs that need a dropdown panel.
102
+
103
+ `<QueryInput>` is a ready-made combo box: it renders a schema-driven text input and calls an async `onQuery` callback on each keystroke (debounced), showing results as a radio list in a popover.
104
+
105
+ ### Notices and error surfacing
106
+
107
+ When an `onSubmit` callback returns a non-empty `ReactNode`, `<Form>` dispatches it as a success notice. When it throws a **string**, the string is parsed into field messages — any line matching `"fieldName: message"` maps to that field's error display; an unmatched remainder appears as the form-wide message shown by `<FormMessage>`. Non-string throws become global error notices.
108
+
109
+ `<FormMessage>` renders the top-level `""` message inline as a `<Message>`. `<FormNotice>` renders it as a larger `<Notice>` block. `<FormNotify>` (no JSX output) forwards the message to the global notice system via a side effect instead.
110
+
111
+ ## Canonical end-to-end example
112
+
113
+ ```tsx
114
+ import { Form, Field, TextInput, NumberInput, CheckboxInput, SubmitButton, FormMessage } from "shelving/ui";
115
+ import { DATA, StringSchema, NumberSchema, BOOLEAN } from "shelving/schema";
116
+
117
+ const LISTING_SCHEMA = DATA({
118
+ title: new StringSchema({ title: "Title", min: 1, max: 80 }),
119
+ price: new NumberSchema({ title: "Price", min: 0 }),
120
+ published: BOOLEAN,
121
+ });
122
+
123
+ export function ListingForm({ listing }: { listing?: typeof LISTING_SCHEMA.type }) {
124
+ return (
125
+ <Form
126
+ schema={LISTING_SCHEMA}
127
+ data={listing}
128
+ submit={listing ? "Save changes" : "Publish listing"}
129
+ onSubmit={async data => {
130
+ await saveListing(data);
131
+ return listing ? "Listing updated" : "Listing published";
132
+ }}
133
+ />
134
+ );
135
+ }
136
+ ```
137
+
138
+ The default `<Form>` children (`<FormFields>` + `<FormFooter>`) handle layout automatically. Customise by providing explicit children when you need control over field order, groupings, or extra buttons:
139
+
140
+ ```tsx
141
+ <Form schema={LISTING_SCHEMA} data={listing} onSubmit={handleSubmit}>
142
+ <Field title="Title" required>
143
+ <FormInput name="title" />
144
+ </Field>
145
+ <Field title="Price">
146
+ <FormInput name="price" />
147
+ </Field>
148
+ <FormInput name="published" />
149
+ <footer>
150
+ <SubmitButton>Save changes</SubmitButton>
151
+ <Button plain onClick={onCancel}>Cancel</Button>
152
+ <FormMessage />
153
+ </footer>
154
+ </Form>
155
+ ```
156
+
157
+ `<FormInput name="…">` uses `useField()` to pull the current value, error, and schema from context, then delegates to `<SchemaInput>` — so each field automatically renders the correct control type.
158
+
159
+ ## See also
160
+
161
+ - [schema](/schema) — `DataSchema`, `StringSchema`, `NumberSchema`, `ChoiceSchema`, and other schema types that drive automatic input selection.
162
+ - [ui/form/FormStore](/ui/form/FormStore) — the state class underlying every form.
163
+ - [ui/form/SchemaInput](/ui/form/SchemaInput) — the schema-to-input dispatch component.
164
+ - [ui](/ui) — top-level UI module index.
165
+ - [react](/react) — `useStore`, `useInstance`, and other hooks used internally.
@@ -0,0 +1,86 @@
1
+ # Inline content
2
+
3
+ Inline-level components for annotating text. Every component here renders a single semantic HTML element — `<Strong>` renders `<strong>`, `<Code>` renders `<code>`, `<Mark>` renders `<mark>` — and applies the matching design-system styles.
4
+
5
+ ## Concepts
6
+
7
+ ### Semantic wrappers
8
+
9
+ These components exist so that text annotations always carry the correct HTML semantics and class names. Prefer them over raw HTML elements inside React components: `<Strong>` instead of `<strong>`, `<Link href="…">` instead of `<a href="…">`.
10
+
11
+ ### Code family
12
+
13
+ `Code.tsx` exports four components that all share the same monospace styling:
14
+
15
+ | Component | HTML element | Use for |
16
+ | ---------- | ------------ | ------------------------------- |
17
+ | `Code` | `<code>` | Inline code fragments |
18
+ | `Keyboard` | `<kbd>` | Keyboard input, e.g. `Ctrl+S` |
19
+ | `Sample` | `<samp>` | Program output |
20
+ | `Variable` | `<var>` | Variable names in documentation |
21
+
22
+ ### `<Link>` delegates to `<Clickable>`
23
+
24
+ `<Link>` renders an `<a>` when `href` is provided or a `<button>` when `onClick` is provided, via the shared `Clickable` helper. It handles busy state, URL resolution, and active-page highlighting automatically.
25
+
26
+ ### `<When>`, `<Ago>`, `<Until>` — time display
27
+
28
+ These components format a date as a compact relative string (`in 6d`, `3w ago`) and wrap it in a `<time>` element with a machine-readable `dateTime` attribute and a `title` showing the full date. Pass `full` to append the absolute date alongside the relative one.
29
+
30
+ - `When` — shows direction: `in 6d` or `3w ago`
31
+ - `Ago` — always backward-looking duration: `6d`
32
+ - `Until` — always forward-looking duration: `6d`
33
+
34
+ ## Canonical usage examples
35
+
36
+ ### Annotated paragraph
37
+
38
+ ```tsx
39
+ import { Strong, Emphasis, Mark, Keyboard, Code, Small } from "shelving/ui";
40
+
41
+ <p>
42
+ Press <Keyboard>Ctrl+S</Keyboard> to save. <Strong>Unsaved changes will be lost.</Strong>{" "}
43
+ Files are stored as <Emphasis>plain text</Emphasis> in{" "}
44
+ <Mark>UTF-8</Mark> encoding. <Small>Maximum 10 MB.</Small>
45
+ </p>
46
+ ```
47
+
48
+ Use `<Keyboard>` for key combinations, `<Code>` for inline code fragments, and `<Sample>` for program output — they share the same monospace appearance but carry different semantics.
49
+
50
+ ### Link inside body copy
51
+
52
+ ```tsx
53
+ import { Link } from "shelving/ui";
54
+
55
+ <p>
56
+ Read our <Link href="/privacy">privacy policy</Link> for details.
57
+ </p>
58
+ ```
59
+
60
+ ### Relative timestamp
61
+
62
+ ```tsx
63
+ import { When, Until } from "shelving/ui";
64
+
65
+ // "in 3d" or "5h ago" with full ISO date as title tooltip
66
+ <span>Last updated <When target={post.updatedAt} /></span>
67
+
68
+ // Just the forward-looking duration: "in 2w"
69
+ <span>Expires <Until target={subscription.expiresAt} /></span>
70
+ ```
71
+
72
+ ### Change tracking
73
+
74
+ ```tsx
75
+ import { Deleted, Inserted } from "shelving/ui";
76
+
77
+ <p>
78
+ Price: <Deleted>£9.99</Deleted> <Inserted>£7.99</Inserted>
79
+ </p>
80
+ ```
81
+
82
+ ## See also
83
+
84
+ - [ui/block](/ui/block) — block-level content components that inline components live inside
85
+ - [ui/block/Prose](/ui/block) — applies cohesive longform typography to a subtree of block and inline elements
86
+ - [ui/form/Clickable](/ui/form) — the underlying clickable primitive that `Link` delegates to