shelving 1.213.0 → 1.214.0
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 +2 -1
- package/ui/README.md +83 -0
- package/ui/app/README.md +32 -0
- package/ui/block/README.md +115 -0
- package/ui/dialog/README.md +80 -0
- package/ui/docs/README.md +65 -0
- package/ui/form/README.md +165 -0
- package/ui/inline/README.md +86 -0
- package/ui/layout/README.md +71 -0
- package/ui/menu/README.md +33 -0
- package/ui/misc/README.md +122 -0
- package/ui/notice/README.md +76 -22
- package/ui/page/README.md +56 -0
- package/ui/router/README.md +4 -4
- package/ui/transition/README.md +80 -0
- package/ui/tree/README.md +74 -0
- package/ui/util/README.md +140 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "shelving",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.214.0",
|
|
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
|
package/ui/app/README.md
ADDED
|
@@ -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
|
|
@@ -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
|