@hackcove/ui 0.2.2 → 0.2.4
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/README.md +183 -4
- package/dist/AppShell.d.ts +37 -0
- package/dist/AppShell.js +61 -0
- package/dist/Avatar.d.ts +9 -4
- package/dist/Avatar.js +9 -3
- package/dist/BulletList.d.ts +3 -0
- package/dist/BulletList.js +6 -0
- package/dist/Code.d.ts +3 -0
- package/dist/Code.js +6 -0
- package/dist/ColourSchemeToggle.d.ts +7 -0
- package/dist/ColourSchemeToggle.js +25 -0
- package/dist/Container.d.ts +12 -0
- package/dist/Container.js +9 -0
- package/dist/DateCalendar.d.ts +4 -0
- package/dist/DateCalendar.js +40 -0
- package/dist/DateField.d.ts +49 -0
- package/dist/DateField.js +77 -0
- package/dist/DatePicker.d.ts +8 -0
- package/dist/DatePicker.js +27 -0
- package/dist/Divider.d.ts +3 -0
- package/dist/Divider.js +6 -0
- package/dist/InlineLinks.d.ts +11 -0
- package/dist/InlineLinks.js +11 -0
- package/dist/Markdown.js +1 -1
- package/dist/PageHeader.d.ts +19 -0
- package/dist/PageHeader.js +11 -0
- package/dist/PageLayouts.d.ts +73 -0
- package/dist/PageLayouts.js +63 -0
- package/dist/Stack.d.ts +30 -0
- package/dist/Stack.js +28 -0
- package/dist/TextLink.d.ts +12 -0
- package/dist/TextLink.js +8 -0
- package/dist/Video.d.ts +10 -0
- package/dist/Video.js +10 -0
- package/dist/colourScheme.d.ts +12 -0
- package/dist/colourScheme.js +40 -0
- package/dist/index.d.ts +18 -1
- package/dist/index.js +16 -0
- package/dist/isoDate.d.ts +5 -0
- package/dist/isoDate.js +16 -0
- package/dist/shownDate.d.ts +45 -0
- package/dist/shownDate.js +88 -0
- package/dist/typography.d.ts +32 -24
- package/dist/typography.js +39 -9
- package/dist/ui.d.ts +1 -0
- package/dist/ui.js +3 -1
- package/package.json +24 -1
package/README.md
CHANGED
|
@@ -10,9 +10,12 @@ Status: `0.x`, published to npm. License: MIT.
|
|
|
10
10
|
|
|
11
11
|
```sh
|
|
12
12
|
bun add @hackcove/ui
|
|
13
|
-
# peers: react >= 18, tailwindcss ^4
|
|
13
|
+
# peers: react >= 18, react-dom >= 18, tailwindcss ^4
|
|
14
14
|
```
|
|
15
15
|
|
|
16
|
+
One runtime dependency comes with it: `react-aria-components` (and `@internationalized/date`),
|
|
17
|
+
for `DatePicker` and `DateField`. It brings no CSS; the classes are ours.
|
|
18
|
+
|
|
16
19
|
Tailwind skips `node_modules`, so tell it to scan the build for the utilities the components use,
|
|
17
20
|
in the stylesheet that imports Tailwind (the path is relative to that file):
|
|
18
21
|
|
|
@@ -36,11 +39,187 @@ export const Example = () => (
|
|
|
36
39
|
|
|
37
40
|
## What is exported
|
|
38
41
|
|
|
39
|
-
- Primitives: `Card`, `EpicHeader`, `Empty`, `PageTitle
|
|
42
|
+
- Primitives: `Card`, `EpicHeader`, `Empty`, `PageTitle` (deprecated: use `PageHeader`), `Pill`, `groupByEpic`, `Markdown`.
|
|
43
|
+
- Layout (see Layout): `AppShell`, `appShellRootClass`, `Container`, `PageHeader`, `Stack`, `Section`, and the page layouts `ListLayout`, `DetailLayout`, `FormLayout`, `ProseLayout`, `CardGrid`, `SidebarLayout`, `FullBleedLayout`, `CenteredLayout`.
|
|
40
44
|
- Typography roles (each pins size + weight, takes a `color` and an `as`): `Display`, `Lead`,
|
|
41
45
|
`PageHeading`, `SectionHeading`, `SubsectionHeading`, `ModalHeading`, `Subtitle`, `Overline`,
|
|
42
|
-
`Body`, `BodyStrong`, `Caption`.
|
|
43
|
-
|
|
46
|
+
`Body`, `BodyStrong`, `Caption`. Three opt-in props, none changing today's look: `SectionHeading rule`
|
|
47
|
+
(a hairline under it, with its spacing), `SubsectionHeading size` (`sm` default, `base` bold, `lg`),
|
|
48
|
+
`Body preserveNewlines` (keeps a note's line breaks).
|
|
49
|
+
- Controls: `Button`, `buttonClass` (the classes of a `Button`, for a router's own `<Link>`), `IconButton` (a square button holding one icon; `label` is required and becomes its `aria-label` and `title`), `ColourSchemeToggle` (a light/dark switch on `<html>`; see Light and dark), `Input` (a text input; `icon` adds a leading icon inside the field), `Textarea` (a multi-line text input, `rows` defaults to 3), `Select` (a native `<select>` in the control look; takes `<option>` children and keeps the browser's arrow). `Checkbox` (a native checkbox with its label; `className` goes on the label, other props on the input). `CopyButton` (a small text button that writes `text` to the clipboard and swaps `label` ("copy") for `copiedLabel` ("copied"); every other button prop, `disabled` included, goes on the `<button>`). `CopyLine` (one line of mono text in a box with a `CopyButton` beside it; other props go on the root `<div>`). `Field` (a label above a control, `htmlFor` names the control's id; a `hint` sits under it, and an `error` replaces the hint with a `role="alert"` line). `Callout` (a tinted notice box; `tone` is `neutral`, `info` (default), `positive`, `warn` or `danger`; an optional `title` sits in bold above the body). `NavTabs` (a row of pill links in a `<nav>`; `items` are `{ label, href, active? }`, `label` names the landmark, the active one is filled and gets `aria-current="page"`). `navTabClass` (the classes of one tab, for a router's own `<Link>`). `Breadcrumbs` (a `<nav aria-label="Breadcrumb">` over an ordered list; `crumbs` are nodes, the last is the current page and gets `aria-current="page"`, and each crumb truncates). `crumbLinkClass` (the classes of a crumb that links, for a router's own `<Link>`). `KeyValueList` (a `<dl>` of label/value rows; each `KeyValue` takes a `label` and its value as children, the value right-aligned in tabular figures). `Avatar` (a round badge of the initials of `name`, titled with the name and `aria-hidden`; with `src` (the app's own local image path, never a remote one) it is a round, cropped `<img alt={name}>` instead; `size` is `sm`, `md` (default), `lg`, `xl` (56px) or `2xl` (96px); `ring` adds a page-colour border and a negative margin for an overlapping stack; the colour is one of seven stock background classes picked by a hash of the name, remapped by each theme, and `avatarColourClass(name)` returns it to colour something else the same way; `initialsOf(name)` is the two letters). `TextLink` (an inline text link in the accent colour; `href` is required, `external` adds `target="_blank"` and `rel="noreferrer"`, `className` and every other anchor prop pass through). `linkClass` (the classes of one, for a router's own `<Link>`). `InlineLinks` (a wrapping row of small links joined by an `aria-hidden` `separator`: `·` (default), `•` or `none`; `className` goes on the row). `Divider` (a hairline `<hr>` in the theme's line colour; spacing is the caller's, on `className`). `BulletList` (a `<ul>` with discs, an indent and a small gap; children are `<li>`s). `Code` (inline code: mono, on a subtle background). `Video` (a full-width, rounded `<video>` with controls that loads metadata only; `src` is required, `poster` and a WebVTT `captions` track are optional, children are the fallback). `Kbd` (a keyboard key in mono; its border takes the surrounding text colour, so it reads on a filled button too). `ToggleChip` (an on/off filter pill; `pressed` sets `aria-pressed` and the filled look, `dot` adds a leading colour dot, and the caller toggles it in `onClick`). `FilterGroup` (a `<fieldset>` with a small uppercase `legend` over a wrapped row of its children, e.g. `ToggleChip`s; `className` goes on the fieldset). `DatePicker` and `DateField` (see Dates).
|
|
50
|
+
- Dates: `toISODate`, `fromISODate`, and the `CalendarDate` type.
|
|
51
|
+
|
|
52
|
+
## Dates
|
|
53
|
+
|
|
54
|
+
```tsx
|
|
55
|
+
import { DatePicker, fromISODate, toISODate, type CalendarDate } from "@hackcove/ui";
|
|
56
|
+
|
|
57
|
+
<DatePicker
|
|
58
|
+
label="Date of birth"
|
|
59
|
+
description="DD/MM/YYYY"
|
|
60
|
+
defaultValue={fromISODate("1990-05-17")}
|
|
61
|
+
maxValue={fromISODate("2026-10-23")}
|
|
62
|
+
onChange={(date: CalendarDate | null) => save(toISODate(date))}
|
|
63
|
+
name="dob"
|
|
64
|
+
isRequired
|
|
65
|
+
/>;
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
- `DatePicker` is a segmented DD/MM/YYYY field (always en-GB, whatever the browser's locale) with a
|
|
69
|
+
calendar button. The popover has ‹ › month steps, month and year dropdowns, Monday-first weeks
|
|
70
|
+
and a Clear action. `defaultOpen` opens it on mount.
|
|
71
|
+
- `DateField` is the same field without the calendar.
|
|
72
|
+
- Both take `label`, `description`, `errorMessage`, `value` / `defaultValue` / `onChange`,
|
|
73
|
+
`minValue`, `maxValue`, `isDisabled`, `isRequired` and `name` (the form posts `YYYY-MM-DD`).
|
|
74
|
+
Values are `CalendarDate`s (from `@internationalized/date`, re-exported as a type).
|
|
75
|
+
- `toISODate(date)` gives `"1990-05-17"` (`""` for `null`); `fromISODate(text)` gives a
|
|
76
|
+
`CalendarDate`, or `null` for a malformed or impossible date such as `"1990-02-31"`.
|
|
77
|
+
- `onChange` reports what the field shows: the date once it is complete, exists and is within
|
|
78
|
+
`minValue` / `maxValue`. When a valid date is edited into one that is not, it reports `null` once
|
|
79
|
+
and `name` posts `""`, so a controlled parent holds `null` until the date is fixed (holding it does
|
|
80
|
+
not clear what was typed). Fixing it reports the date again. A `value` the parent sets is shown,
|
|
81
|
+
not reported back.
|
|
82
|
+
- With `isRequired`, a partly filled date blocks the form and shows "Enter the whole date." once
|
|
83
|
+
the field loses focus.
|
|
84
|
+
- A date before `minValue` or after `maxValue` stays as typed (it is never clamped) and shows the
|
|
85
|
+
error once the field loses focus.
|
|
86
|
+
- An impossible date such as 31/02/1990 shows "February 1990 has 28 days." and stays exactly as
|
|
87
|
+
typed, focus leaving included, until it is fixed. `errorMessage` replaces either message.
|
|
88
|
+
|
|
89
|
+
## Layout
|
|
90
|
+
|
|
91
|
+
One content edge for every page, so nothing moves sideways between pages or viewport sizes.
|
|
92
|
+
|
|
93
|
+
```tsx
|
|
94
|
+
import { AppShell, PageHeader, Section, Stack, appShellRootClass } from "@hackcove/ui";
|
|
95
|
+
|
|
96
|
+
// Your root layout. `appShellRootClass` is `overflow-y-scroll`: it reserves the scrollbar, so a
|
|
97
|
+
// short page and a long one centre their content at the same x. AppShell cannot set it itself.
|
|
98
|
+
<html className={appShellRootClass}>
|
|
99
|
+
<body>
|
|
100
|
+
<AppShell brand={<Logo />} nav={<NavTabs label="Main" items={nav} />} footer="…">
|
|
101
|
+
<PageHeader title="Builds" subtitle="Every build" actions={<Button>New</Button>} />
|
|
102
|
+
<Stack>
|
|
103
|
+
<Section title="Recent">…</Section>
|
|
104
|
+
</Stack>
|
|
105
|
+
</AppShell>
|
|
106
|
+
</body>
|
|
107
|
+
</html>;
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
- **`AppShell`** is the page frame: a sticky header that is always one `h-14` row (a nav that does
|
|
111
|
+
not fit scrolls sideways, it never wraps, and the edge with more to scroll to fades out until it
|
|
112
|
+
is reached), an optional `subnav` second row (same rule), `main`
|
|
113
|
+
with fixed `py-8`, and an optional `footer`. The header row is filled by slots, left to right:
|
|
114
|
+
`brand` (logo and name, at the brand type with the logo gap), `nav` (a `NavTabs`), `header` (a
|
|
115
|
+
free node, the escape hatch) and `controls` (end-aligned, `gap-2`, centred): no wrapper, no
|
|
116
|
+
`className`. `hideChromeInPrint` is opt-in: the header and footer are hidden in print and main
|
|
117
|
+
drops its padding (the page margin is then your `@page`); without it a print is unchanged. `bleed` drops main's `Container` for a full-bleed
|
|
118
|
+
page; the header keeps its own, so put a `Container` back around anything that should line up.
|
|
119
|
+
- **`Container`** is the only place the content width (`max-w-6xl`) and side padding
|
|
120
|
+
(`px-4 sm:px-6`) are defined. **Pages never set their own width or outer padding**: no `max-w-*`,
|
|
121
|
+
no `mx-auto`, no `px-*` on a page. AppShell already gives them the edge.
|
|
122
|
+
- **`PageHeader`** is the top of every page: optional `breadcrumbs`, the `title` (a `PageHeading`),
|
|
123
|
+
`subtitle`, `actions` (right of the title from `sm`, under it on a phone) and `tabs` under it. Its
|
|
124
|
+
fixed bottom margin puts every page's first block at the same y.
|
|
125
|
+
- **`Stack`** spaces blocks in a column: `gap="xs"` (`gap-2`) between lines of one text block (a
|
|
126
|
+
heading, the line under it), `sm` (`gap-4`) inside a group, `md` (`gap-6`, the default) between
|
|
127
|
+
blocks, `lg` (`gap-8`) between groups. Use it instead of `space-y-*`. `width` caps the column:
|
|
128
|
+
`narrow` (`max-w-3xl`, a reading measure), `wide` (`max-w-5xl`), `full` (`w-full`) or `default`
|
|
129
|
+
(nothing). A capped Stack starts at the Container's left edge and is never re-centred.
|
|
130
|
+
**`Section`** is a `<section>` Stack (`sm` by default) with an optional `SectionHeading` `title`,
|
|
131
|
+
`actions` beside it and the same `width`.
|
|
132
|
+
|
|
133
|
+
### Page layouts
|
|
134
|
+
|
|
135
|
+
Small compositions of the above: they take slots, own no data, and set no page width or outer
|
|
136
|
+
padding. Each takes the `PageHeader` props (`title`, `subtitle`, `actions`, `breadcrumbs`, `tabs`)
|
|
137
|
+
except the last four.
|
|
138
|
+
|
|
139
|
+
- **`ListLayout`**: use this when a page is a filterable list. `filters` sits under the header;
|
|
140
|
+
`children` is the list, replaced by `empty` when `isEmpty`.
|
|
141
|
+
- **`DetailLayout`**: use this when a page is one thing with facts beside it. `children` take two
|
|
142
|
+
thirds from `lg` and `aside` one third; below `lg` the aside stacks under the main column.
|
|
143
|
+
- **`FormLayout`**: use this for a create or edit page. A `max-w-3xl` `<form>` that starts at the
|
|
144
|
+
Container's left edge (it is not re-centred, so the title does not jump), with an `actions` row at
|
|
145
|
+
the end. Other props (`onSubmit`, `action`) go on the `<form>`.
|
|
146
|
+
- **`ProseLayout`**: use this for a text page (privacy, terms, a CV, an about page). One `narrow`
|
|
147
|
+
(`max-w-3xl`) column that starts at the Container's left edge (not re-centred, so the title does
|
|
148
|
+
not jump): the header, then `children` in a `Stack` (`gap` prop, `md` by default). Unlike
|
|
149
|
+
`FormLayout` the header sits inside the column, so its `actions` stay beside the text.
|
|
150
|
+
`className` goes on the column.
|
|
151
|
+
- **`CardGrid`**: use this for stat tiles or a gallery of cards. `columns` is `2`, `3` (default) or
|
|
152
|
+
`4`: one column on a phone, two from `sm`, `columns` from `lg`.
|
|
153
|
+
- **`SidebarLayout`**: use this when a `rail` of navigation sits beside the content. Render it in
|
|
154
|
+
`<AppShell bleed>`: it fills the height under the header with no header height written down
|
|
155
|
+
(`AppShell`'s main is a flex column). The rail is `md:w-60` and stacks above the canvas below `md`;
|
|
156
|
+
the canvas keeps the Container's padding.
|
|
157
|
+
- **`FullBleedLayout`**: use this for a viewer that needs the whole width. Render it in
|
|
158
|
+
`<AppShell bleed>`; `children` fill the width, and an optional `panel` takes `lg:w-72` on the
|
|
159
|
+
right and stacks below under `lg`.
|
|
160
|
+
|
|
161
|
+
**`SidebarLayout` and `FullBleedLayout` deliberately break the shared left edge from `md` and
|
|
162
|
+
`lg` up.** A full-height rail and an edge-to-edge viewer need the viewport's edges, so they cannot
|
|
163
|
+
sit inside the Container the header uses. At 1280 px the header logo and every other layout start
|
|
164
|
+
at x=88, `SidebarLayout`'s rail at x=0 and its canvas at x=264 (rail 240 + the Container's 24
|
|
165
|
+
padding), `FullBleedLayout`'s `children` at x=0 (they add their own padding). Below `md` (`lg` for
|
|
166
|
+
`FullBleedLayout`) everything stacks and all layouts agree at the Container's padding. To avoid
|
|
167
|
+
the shift between pages, give a whole section of an app the same layout (every page under a
|
|
168
|
+
project on `SidebarLayout`) rather than mixing it with `ListLayout` pages that a user moves
|
|
169
|
+
between; the header, which stays inside the Container, is the constant. The title's y also
|
|
170
|
+
differs from `ListLayout` only when one page passes `breadcrumbs` and the other does not:
|
|
171
|
+
`PageHeader` puts them above the title in every layout.
|
|
172
|
+
- **`CenteredLayout`**: use this for sign-in, landing, not-found and error pages. A `max-w-md` card
|
|
173
|
+
centred at a fixed offset from the top; the one layout that does not share the left edge.
|
|
174
|
+
|
|
175
|
+
```tsx
|
|
176
|
+
<AppShell header={header}>
|
|
177
|
+
<ListLayout title="Quotes" actions={<Button>New</Button>} filters={<FilterGroup …/>}
|
|
178
|
+
isEmpty={quotes.length === 0} empty={<Empty>No quotes yet.</Empty>}>
|
|
179
|
+
{quotes.map(…)}
|
|
180
|
+
</ListLayout>
|
|
181
|
+
</AppShell>
|
|
182
|
+
```
|
|
183
|
+
|
|
184
|
+
## Light and dark
|
|
185
|
+
|
|
186
|
+
A dark theme does its colour work under `.dark`; this only decides whether `.dark` is on `<html>`.
|
|
187
|
+
Two states, light and dark. The stored choice wins; with nothing stored the system's
|
|
188
|
+
`prefers-color-scheme` decides. Which dark theme is loaded stays your CSS import.
|
|
189
|
+
|
|
190
|
+
1. **Inline `colourSchemeScript` in `<head>`**, before the stylesheet, so the class is right at
|
|
191
|
+
first paint and a light user gets no dark flash. It is a few hundred bytes of plain JavaScript:
|
|
192
|
+
|
|
193
|
+
```tsx
|
|
194
|
+
import { colourSchemeScript } from "@hackcove/ui";
|
|
195
|
+
|
|
196
|
+
<head>
|
|
197
|
+
<script dangerouslySetInnerHTML={{ __html: colourSchemeScript }} />
|
|
198
|
+
</head>;
|
|
199
|
+
```
|
|
200
|
+
|
|
201
|
+
In Astro, use `<script is:inline set:html={colourSchemeScript} />`.
|
|
202
|
+
|
|
203
|
+
2. **Drop the hard-coded `class="dark"`** from `<html>`. The script sets and removes it (and
|
|
204
|
+
`color-scheme`, so native controls and scrollbars follow). For a server-rendered `<html>`, add
|
|
205
|
+
`suppressHydrationWarning` (React) because the script changes its `class` before React hydrates.
|
|
206
|
+
|
|
207
|
+
3. **Place the toggle** wherever the header wants it:
|
|
208
|
+
|
|
209
|
+
```tsx
|
|
210
|
+
import { ColourSchemeToggle } from "@hackcove/ui";
|
|
211
|
+
|
|
212
|
+
<ColourSchemeToggle className="ml-auto" onChange={(scheme) => track(scheme)} />;
|
|
213
|
+
```
|
|
214
|
+
|
|
215
|
+
It is an `IconButton` showing the scheme you would switch *to* (a sun on a dark page, a moon on a
|
|
216
|
+
light one), labelled "Switch to light theme" / "Switch to dark theme". A click applies the scheme
|
|
217
|
+
to `<html>` and stores it under `localStorage["hackcove:colour-scheme"]`; a storage error (private
|
|
218
|
+
mode) is swallowed and the page still switches. It reads the current state from the `dark` class
|
|
219
|
+
when it mounts, so it agrees with the script and renders the same on the server and the client.
|
|
220
|
+
|
|
221
|
+
Without React, `applyColourScheme(scheme, root?)`, `readColourScheme(fallback?)` and
|
|
222
|
+
`storeColourScheme(scheme)` are exported too; `ColourScheme` is `"light" | "dark"`.
|
|
44
223
|
|
|
45
224
|
## Palette
|
|
46
225
|
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { type ReactElement, type ReactNode } from "react";
|
|
2
|
+
/**
|
|
3
|
+
* The class an app puts on its own `<html>`: it always reserves the scrollbar, so centred content
|
|
4
|
+
* does not shift between a short page and a long one. AppShell never touches `<html>` itself.
|
|
5
|
+
*/
|
|
6
|
+
export declare const appShellRootClass = "overflow-y-scroll";
|
|
7
|
+
/**
|
|
8
|
+
* The page frame: a sticky one-row header, an optional second `subnav` row, the main area and an
|
|
9
|
+
* optional footer. Header, main and footer each sit in a `Container`, so pages never set their own
|
|
10
|
+
* width or padding. `bleed` drops main's Container (the header keeps its own). Main is a flex
|
|
11
|
+
* column, so a full-height child (`SidebarLayout`) is `flex-1 min-h-0` and needs no header height.
|
|
12
|
+
*
|
|
13
|
+
* The header row is laid out by its slots, left to right: `brand`, `nav`, then `header` (anything
|
|
14
|
+
* the slots do not cover), then `controls`, pushed to the end. An app writes no wrapper or
|
|
15
|
+
* `className` for any of them.
|
|
16
|
+
*/
|
|
17
|
+
export declare function AppShell({ brand, nav, controls, header, subnav, footer, bleed, hideChromeInPrint, children, }: {
|
|
18
|
+
/** The brand: a logo and a name, in the header's brand type, with the gap between them. */
|
|
19
|
+
brand?: ReactNode;
|
|
20
|
+
/** The main navigation, e.g. `NavTabs` (it is already a `<nav>`). */
|
|
21
|
+
nav?: ReactNode;
|
|
22
|
+
/** The end of the row: a picker, a theme toggle. Gap-2, vertically centred. */
|
|
23
|
+
controls?: ReactNode;
|
|
24
|
+
/** The escape hatch: a free node in the header row, after `brand` and `nav`. */
|
|
25
|
+
header?: ReactNode;
|
|
26
|
+
/** A second fixed-height row under the header, e.g. `NavTabs`. */
|
|
27
|
+
subnav?: ReactNode;
|
|
28
|
+
footer?: ReactNode;
|
|
29
|
+
/** Main without a Container, for a full-bleed layout. */
|
|
30
|
+
bleed?: boolean;
|
|
31
|
+
/**
|
|
32
|
+
* Print only the page: the header and footer are hidden, and main's Container drops its padding
|
|
33
|
+
* (the page margin is then `@page`'s). Off by default, so a print is unchanged unless an app asks.
|
|
34
|
+
*/
|
|
35
|
+
hideChromeInPrint?: boolean;
|
|
36
|
+
children: ReactNode;
|
|
37
|
+
}): ReactElement;
|
package/dist/AppShell.js
ADDED
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { useEffect, useRef } from "react";
|
|
3
|
+
import { Container } from "./Container.js";
|
|
4
|
+
import { cx } from "./cx.js";
|
|
5
|
+
/**
|
|
6
|
+
* The class an app puts on its own `<html>`: it always reserves the scrollbar, so centred content
|
|
7
|
+
* does not shift between a short page and a long one. AppShell never touches `<html>` itself.
|
|
8
|
+
*/
|
|
9
|
+
export const appShellRootClass = "overflow-y-scroll";
|
|
10
|
+
// One fixed-height row that scrolls sideways and never wraps; its children keep their width.
|
|
11
|
+
const ROW = "flex items-center gap-4 overflow-x-auto whitespace-nowrap *:shrink-0";
|
|
12
|
+
// While a row has more to scroll to on a side, that edge fades out (a mask, so it follows any theme).
|
|
13
|
+
// `ScrollRow` sets `data-more-start` / `data-more-end` (and `data-more` for either); a row that fits
|
|
14
|
+
// has none and is untouched.
|
|
15
|
+
const FADE = "data-more-start:[--fade-start:2rem] data-more-end:[--fade-end:2rem] data-more:[mask-image:linear-gradient(to_right,transparent,#000_var(--fade-start,0px),#000_calc(100%_-_var(--fade-end,0px)),transparent)]";
|
|
16
|
+
/** A row in a Container, with the fade on whichever side still has content to scroll to. */
|
|
17
|
+
function ScrollRow({ className, children }) {
|
|
18
|
+
const row = useRef(null);
|
|
19
|
+
// Every render: the children may have changed, and each needs observing.
|
|
20
|
+
useEffect(() => {
|
|
21
|
+
const el = row.current;
|
|
22
|
+
if (!el)
|
|
23
|
+
return undefined;
|
|
24
|
+
const update = () => {
|
|
25
|
+
const start = el.scrollLeft > 1;
|
|
26
|
+
const end = el.scrollWidth - el.clientWidth - el.scrollLeft > 1;
|
|
27
|
+
el.toggleAttribute("data-more-start", start);
|
|
28
|
+
el.toggleAttribute("data-more-end", end);
|
|
29
|
+
el.toggleAttribute("data-more", start || end);
|
|
30
|
+
};
|
|
31
|
+
update();
|
|
32
|
+
el.addEventListener("scroll", update, { passive: true });
|
|
33
|
+
// jsdom (a consumer's own tests) has no ResizeObserver.
|
|
34
|
+
const resize = typeof ResizeObserver === "undefined" ? undefined : new ResizeObserver(update);
|
|
35
|
+
resize?.observe(el);
|
|
36
|
+
for (const child of el.children)
|
|
37
|
+
resize?.observe(child);
|
|
38
|
+
return () => {
|
|
39
|
+
el.removeEventListener("scroll", update);
|
|
40
|
+
resize?.disconnect();
|
|
41
|
+
};
|
|
42
|
+
});
|
|
43
|
+
return (_jsx(Container, { elementRef: (el) => void (row.current = el), className: cx(ROW, FADE, className), children: children }));
|
|
44
|
+
}
|
|
45
|
+
/** The header row's slots in order: brand, nav, the free `children`, then controls at the end. */
|
|
46
|
+
function HeaderSlots({ brand, nav, controls, children, }) {
|
|
47
|
+
return (_jsxs(_Fragment, { children: [brand && (_jsx("div", { className: "flex items-center gap-2 text-base font-bold tracking-tight text-slate-900", children: brand })), nav, children, controls && _jsx("div", { className: "ml-auto flex items-center gap-2", children: controls })] }));
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* The page frame: a sticky one-row header, an optional second `subnav` row, the main area and an
|
|
51
|
+
* optional footer. Header, main and footer each sit in a `Container`, so pages never set their own
|
|
52
|
+
* width or padding. `bleed` drops main's Container (the header keeps its own). Main is a flex
|
|
53
|
+
* column, so a full-height child (`SidebarLayout`) is `flex-1 min-h-0` and needs no header height.
|
|
54
|
+
*
|
|
55
|
+
* The header row is laid out by its slots, left to right: `brand`, `nav`, then `header` (anything
|
|
56
|
+
* the slots do not cover), then `controls`, pushed to the end. An app writes no wrapper or
|
|
57
|
+
* `className` for any of them.
|
|
58
|
+
*/
|
|
59
|
+
export function AppShell({ brand, nav, controls, header, subnav, footer, bleed = false, hideChromeInPrint = false, children, }) {
|
|
60
|
+
return (_jsxs("div", { className: "flex min-h-dvh flex-col", children: [_jsxs("header", { className: cx("sticky top-0 z-10 border-b border-slate-200 bg-white", hideChromeInPrint && "print:hidden"), children: [_jsx(ScrollRow, { className: "h-14", children: _jsx(HeaderSlots, { brand: brand, nav: nav, controls: controls, children: header }) }), subnav && _jsx(ScrollRow, { className: "h-12", children: subnav })] }), _jsx("main", { className: "flex flex-1 flex-col", children: bleed ? (children) : (_jsx(Container, { className: cx("py-8", hideChromeInPrint && "print:p-0"), children: children })) }), footer && (_jsx("footer", { className: cx("border-t border-slate-200", hideChromeInPrint && "print:hidden"), children: _jsx(Container, { className: "py-6 text-sm text-slate-500", children: footer }) }))] }));
|
|
61
|
+
}
|
package/dist/Avatar.d.ts
CHANGED
|
@@ -3,6 +3,8 @@ declare const SIZE_CLASSES: {
|
|
|
3
3
|
readonly sm: "size-6 text-xs";
|
|
4
4
|
readonly md: "size-7 text-xs";
|
|
5
5
|
readonly lg: "size-10 text-base";
|
|
6
|
+
readonly xl: "size-14 text-xl";
|
|
7
|
+
readonly "2xl": "size-24 text-3xl";
|
|
6
8
|
};
|
|
7
9
|
export type AvatarSize = keyof typeof SIZE_CLASSES;
|
|
8
10
|
/** The background class for a name: the same name gets the same colour everywhere. */
|
|
@@ -10,13 +12,16 @@ export declare function avatarColourClass(name: string): string;
|
|
|
10
12
|
/** Up to two letters or digits of a name, upper-cased; `?` when it has none. */
|
|
11
13
|
export declare function initialsOf(name: string): string;
|
|
12
14
|
/**
|
|
13
|
-
* A person as a round badge of initials
|
|
14
|
-
* another server for decoration, and it makes a
|
|
15
|
+
* A person as a round badge of initials, or as their photo when `src` is given. It never loads a
|
|
16
|
+
* remote image on its own: that is a request to another server for decoration, and it makes a
|
|
17
|
+
* screenshot depend on the network. `src` is the app's own local path.
|
|
15
18
|
*/
|
|
16
|
-
export declare function Avatar({ name, size, ring, className, ...rest }: {
|
|
19
|
+
export declare function Avatar({ name, src, size, ring, className, ...rest }: {
|
|
17
20
|
name: string;
|
|
21
|
+
/** The person's photo, a local path. It replaces the initials and is announced as `name`. */
|
|
22
|
+
src?: string;
|
|
18
23
|
size?: AvatarSize;
|
|
19
24
|
/** A ring in the page colour, for a stack of overlapping avatars. */
|
|
20
25
|
ring?: boolean;
|
|
21
|
-
} & Omit<HTMLAttributes<
|
|
26
|
+
} & Omit<HTMLAttributes<HTMLElement>, "children">): ReactElement;
|
|
22
27
|
export {};
|
package/dist/Avatar.js
CHANGED
|
@@ -14,6 +14,8 @@ const SIZE_CLASSES = {
|
|
|
14
14
|
sm: "size-6 text-xs",
|
|
15
15
|
md: "size-7 text-xs",
|
|
16
16
|
lg: "size-10 text-base",
|
|
17
|
+
xl: "size-14 text-xl",
|
|
18
|
+
"2xl": "size-24 text-3xl",
|
|
17
19
|
};
|
|
18
20
|
/** The background class for a name: the same name gets the same colour everywhere. */
|
|
19
21
|
export function avatarColourClass(name) {
|
|
@@ -30,9 +32,13 @@ export function initialsOf(name) {
|
|
|
30
32
|
.toUpperCase() || "?");
|
|
31
33
|
}
|
|
32
34
|
/**
|
|
33
|
-
* A person as a round badge of initials
|
|
34
|
-
* another server for decoration, and it makes a
|
|
35
|
+
* A person as a round badge of initials, or as their photo when `src` is given. It never loads a
|
|
36
|
+
* remote image on its own: that is a request to another server for decoration, and it makes a
|
|
37
|
+
* screenshot depend on the network. `src` is the app's own local path.
|
|
35
38
|
*/
|
|
36
|
-
export function Avatar({ name, size = "md", ring = false, className, ...rest }) {
|
|
39
|
+
export function Avatar({ name, src, size = "md", ring = false, className, ...rest }) {
|
|
40
|
+
if (src) {
|
|
41
|
+
return (_jsx("img", { src: src, alt: name, title: name, className: cx("shrink-0 rounded-full object-cover", SIZE_CLASSES[size], ring && "-ml-1.5 border-2 border-slate-50 first:ml-0", className), ...rest }));
|
|
42
|
+
}
|
|
37
43
|
return (_jsx("span", { "aria-hidden": "true", title: name, className: cx("inline-grid shrink-0 place-items-center rounded-full font-semibold text-violet-50", SIZE_CLASSES[size], avatarColourClass(name), ring && "-ml-1.5 border-2 border-slate-50 first:ml-0", className), ...rest, children: initialsOf(name) }));
|
|
38
44
|
}
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
import type { HTMLAttributes, ReactElement } from "react";
|
|
2
|
+
/** A bulleted `<ul>`: discs, indented, a small gap between items. Its children are `<li>`s (e.g. `Body as="li"`). */
|
|
3
|
+
export declare function BulletList({ className, ...rest }: HTMLAttributes<HTMLUListElement>): ReactElement;
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { cx } from "./cx.js";
|
|
3
|
+
/** A bulleted `<ul>`: discs, indented, a small gap between items. Its children are `<li>`s (e.g. `Body as="li"`). */
|
|
4
|
+
export function BulletList({ className, ...rest }) {
|
|
5
|
+
return _jsx("ul", { className: cx("list-disc space-y-1 pl-5", className), ...rest });
|
|
6
|
+
}
|
package/dist/Code.d.ts
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
import type { HTMLAttributes, ReactElement } from "react";
|
|
2
|
+
/** Inline code: mono, on a subtle inset background. It takes the surrounding text colour and size (a touch smaller, for the mono face). */
|
|
3
|
+
export declare function Code({ className, ...rest }: HTMLAttributes<HTMLElement>): ReactElement;
|
package/dist/Code.js
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { cx } from "./cx.js";
|
|
3
|
+
/** Inline code: mono, on a subtle inset background. It takes the surrounding text colour and size (a touch smaller, for the mono face). */
|
|
4
|
+
export function Code({ className, ...rest }) {
|
|
5
|
+
return (_jsx("code", { className: cx("rounded bg-slate-100 px-1 py-0.5 font-mono text-[0.9em]", className), ...rest }));
|
|
6
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { type ReactElement } from "react";
|
|
2
|
+
import { type ColourScheme } from "./colourScheme.js";
|
|
3
|
+
/** A sun/moon button that switches `<html>` between light and dark and remembers the choice. It shows the scheme you would switch to. Pair it with `colourSchemeScript` in `<head>`. */
|
|
4
|
+
export declare function ColourSchemeToggle({ className, onChange, }: {
|
|
5
|
+
className?: string;
|
|
6
|
+
onChange?: (scheme: ColourScheme) => void;
|
|
7
|
+
}): ReactElement;
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { useSyncExternalStore } from "react";
|
|
3
|
+
import { applyColourScheme, storeColourScheme } from "./colourScheme.js";
|
|
4
|
+
import { IconButton } from "./IconButton.js";
|
|
5
|
+
const sun = (_jsxs("svg", { viewBox: "0 0 24 24", className: "h-4 w-4", fill: "none", stroke: "currentColor", strokeWidth: "1.8", strokeLinecap: "round", "aria-hidden": "true", children: [_jsx("circle", { cx: "12", cy: "12", r: "4" }), _jsx("path", { d: "M12 2v2M12 20v2M4.9 4.9l1.4 1.4M17.7 17.7l1.4 1.4M2 12h2M20 12h2M4.9 19.1l1.4-1.4M17.7 6.3l1.4-1.4" })] }));
|
|
6
|
+
const moon = (_jsx("svg", { viewBox: "0 0 24 24", className: "h-4 w-4", fill: "none", stroke: "currentColor", strokeWidth: "1.8", strokeLinecap: "round", "aria-hidden": "true", children: _jsx("path", { d: "M21 12.8A9 9 0 1 1 11.2 3a7 7 0 0 0 9.8 9.8Z" }) }));
|
|
7
|
+
// The `dark` class on <html> is the source of truth: the head script set it before React ran.
|
|
8
|
+
function subscribe(onChange) {
|
|
9
|
+
const observer = new MutationObserver(onChange);
|
|
10
|
+
observer.observe(document.documentElement, { attributes: true, attributeFilter: ["class"] });
|
|
11
|
+
return () => observer.disconnect();
|
|
12
|
+
}
|
|
13
|
+
const current = () => document.documentElement.classList.contains("dark") ? "dark" : "light";
|
|
14
|
+
// What the server and the hydrating client both render; the real state follows right after.
|
|
15
|
+
const server = () => "light";
|
|
16
|
+
/** A sun/moon button that switches `<html>` between light and dark and remembers the choice. It shows the scheme you would switch to. Pair it with `colourSchemeScript` in `<head>`. */
|
|
17
|
+
export function ColourSchemeToggle({ className, onChange, }) {
|
|
18
|
+
const scheme = useSyncExternalStore(subscribe, current, server);
|
|
19
|
+
const next = scheme === "dark" ? "light" : "dark";
|
|
20
|
+
return (_jsx(IconButton, { label: `Switch to ${next} theme`, className: className, onClick: () => {
|
|
21
|
+
applyColourScheme(next);
|
|
22
|
+
storeColourScheme(next);
|
|
23
|
+
onChange?.(next);
|
|
24
|
+
}, children: next === "light" ? sun : moon }));
|
|
25
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { ReactElement, ReactNode } from "react";
|
|
2
|
+
/**
|
|
3
|
+
* The one content edge: every page's width and side padding. This file is the only place they are
|
|
4
|
+
* defined, so a header, a page and a footer inside a Container start at the same x.
|
|
5
|
+
*/
|
|
6
|
+
export declare function Container({ as: Tag, className, elementRef, children, }: {
|
|
7
|
+
as?: "div" | "main" | "header" | "footer" | "section" | "nav";
|
|
8
|
+
className?: string;
|
|
9
|
+
/** The rendered element, for a parent that measures or scrolls it. */
|
|
10
|
+
elementRef?: (element: HTMLElement | null) => void;
|
|
11
|
+
children?: ReactNode;
|
|
12
|
+
}): ReactElement;
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { cx } from "./cx.js";
|
|
3
|
+
/**
|
|
4
|
+
* The one content edge: every page's width and side padding. This file is the only place they are
|
|
5
|
+
* defined, so a header, a page and a footer inside a Container start at the same x.
|
|
6
|
+
*/
|
|
7
|
+
export function Container({ as: Tag = "div", className, elementRef, children, }) {
|
|
8
|
+
return (_jsx(Tag, { ref: elementRef, className: cx("mx-auto w-full max-w-6xl px-4 sm:px-6", className), children: children }));
|
|
9
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { getLocalTimeZone, today } from "@internationalized/date";
|
|
3
|
+
import { useContext } from "react";
|
|
4
|
+
import { Button, Calendar, CalendarCell, CalendarGrid, CalendarGridBody, CalendarGridHeader, CalendarHeaderCell, CalendarStateContext, DatePickerStateContext, Dialog, Popover, } from "react-aria-components";
|
|
5
|
+
import { cx, focusRing } from "./cx.js";
|
|
6
|
+
import { MONTH_OPTIONS, iconButtonClass } from "./DateField.js";
|
|
7
|
+
export const calendarIcon = (_jsxs("svg", { viewBox: "0 0 20 20", className: "h-4 w-4", fill: "none", stroke: "currentColor", strokeWidth: "1.6", strokeLinecap: "round", "aria-hidden": "true", children: [_jsx("rect", { x: "3", y: "4.5", width: "14", height: "12.5", rx: "2" }), _jsx("path", { d: "M3 8.5h14M7 2.5v4M13 2.5v4" })] }));
|
|
8
|
+
const chevron = (d) => (_jsx("svg", { viewBox: "0 0 20 20", className: "h-4 w-4", fill: "none", stroke: "currentColor", strokeWidth: "1.8", strokeLinecap: "round", strokeLinejoin: "round", "aria-hidden": "true", children: _jsx("path", { d: d }) }));
|
|
9
|
+
const selectClass = `rounded-md border border-slate-300 bg-white py-1 pr-1 pl-2 text-sm text-slate-900 transition hover:border-violet-400 ${focusRing}`;
|
|
10
|
+
/** ‹, a month dropdown, a year dropdown and ›: a birth date decades back is two picks away. */
|
|
11
|
+
function CalendarHeader() {
|
|
12
|
+
const state = useContext(CalendarStateContext);
|
|
13
|
+
if (!state)
|
|
14
|
+
return null;
|
|
15
|
+
const focused = state.focusedDate;
|
|
16
|
+
const now = today(getLocalTimeZone()).year;
|
|
17
|
+
const first = Math.min(state.minValue?.year ?? now - 100, focused.year);
|
|
18
|
+
const last = Math.max(state.maxValue?.year ?? now + 10, focused.year);
|
|
19
|
+
const years = Array.from({ length: last - first + 1 }, (_, i) => first + i);
|
|
20
|
+
return (_jsxs("div", { className: "flex items-center gap-1.5", children: [_jsx(Button, { slot: "previous", className: iconButtonClass, children: chevron("M12.5 15l-5-5 5-5") }), _jsx("select", { "aria-label": "Month", className: cx(selectClass, "flex-1"), value: focused.month, disabled: state.isDisabled, onChange: (event) => state.setFocusedDate(focused.set({ month: Number(event.target.value) })), children: MONTH_OPTIONS.map(({ month, name }) => (_jsx("option", { value: month, children: name }, month))) }), _jsx("select", { "aria-label": "Year", className: selectClass, value: focused.year, disabled: state.isDisabled, onChange: (event) => state.setFocusedDate(focused.set({ year: Number(event.target.value) })), children: years.map((year) => (_jsx("option", { value: year, children: year }, year))) }), _jsx(Button, { slot: "next", className: iconButtonClass, children: chevron("M7.5 5l5 5-5 5") })] }));
|
|
21
|
+
}
|
|
22
|
+
function ClearButton() {
|
|
23
|
+
const picker = useContext(DatePickerStateContext);
|
|
24
|
+
return (_jsx("button", { type: "button", className: `rounded-md px-2 py-1 text-sm font-medium text-violet-600 transition hover:bg-violet-50 ${focusRing}`, onClick: () => {
|
|
25
|
+
picker?.setValue(null);
|
|
26
|
+
picker?.close();
|
|
27
|
+
}, children: "Clear" }));
|
|
28
|
+
}
|
|
29
|
+
const cellClass = ({ isSelected, isOutsideMonth, isDisabled, isUnavailable, isInvalid, }) => {
|
|
30
|
+
let tone = "cursor-pointer text-slate-700 hover:bg-slate-100";
|
|
31
|
+
if (isSelected)
|
|
32
|
+
tone = isInvalid ? "bg-red-50 text-red-700" : "bg-violet-600 text-violet-50";
|
|
33
|
+
else if (isOutsideMonth || isDisabled || isUnavailable)
|
|
34
|
+
tone = "text-slate-300";
|
|
35
|
+
return cx("flex h-9 w-9 items-center justify-center rounded-lg text-sm tabular-nums", tone, focusRing);
|
|
36
|
+
};
|
|
37
|
+
/** The picker's popover: header, a Monday-first month grid and a Clear footer. */
|
|
38
|
+
export function CalendarPopover() {
|
|
39
|
+
return (_jsx(Popover, { placement: "bottom start", className: "rounded-xl border border-slate-200 bg-white p-3 text-slate-900 shadow-lg", children: _jsxs(Dialog, { className: "flex flex-col gap-3 outline-none", children: [_jsxs(Calendar, { firstDayOfWeek: "mon", className: "flex flex-col gap-3", children: [_jsx(CalendarHeader, {}), _jsxs(CalendarGrid, { weekdayStyle: "short", children: [_jsx(CalendarGridHeader, { children: (day) => (_jsx(CalendarHeaderCell, { className: "pb-1 text-xs font-medium text-slate-500", children: day })) }), _jsx(CalendarGridBody, { children: (date) => _jsx(CalendarCell, { date: date, className: cellClass }) })] })] }), _jsx("div", { className: "flex justify-end border-t border-slate-200 pt-2", children: _jsx(ClearButton, {}) })] }) }));
|
|
40
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { type CalendarDate } from "@internationalized/date";
|
|
2
|
+
import { type ContextType, type ReactElement, type ReactNode } from "react";
|
|
3
|
+
import { type DateFieldProps as AriaDateFieldProps, DateFieldStateContext } from "react-aria-components";
|
|
4
|
+
/** Fixed, whatever the browser's locale: DD/MM/YYYY and Monday-first weeks. */
|
|
5
|
+
export declare const LOCALE = "en-GB";
|
|
6
|
+
export declare const MONTH_OPTIONS: {
|
|
7
|
+
month: number;
|
|
8
|
+
name: string;
|
|
9
|
+
}[];
|
|
10
|
+
type Segment = NonNullable<ContextType<typeof DateFieldStateContext>>["segments"][number];
|
|
11
|
+
export type DateFieldProps = {
|
|
12
|
+
label?: ReactNode;
|
|
13
|
+
/** A hint under the field; an error replaces it. */
|
|
14
|
+
description?: ReactNode;
|
|
15
|
+
/** Replaces the built-in error message (an impossible date, or one outside min/max). */
|
|
16
|
+
errorMessage?: ReactNode;
|
|
17
|
+
className?: string;
|
|
18
|
+
} & Pick<AriaDateFieldProps<CalendarDate>, "value" | "defaultValue" | "onChange" | "minValue" | "maxValue" | "isDisabled" | "isRequired" | "name" | "aria-label">;
|
|
19
|
+
/** What the segments show, as the day segment reports it to the field. */
|
|
20
|
+
export type Read = {
|
|
21
|
+
/** The date as `YYYY-MM-DD` once every segment is filled and the day exists, else `""`. */
|
|
22
|
+
typed: string;
|
|
23
|
+
/** "February 1990 has 28 days." when the day does not exist. */
|
|
24
|
+
impossible: string | null;
|
|
25
|
+
/** Some segments are filled and some are not. */
|
|
26
|
+
partial: boolean;
|
|
27
|
+
};
|
|
28
|
+
/** One segment. The day segment also reports what the segments show to the field. */
|
|
29
|
+
export declare function FieldSegment({ segment, onRead, }: {
|
|
30
|
+
segment: Segment;
|
|
31
|
+
onRead?: (read: Read) => void;
|
|
32
|
+
}): ReactElement;
|
|
33
|
+
export declare function frameClass({ isInvalid, isFocusWithin, isDisabled, }: {
|
|
34
|
+
isInvalid: boolean;
|
|
35
|
+
isFocusWithin: boolean;
|
|
36
|
+
isDisabled: boolean;
|
|
37
|
+
}): string;
|
|
38
|
+
/** The label, the description or the error line: the shared outside of both components. */
|
|
39
|
+
export declare function Notes({ description, errorMessage, impossible, isInvalid, }: {
|
|
40
|
+
description: ReactNode;
|
|
41
|
+
errorMessage: ReactNode;
|
|
42
|
+
impossible: string | null;
|
|
43
|
+
isInvalid: boolean;
|
|
44
|
+
}): ReactElement;
|
|
45
|
+
export declare const labelClass = "text-sm font-medium text-slate-700";
|
|
46
|
+
export declare const iconButtonClass = "inline-flex h-7 w-7 shrink-0 items-center justify-center rounded-md text-slate-500 transition hover:bg-slate-100 disabled:opacity-50 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-violet-600";
|
|
47
|
+
/** A segmented DD / MM / YYYY date input. Values are `CalendarDate`s; `name` posts it as `YYYY-MM-DD`. */
|
|
48
|
+
export declare function DateField({ label, description, errorMessage, className, value, defaultValue, onChange, name, ...rest }: DateFieldProps): ReactElement;
|
|
49
|
+
export {};
|